Background / engine_v80.py
carm5333's picture
add engine_v80.py
ed00569 verified
Raw
History Blame Contribute Delete
81.7 kB
#!/usr/bin/env python3
"""
╔══════════════════════════════════════════════════════════════════════════════╗
║ Audio Enhancement Engine v8.0 — "Calibrated Precision" ║
║ المرجع: الشيخ ياسر الدوسري — 1425H ║
╠══════════════════════════════════════════════════════════════════════════════╣
║ ║
║ الأخطاء المُشخَّصة في v7.6 (Forensic Deep-Dive): ║
║ ║
║ 🔴 BUG #1 — SPECTRAL_BIAS اتجاه معكوس في 3 نطاقات حرجة ║
║ 250Hz: bias=+11 → cut بدل boost (output -7dB تحت ref!) ║
║ 4kHz: bias=-1.5 → boost بدل cut (output +5dB فوق ref!) ║
║ 8kHz: bias=-4.0 → boost بدل cut (output +10dB فوق ref!) ║
║ الإصلاح: SPECTRAL_BIAS_V8 بالاتفاقية الصحيحة (output-ref) ║
║ ║
║ 🔴 BUG #2 — Double Compand Stacking يسحق Crest ║
║ LRA compand + Main compand → ضغط مزدوج → Crest ينهار ║
║ الإصلاح: حذف LRA compand من Pass1 — Main compand وحيد ║
║ ║
║ 🟠 BUG #3 — 5 تطبيقات alimiter تطحن Crest تراكمياً ║
║ P1(×2) + P2 + P3 + P4 = 5 مرات limit=0.891 → Crest ينخفض 0.8-1.5LU ║
║ الإصلاح: WAV وسيطة = limit=0.9997 فقط | MP3 نهائي = 0.891 ║
║ ║
║ 🟠 BUG #4 — build_compand_mds يستخدم DR بدل LRA ║
║ lra_delta = damage.dr - TARGET['dr'] ← خطأ نوع! ║
║ الإصلاح: lra_delta = inp_lra - ref_fp.lra_clip (صحيح) ║
║ ║
║ 🟡 BUG #5 — Quality Gate لا يحمي Crest بشكل منفصل ║
║ عتبة 1.0 نقطة تتجاهل انهيار Crest 3+LU ║
║ الإصلاح: حارس مستقل Crest < P1-1.5LU AND < target-0.8 ║
║ ║
║ المحافظ عليه من v7.6 (Architecture سليم): ║
║ ✅ MDS System (SFM + DR + Spectral Distance + Per-Band SNR) ║
║ ✅ SFM-Adaptive NR ║
║ ✅ Full-File LRA Target 4.19 (v7.6 fix) ║
║ ✅ Dual LRA: lra_clip للـ compand | lra للـ quality score ║
║ ✅ 9-Segment Full-File Spectral Average ║
║ ✅ 4-Pass WAV Pipeline (lossless حتى Pass4) ║
║ ✅ Crest-Aware Warmth Nodes ║
║ ✅ Scipy Perceptual EQ (Bark + A-weight) ║
║ ✅ Arabic Filename Safety ║
║ ║
║ الهدف: LUFS=-6.29 RMS=-10.01 Crest=10.25 LRA=4.19 ≥96/100 ║
╚══════════════════════════════════════════════════════════════════════════════╝
"""
from __future__ import annotations
import argparse, json, os, shutil, subprocess, sys, warnings, time
from dataclasses import dataclass, field
from pathlib import Path
from typing import Dict, List, Optional, Tuple
warnings.filterwarnings('ignore')
try:
import numpy as np
from scipy.fft import rfft, rfftfreq
from scipy.optimize import minimize
from scipy.interpolate import CubicSpline
NUMPY_OK = SCIPY_OK = True
except ImportError:
NUMPY_OK = SCIPY_OK = False
# ══════════════════════════════════════════════════════════════════════════════
# CONSTANTS
# ══════════════════════════════════════════════════════════════════════════════
SR = 48000
TARGET = {
'lufs': -6.29,
'rms': -10.01,
'crest': 10.25,
'lra': 4.19, # full-file measurement (corrected in v7.6, kept in v8)
'true_peak': -1.0,
'bitrate': '320k',
'sfm': 0.0444,
'dr': 7.9,
}
REF_FILES = [
'/mnt/user-data/uploads/المرجع1425.mp3',
'/mnt/user-data/uploads/سوره_الفتح.mp3',
'/mnt/user-data/uploads/ياسر_الدوسري_ما_تسير_من_سورة_فاطر_1425__اول_مرة_تنشر_-_سعد_العنزي.mp3',
]
REF_CACHE = '/tmp/enhance_ref_fp.v80.json'
CENTERS_31 = [
20,25,31.5,40,50,63,80,100,125,160,
200,250,315,400,500,630,800,1000,1250,1600,
2000,2500,3150,4000,5000,6300,8000,10000,12500,16000,20000,
]
A_WEIGHT: Dict[float,float] = {
20:-50.5,25:-44.7,31.5:-39.4,40:-34.6,50:-30.2,
63:-26.2,80:-22.5,100:-19.1,125:-16.1,160:-13.4,
200:-10.9,250:-8.6,315:-6.6,400:-4.8,500:-3.2,
630:-1.9,800:-0.8,1000:0.0,1250:0.6,1600:1.0,
2000:1.2,2500:1.3,3150:1.2,4000:1.0,5000:0.5,
6300:-0.1,8000:-1.1,10000:-2.5,12500:-4.3,16000:-6.6,20000:-9.3,
}
# ══════════════════════════════════════════════════════════════════════════════
# v8 BUG #1 FIX — SPECTRAL_BIAS_V8: اتفاقية صحيحة موحدة
#
# الاتفاقية: bias = (output - ref)
# سالب = output تحت ref → g = -(-)*scale = موجب (boost) ✅
# موجب = output فوق ref → g = -(+)*scale = سالب (cut) ✅
#
# v7.6 كان يستخدم "رغبة في التصحيح" بدل "الخطأ المُقاس":
# 250Hz: +11 → يقطع بدل رفع (output -7dB تحت ref) ❌
# 4kHz: -1.5 → يرفع بدل قطع (output +5dB فوق ref) ❌
# 8kHz: -4.0 → يرفع بدل قطع (output +10dB فوق ref) ❌
# ══════════════════════════════════════════════════════════════════════════════
SPECTRAL_BIAS_V8: Dict[int,float] = {
# النطاقات الدنيا — محتاج رفع (output تحت ref في ملفات 64kbps)
80: -2.50, # output -2.5dB تحت ref → g=+0.625dB boost
100: -4.00, # output -4dB تحت ref → g=+1.00dB boost
125: +3.50, # output +3.5dB فوق ref → g=-0.875dB cut
200: -4.00, # output -4dB تحت ref → g=+1.00dB boost
# ↓ إصلاح v8 الحرج: كان +11.00 → يقطع عوضاً عن الرفع!
250: -7.00, # output -7dB تحت ref → g=+1.75dB boost ← v8 FIX الأكبر
315: +6.00, # output +6dB فوق ref → g=-1.50dB cut
400: -1.50, # output تحت ref → g=+0.375dB boost
500: +1.50, # output فوق ref → g=-0.375dB cut
630: -2.50, # output تحت ref → g=+0.625dB boost
800: +1.50, # output فوق ref → g=-0.375dB cut
1000: -1.00, # output تحت ref → g=+0.25dB boost (v7.55: كان يعطي قطع)
1250: +0.40, # صغير جداً → تأثير ضئيل
2000: +0.50, # صغير جداً → تأثير ضئيل
2500: +1.80, # output فوق ref → g=-0.45dB cut
3150: +1.20, # output فوق ref → g=-0.30dB cut
# ↓ إصلاح v8 الحرج: كان -1.50 → يرفع عوضاً عن القطع!
4000: +5.00, # output +5dB فوق ref → g=-1.25dB cut ← v8 FIX
# ↓ إصلاح v8: كان -0.80 و-0.90 → يرفع في منطقة مرتفعة أصلاً
5000: +0.80, # output فوق ref → g=-0.20dB cut ← v8 FIX (was -0.80)
6300: +0.90, # output فوق ref → g=-0.225dB cut ← v8 FIX (was -0.90)
# ↓ إصلاح v8 الحرج: كان -4.00 → يرفع عوضاً عن القطع!
8000: +8.00, # output +8-10dB فوق ref → g=-2.00dB cut ← v8 FIX الأكبر
10000: -2.00, # output تحت ref (rolloff) → g=+0.50dB boost
}
BIAS_SCALE = 0.25 # الصيغة: g = round(-bias_db * BIAS_SCALE, 2)
# ══════════════════════════════════════════════════════════════════════════════
# DATA CLASSES
# ══════════════════════════════════════════════════════════════════════════════
@dataclass
class ReferenceFingerprint:
third_oct: Dict[float,float] = field(default_factory=dict)
a_weighted: Dict[float,float] = field(default_factory=dict)
rms: float = TARGET['rms']
peak: float = TARGET['true_peak']
crest: float = TARGET['crest']
lra: float = TARGET['lra']
lra_clip: float = 2.94
tilt_slope: float = 0.0
warmth_ratio: float = 0.0
sfm: float = TARGET['sfm']
dr: float = TARGET['dr']
n_files: int = 0
@dataclass
class DamageProfile:
"""v7.6 Multi-Metric Damage Score (MDS) — محافَظ عليه في v8"""
snr: float = 30.0
sfm: float = 0.05
dr: float = 8.0
hf_deficit: float = 0.0
spectral_dist: float = 0.0
crest: float = 10.0
src_br: int = 128000
band_snr: Dict[float,float] = field(default_factory=dict)
mds: float = 0.0
nr_intensity: float = 0.0
compand_score: float = 0.0
has_ringing: bool = False
rolloff_hz: float = 20000.0
quality_label: str = 'GOOD'
@dataclass
class QualityReport:
score: float = 0.0
spectral: float = 0.0
lufs: float = 0.0
crest: float = 0.0
lra: float = 0.0
warmth: float = 0.0
hf: float = 0.0
avg_err: float = 99.0
warmth_tilt: float = 0.0
warmth_ref: float = 0.0
lra_target: float = TARGET['lra']
notes: List[str] = field(default_factory=list)
# ══════════════════════════════════════════════════════════════════════════════
# AUDIO I/O
# ══════════════════════════════════════════════════════════════════════════════
def _safe(path:str) -> Tuple[str,Optional[str]]:
try: path.encode('ascii'); return path,None
except UnicodeEncodeError:
import uuid as _u
ext=os.path.splitext(path)[1] or '.mp3'
tmp=f'/tmp/v80_safe_{_u.uuid4().hex[:8]}{ext}'
shutil.copy2(path,tmp); return tmp,tmp
def load_audio(path:str,sr:int=SR,mono:bool=True,
skip:int=0,duration:Optional[int]=None) -> 'np.ndarray':
sp,tc=_safe(path)
cmd=['ffmpeg','-i',sp]
if skip>0: cmd+=['-ss',str(skip)]
if duration: cmd+=['-t',str(duration)]
cmd+=['-f','s16le','-ac','1' if mono else '2',
'-ar',str(sr),'-loglevel','error','-']
r=subprocess.run(cmd,capture_output=True)
if tc:
try: os.remove(tc)
except: pass
if not r.stdout: raise RuntimeError(f'فشل تحميل: {path}')
return np.frombuffer(r.stdout,np.int16).astype(np.float32)/32768.0
def probe(path:str) -> Dict:
sp,tc=_safe(path)
r=subprocess.run(['ffprobe','-v','quiet','-print_format','json',
'-show_streams','-show_format',sp],
capture_output=True,text=True)
if tc:
try: os.remove(tc)
except: pass
return json.loads(r.stdout) if r.returncode==0 else {}
def measure_lufs(path:str) -> float:
sp,tc=_safe(path)
r=subprocess.run(['ffmpeg','-i',sp,'-af','ebur128=peak=true',
'-f','null','-','-loglevel','info'],
capture_output=True,text=True)
if tc:
try: os.remove(tc)
except: pass
for line in r.stderr.split('\n'):
s=line.strip()
if s.startswith('I:') and 'LUFS' in s and 'LRA' not in s:
try: return float(s.split('I:')[1].strip().split()[0])
except: pass
return -99.0
# ══════════════════════════════════════════════════════════════════════════════
# SIGNAL METRICS
# ══════════════════════════════════════════════════════════════════════════════
def rms_db(a:'np.ndarray') -> float:
return float(20*np.log10(np.sqrt(np.mean(a**2))+1e-10))
def peak_db(a:'np.ndarray') -> float:
return float(20*np.log10(np.max(np.abs(a))+1e-10))
def crest_factor(a:'np.ndarray') -> float:
return float(peak_db(a)-rms_db(a))
def lra_estimate(a:'np.ndarray',sr:int=SR) -> float:
n=int(0.4*sr); step=n//2
lvls=np.array([20*np.log10(np.sqrt(np.mean(a[i:i+n]**2))+1e-10)
for i in range(0,len(a)-n,step)])
if len(lvls)<2: return 0.0
active=lvls[lvls>np.max(lvls)-30]
return float(np.percentile(active,95)-np.percentile(active,10)) if len(active)>=2 else 0.0
def snr_estimate(a:'np.ndarray',sr:int=SR) -> float:
n=int(0.1*sr)
blocks=np.array([np.sqrt(np.mean(a[i:i+n]**2)) for i in range(0,len(a)-n,n)])
if len(blocks)<4: return 30.0
return float(20*np.log10(np.percentile(blocks,85)/(np.percentile(blocks,3)+1e-10)))
def count_clips(a:'np.ndarray',thr:float=0.99) -> int:
return int(np.sum(np.abs(a)>=thr))
def declip(audio:'np.ndarray',thr:float=0.98) -> Tuple['np.ndarray',int]:
clipped=np.abs(audio)>=thr; nc=int(np.sum(clipped))
if nc==0: return audio,0
out=audio.copy(); n=len(audio)
diff=np.diff(clipped.astype(int))
starts=np.where(diff==1)[0]+1; ends=np.where(diff==-1)[0]+1
if clipped[0]: starts=np.insert(starts,0,0)
if clipped[-1]: ends=np.append(ends,n)
for s,e in zip(starts,ends):
ctx=40; pre=np.arange(max(0,s-ctx),s); post=np.arange(e,min(n,e+ctx))
good=np.concatenate([pre,post])
if len(good)<4: continue
try:
cs=CubicSpline(good,audio[good],extrapolate=True)
out[np.arange(s,e)]=cs(np.arange(s,e))
except: pass
return out,nc
# ══════════════════════════════════════════════════════════════════════════════
# SPECTRAL ANALYSIS
# ══════════════════════════════════════════════════════════════════════════════
def third_octave(audio:'np.ndarray',sr:int=SR,
chunk_sec:int=45,a_weighted:bool=False) -> Dict[float,float]:
chunk=audio[:sr*chunk_sec] if len(audio)>sr*chunk_sec else audio
N=len(chunk); spec=np.abs(rfft(chunk)); freqs=rfftfreq(N,1.0/sr)
out:Dict[float,float]={}
for fc in CENTERS_31:
if fc>=sr/2: continue
fl=fc/(2**(1/6)); fh=fc*(2**(1/6))
mask=(freqs>=fl)&(freqs<fh)
if mask.sum()>0:
v=float(20*np.log10(np.mean(spec[mask])+1e-10))
if a_weighted and fc in A_WEIGHT: v+=A_WEIGHT[fc]
out[fc]=v
return out
def hf_status(bands:Dict[float,float]) -> str:
hfk=[f for f in bands if f>=8000]
if not hfk: return 'absent'
avg=float(np.mean([bands[f] for f in hfk]))
return 'good' if avg>10 else 'weak' if avg>-5 else 'absent'
def detect_hf_rolloff(bands:Dict[float,float],drop:float=12.0) -> float:
fs=sorted([f for f in bands if 1600<=f<=20000])
if not fs: return 20000.0
prev=bands[fs[0]]
for fc in fs[1:]:
curr=bands[fc]
if prev-curr>drop: return float(fc)
prev=curr
return 20000.0
def spectral_tilt(bands:Dict[float,float],lo:float=100.0,hi:float=10000.0) -> float:
fc_arr=np.array([fc for fc in CENTERS_31 if lo<=fc<=hi and fc in bands],dtype=float)
if len(fc_arr)<3: return 0.0
return float(np.polyfit(np.log2(fc_arr/1000.0),
np.array([bands[fc] for fc in fc_arr]),1)[0])
def warmth_tilt(bands:Dict[float,float]) -> float:
return spectral_tilt(bands,200.0,2000.0)
def merge_eq(nodes:List[Tuple],gap:float=50.0) -> List[Tuple]:
if not nodes: return nodes
nodes=sorted(nodes,key=lambda x:x[0]); merged=[list(nodes[0])]
for f0,g,Q in nodes[1:]:
pf,pg,pq=merged[-1]
if abs(f0-pf)<gap:
total=float(np.clip(pg+g,-16,16))
avg_f=(pf*abs(pg)+f0*abs(g))/(abs(pg)+abs(g)+1e-6)
merged[-1]=[round(avg_f,0),round(total,2),round((pq+Q)/2,2)]
else: merged.append([f0,g,Q])
return [tuple(x) for x in merged]
# ══════════════════════════════════════════════════════════════════════════════
# STEP 1a — SPECTRAL FLATNESS MEASURE (SFM)
# ══════════════════════════════════════════════════════════════════════════════
def compute_sfm(audio:'np.ndarray',sr:int=SR,
f_lo:float=100.0,f_hi:float=8000.0) -> float:
"""Wiener entropy — مقياس نظافة الطيف | REF 1425H: 0.044"""
chunk=audio[:sr*30] if len(audio)>sr*30 else audio
N=len(chunk); spec=np.abs(rfft(chunk))**2; freqs=rfftfreq(N,1.0/sr)
mask=(freqs>=f_lo)&(freqs<=f_hi)
s=spec[mask]
if len(s)<10: return 0.1
eps=1e-10
geo=float(np.exp(np.mean(np.log(s+eps))))
arith=float(np.mean(s))
return float(np.clip(geo/(arith+eps),0.0,1.0))
# ══════════════════════════════════════════════════════════════════════════════
# STEP 1b — DYNAMIC RANGE SCORE
# ══════════════════════════════════════════════════════════════════════════════
def compute_dynamic_range(audio:'np.ndarray',sr:int=SR) -> float:
"""نطاق الديناميك الفعلي (20ms frames) | REF: 7.9dB"""
n=int(0.020*sr)
frames=np.array([float(np.sqrt(np.mean(audio[i:i+n]**2)))
for i in range(0,len(audio)-n,n)])
if len(frames)<10: return 8.0
frames_db=20*np.log10(frames+1e-10)
return float(np.percentile(frames_db,95)-np.percentile(frames_db,5))
# ══════════════════════════════════════════════════════════════════════════════
# STEP 1c — SPECTRAL SHAPE DISTANCE FROM REFERENCE
# ══════════════════════════════════════════════════════════════════════════════
def compute_spectral_distance(inp_b:Dict,ref_fp:ReferenceFingerprint,
hf_rolloff:float=20000.0) -> float:
"""المسافة الطيفية الحقيقية من المرجع بعد level normalization"""
ref_b=ref_fp.third_oct
ceil=min(10000.0,hf_rolloff*0.9)
common=[fc for fc in inp_b if fc in ref_b and 80<=fc<=ceil]
if len(common)<4: return 20.0
out_arr=np.array([inp_b[fc] for fc in common])
ref_arr=np.array([ref_b[fc] for fc in common])
loff=float(np.mean(ref_arr-out_arr))
shape_diffs=np.abs((ref_arr-out_arr)-loff)
aw=np.array([max(0.2,1+A_WEIGHT.get(fc,0)/10) for fc in common])
return float(np.sum(aw*shape_diffs)/np.sum(aw))
# ══════════════════════════════════════════════════════════════════════════════
# STEP 1d — CODEC DAMAGE FINGERPRINT (Per-Band SNR)
# ══════════════════════════════════════════════════════════════════════════════
def compute_band_snr(audio:'np.ndarray',sr:int=SR) -> Dict[float,float]:
"""SNR لكل Bark band — يُحدد أين تحتاج NR"""
N=len(audio); spec=np.abs(rfft(audio))**2; freqs=rfftfreq(N,1.0/sr)
band_snr={}
for fc in [125,250,500,1000,2000,4000,8000]:
fl=fc*0.7; fh=fc*1.4
mask=(freqs>=fl)&(freqs<fh)
if mask.sum()<4: continue
s=spec[mask]
snr=float(10*np.log10(np.percentile(s,85)/(np.percentile(s,5)+1e-30)+1e-10))
band_snr[float(fc)]=snr
return band_snr
# ══════════════════════════════════════════════════════════════════════════════
# MULTI-METRIC DAMAGE SCORE (MDS)
# ══════════════════════════════════════════════════════════════════════════════
def compute_mds(snr:float,sfm:float,dr:float,hf_deficit:float,
spectral_dist:float,src_br:int,
ref_sfm:float=TARGET['sfm'],
ref_dr:float=TARGET['dr']) -> float:
"""
Multi-Metric Damage Score: 0=مثالي | 100=أسوأ حالة
أوزان: SNR 25% | SFM 25% | Spectral Distance 20% | HF 15% | DR 10% | BR 5%
"""
snr_score =float(np.clip((30.0-snr)/30.0,0,1))*100
sfm_ratio =sfm/(ref_sfm+1e-6)
sfm_score =float(np.clip((sfm_ratio-1.0)/5.0,0,1))*100
spec_score =float(np.clip(spectral_dist/15.0,0,1))*100
hf_score =float(np.clip(hf_deficit/30.0,0,1))*100
dr_excess =max(0.0,dr-ref_dr)
dr_score =float(np.clip(dr_excess/8.0,0,1))*100
br_score =float(np.clip((128000-src_br)/100000,0,1))*100
mds=(snr_score*0.25+sfm_score*0.25+spec_score*0.20+
hf_score*0.15+dr_score*0.10+br_score*0.05)
return float(np.clip(mds,0,100))
def mds_to_label(mds:float) -> str:
if mds>=75: return 'EXTREME'
if mds>=55: return 'VERY_POOR'
if mds>=35: return 'POOR'
if mds>=18: return 'FAIR'
return 'GOOD'
# ══════════════════════════════════════════════════════════════════════════════
# STEP 2a — SFM-ADAPTIVE NR
# ══════════════════════════════════════════════════════════════════════════════
def build_nr_sfm(damage:DamageProfile) -> List[str]:
"""NR intensity = f(SFM ratio) — محافَظ عليه من v7.6"""
parts:List[str]=[]
if damage.src_br < 96000 and damage.band_snr.get(1000.0,20) < 8:
if damage.has_ringing and damage.rolloff_hz < 17000:
parts.append(f'lowpass=f={int(damage.rolloff_hz*0.97)}:poles=2')
return parts
sfm_ratio=damage.sfm/(TARGET['sfm']+1e-6)
if sfm_ratio >= 5.0:
parts.append('afftdn=nr=20:nf=-55:tn=1')
parts.append('afftdn=nr=6:nf=-65:tn=1')
elif sfm_ratio >= 3.0:
nr=int(np.interp(sfm_ratio,[3,5],[12,20]))
parts.append(f'afftdn=nr={nr}:nf=-58:tn=1')
elif sfm_ratio >= 2.0:
nr=int(np.interp(sfm_ratio,[2,3],[6,12]))
parts.append(f'afftdn=nr={nr}:nf=-62:tn=1')
elif sfm_ratio >= 1.5:
parts.append('afftdn=nr=4:nf=-68:tn=1')
if damage.has_ringing and damage.rolloff_hz < 17000:
parts.append(f'lowpass=f={int(damage.rolloff_hz*0.97)}:poles=2')
return parts
# ══════════════════════════════════════════════════════════════════════════════
# STEP 2b — v8 BUG #4 FIX — DR-CALIBRATED COMPAND (LRA delta صحيح)
# ══════════════════════════════════════════════════════════════════════════════
def build_compand_mds(damage:DamageProfile,
ref_fp:ReferenceFingerprint,
inp_lra:float) -> Tuple[str,float,str,float]:
"""
v8 FIX #4: compand selection باستخدام LRA الفعلي بدل DR
v7.6 كان: lra_delta = damage.dr - TARGET['dr'] ← خطأ نوع!
v8: lra_delta = inp_lra - ref_fp.lra_clip ← صحيح
"""
crest_delta = damage.crest - TARGET['crest']
# v8 FIX: LRA excess الحقيقي مقارنة بالـ clip reference
lra_delta = max(0.0, inp_lra - ref_fp.lra_clip)
# MDS contribution — مقلّص من 3.0 إلى 2.0 لتجنب overweighting
mds_contrib = damage.mds / 100.0 * 2.0
score = crest_delta * 0.72 + lra_delta * 0.20 + mds_contrib
if score >= 11:
return ("-90/-68|-45/-20|-28/-9|-14/-4.5|-7/-2.0|-3/-0.6|0/-0.1",
2.5,'EXTREME',2.0)
elif score >= 6.5:
return ("-90/-72|-42/-21|-26/-10.5|-13/-5.2|-6/-2.4|-2.5/-0.8|-0.5/-0.3|0/-0.1",
3.2,'HEAVY',1.8)
elif score >= 3.5:
return ("-90/-78|-40/-25|-22/-12.5|-12/-6.8|-6/-3.5|-2.5/-1.6|-0.8/-0.5|0/-0.2",
2.5,'MEDIUM',1.4)
elif score >= 1.5:
return ("-90/-85|-40/-36|-20/-17|-10/-8.2|-5/-4.1|-2/-1.6|-0.5/-0.4|0/-0.3",
1.2,'LIGHT',0.9)
elif score >= 0.5:
return ("-90/-89|-40/-39|-20/-19.5|-10/-9.8|-4/-3.9|-1/-0.95|0/-0.3",
0.4,'MINIMAL',0.4)
else:
return ("-90/-90|-20/-20|-3/-3|0/0",0.0,'BYPASS',0.0)
# ══════════════════════════════════════════════════════════════════════════════
# STEP 2c — v8 BUG #1 FIX — CORRECTED SPECTRAL BIAS
# ══════════════════════════════════════════════════════════════════════════════
def build_bias_filter(hf_rolloff:float=20000.0) -> str:
"""
v8 FIX #1: SPECTRAL_BIAS_V8 باتفاقية صحيحة موحدة
الصيغة: g = -bias * BIAS_SCALE
bias سالب → g موجب (boost) | bias موجب → g سالب (cut)
"""
parts=[]
for fc,bias_db in SPECTRAL_BIAS_V8.items():
if fc > hf_rolloff * 0.9: continue
g=round(-bias_db * BIAS_SCALE, 2)
if abs(g) >= 0.20: # عتبة أقل من v7.6 (0.25) للتقاط تصحيحات أدق
Q=0.65 if abs(g)>1.5 else 0.90
parts.append(f'equalizer=f={fc}:width_type=q:width={Q}:g={g}')
return ','.join(parts)
# ══════════════════════════════════════════════════════════════════════════════
# STEP 2d — SPECTRAL DISTANCE EQ (Perceptual Optimizer)
# ══════════════════════════════════════════════════════════════════════════════
def optimize_eq(new_b:Dict,ref_fp:ReferenceFingerprint,
n_nodes:int=12,max_db:float=6.0,
shape_only:bool=False,hf_rolloff:float=20000.0) -> List[Tuple]:
"""scipy optimizer — يُصحح المسافة الطيفية الحقيقية | محافَظ عليه من v7.6"""
ref_b=ref_fp.third_oct
ceil=min(12000.0,hf_rolloff*0.92)
common=sorted([fc for fc in new_b if fc in ref_b and 63<=fc<=ceil])
if len(common)<4: return []
fc_arr=np.array(common,dtype=float)
new_arr=np.array([new_b[fc] for fc in common])
ref_arr=np.array([ref_b[fc] for fc in common])
loff=float(np.mean(ref_arr-new_arr))
target=(ref_arr-new_arr)-loff
if shape_only: target=target-float(np.mean(target))
def baw(fc:float) -> float:
bw=2.0 if 500<=fc<=4000 else 1.6 if 200<=fc<500 else 1.4 if 4000<fc<=8000 else 0.9
return bw*max(0.3,1+A_WEIGHT.get(fc,0)/10)
aw=np.array([baw(fc) for fc in common])
init_f=np.logspace(np.log10(63),np.log10(ceil),n_nodes)
def resp(fa,p):
r=np.zeros(len(fa))
for i in range(n_nodes):
f0=abs(p[i*3])+1e-6; g=p[i*3+1]; Q=max(0.3,abs(p[i*3+2]))
rat=fa/f0; r+=g/(1+Q**2*(rat-1.0/(rat+1e-9))**2)
return r
def obj(p):
e=np.mean(aw*(resp(fc_arr,p)-target)**2)
gs=[p[i*3+1] for i in range(n_nodes)]
sm=sum(0.012*(gs[i+1]-gs[i])**2 for i in range(len(gs)-1))
mg=sum(0.002*g**2 for g in gs)
return e+sm+mg
ig=np.interp(np.log10(init_f),np.log10(fc_arr),target)
x0=[]
for f,g in zip(init_f,ig):
x0.extend([float(np.clip(f,63,ceil)),float(np.clip(g,-max_db,max_db)),1.0])
res=minimize(obj,x0,method='L-BFGS-B',
bounds=[(63,ceil),(-max_db,max_db),(0.3,4.5)]*n_nodes,
options={'maxiter':500,'ftol':1e-10,'gtol':1e-9})
nodes=[]
for i in range(n_nodes):
f0=abs(res.x[i*3]); g=res.x[i*3+1]; Q=max(0.3,abs(res.x[i*3+2]))
if shape_only and g<0 and 400<=f0<=1600: g=max(g,-2.0)
if abs(g)>=0.35: nodes.append((round(f0,0),round(g,2),round(Q,2)))
return sorted(nodes,key=lambda x:x[0])
# ══════════════════════════════════════════════════════════════════════════════
# WARMTH CORRECTION (Crest-Aware — محافَظ عليه من v7.55/v7.6)
# ══════════════════════════════════════════════════════════════════════════════
def build_warmth_nodes(inp_b:Dict,ref_fp:ReferenceFingerprint,
hf_rolloff:float=20000.0,post_compand:bool=False,
current_crest:float=99.0) -> List[Tuple]:
tfc=np.array([fc for fc in CENTERS_31 if 200<=fc<=2000
and fc in inp_b and fc<hf_rolloff],dtype=float)
if len(tfc)<3: return []
tdb=np.array([inp_b[fc] for fc in tfc])
new_tilt=float(np.polyfit(np.log2(tfc/1000.0),tdb,1)[0])
tilt_diff=ref_fp.warmth_ratio-new_tilt
threshold=1.5 if not post_compand else 2.5
if abs(tilt_diff)<threshold: return []
# حارس Crest (من v7.55 — محافَظ عليه)
if current_crest-TARGET['crest']<-1.0: return []
max_adj=min(3.0,2.0+(current_crest-TARGET['crest'])*0.5) if not post_compand else 1.2
scale=0.35 if not post_compand else 0.20
nodes=[]
adj=float(np.clip(tilt_diff*scale,-max_adj,max_adj))
if abs(adj)>=0.4: nodes.append((200.0,round(adj,2),0.55))
madj=float(np.clip(-tilt_diff*0.12,-1.5,1.5))
if abs(madj)>=0.3: nodes.append((1000.0,round(madj,2),0.80))
return nodes
# ══════════════════════════════════════════════════════════════════════════════
# SPECTRAL CORRECTION (Conservative — محافَظ عليه من v7.6)
# ══════════════════════════════════════════════════════════════════════════════
def spectral_correction(out_b:Dict,ref_fp:ReferenceFingerprint,
hf_rolloff:float=20000.0,max_db:float=3.0,
pass_num:int=2,current_crest:float=99.0) -> List[Tuple]:
ref_b=ref_fp.third_oct
ceil=min(10000.0,hf_rolloff*0.9)
common=sorted([fc for fc in out_b if fc in ref_b and 80<=fc<=ceil])
if len(common)<4: return []
out_arr=np.array([out_b[fc] for fc in common])
ref_arr=np.array([ref_b[fc] for fc in common])
loff=float(np.mean(ref_arr-out_arr))
shape=(ref_arr-out_arr)-loff
avg_err=float(np.mean(np.abs(shape)))
base=0.58 if avg_err>4.0 else 0.48 if avg_err>2.0 else 0.35
pm={2:1.00,3:0.70,4:0.45}.get(pass_num,1.00)
scale=base*pm
aw=np.array([max(0.3,1+A_WEIGHT.get(fc,0)/10) for fc in common])
tfc=np.array([fc for fc in common if 200<=fc<=2000],dtype=float)
warmth_ok=False
if len(tfc)>=3:
tdb=np.array([out_b[fc] for fc in tfc])
out_tw=float(np.polyfit(np.log2(tfc/1000.0),tdb,1)[0])
warmth_ok=abs(out_tw-ref_fp.warmth_ratio)<3.0
crest_headroom=current_crest-TARGET['crest']
nodes:List[Tuple]=[]; prev_g=0.0
for i,fc in enumerate(common):
raw_g=float(shape[i])
g=float(np.clip(raw_g*aw[i]*scale,-max_db,max_db))
if warmth_ok and 400<=fc<=1600 and g<-0.3 and abs(raw_g)<4.0:
g=max(g*0.08,-0.20)
if crest_headroom<-1.5 and fc<=400 and g>0:
g=g*0.20
if abs(g)>=0.28 and abs(g-prev_g)<5.0:
Q=1.2 if abs(g)<2 else 0.85
nodes.append((float(fc),round(g,2),Q))
prev_g=g
return nodes
# ══════════════════════════════════════════════════════════════════════════════
# QUALITY SCORE (محافَظ عليه — ref_fp.lra = 4.19 صحيح منذ v7.6)
# ══════════════════════════════════════════════════════════════════════════════
def quality_score(out_b:Dict,ref_fp:ReferenceFingerprint,
metrics:Dict,hf_rolloff:float=20000.0) -> Tuple[float,QualityReport]:
ref_b=ref_fp.third_oct
ceil=min(10000,int(hf_rolloff*0.85))
common=[fc for fc in out_b if fc in ref_b and 80<=fc<=ceil]
if common:
out_v=np.array([out_b[fc] for fc in common])
ref_v=np.array([ref_b[fc] for fc in common])
aw=np.array([max(0.2,1+A_WEIGHT.get(fc,0)/10) for fc in common])
loff=float(np.mean(ref_v-out_v))
diffs=np.abs((ref_v-out_v)-loff)
w_avg=float(np.sum(aw*diffs)/np.sum(aw))
spectral_s=max(0.0,100.0-w_avg*4.5)
else:
w_avg=99.0; spectral_s=0.0
lufs_e=abs(metrics.get('lufs',-20)-TARGET['lufs'])
crest_e=abs(metrics.get('crest',15)-TARGET['crest'])
lra_t=ref_fp.lra # 4.19 — صحيح في v7.6 وv8
lra_e=abs(metrics.get('lra',8)-lra_t)
lufs_s=max(0.0,100.0-lufs_e*12)
crest_s=max(0.0,100.0-crest_e*8)
lra_s=max(0.0,100.0-lra_e*10)
tfc=np.array([fc for fc in CENTERS_31 if 200<=fc<=2000
and fc in out_b and fc<hf_rolloff],dtype=float)
out_tilt=(float(np.polyfit(np.log2(tfc/1000.0),
np.array([out_b[fc] for fc in tfc]),1)[0])
if len(tfc)>=3 else 0.0)
warmth_s=max(0.0,100.0-abs(out_tilt-ref_fp.warmth_ratio)*5.5)
hf_fcs=[fc for fc in [4000,5000,6300,8000,10000,12500]
if fc<hf_rolloff and fc in out_b and fc in ref_b]
if len(hf_fcs)>=3:
hf_o=np.array([out_b[fc] for fc in hf_fcs])
hf_r=np.array([ref_b[fc] for fc in hf_fcs])
hf_e=float(np.mean(np.abs((hf_r-hf_o)-float(np.mean(hf_r-hf_o)))))
hf_s=max(0.0,100.0-hf_e*4)
else: hf_s=50.0
total=(spectral_s*0.38+lufs_s*0.20+crest_s*0.15+
lra_s*0.12+warmth_s*0.10+hf_s*0.05)
notes=[]
if lufs_e>0.6: notes.append(f"LUFS:{metrics.get('lufs',-99):.2f}{TARGET['lufs']}")
if crest_e>1.2: notes.append(f"Crest:{metrics.get('crest',0):.2f}{TARGET['crest']}")
if lra_e>1.0: notes.append(f"LRA:{metrics.get('lra',0):.2f}{lra_t:.2f}")
if w_avg>2.5: notes.append(f"Spectral:±{w_avg:.2f}dB")
rpt=QualityReport(
score=round(total,1),spectral=round(spectral_s,1),
lufs=round(lufs_s,1),crest=round(crest_s,1),lra=round(lra_s,1),
warmth=round(warmth_s,1),hf=round(hf_s,1),
avg_err=round(w_avg,2),warmth_tilt=round(out_tilt,2),
warmth_ref=round(ref_fp.warmth_ratio,2),lra_target=round(lra_t,2),
notes=notes)
return rpt.score,rpt
# ══════════════════════════════════════════════════════════════════════════════
# 9-SEGMENT FULL-FILE SPECTRAL AVERAGE
# ══════════════════════════════════════════════════════════════════════════════
def analyze_full_spectrum(input_path:str,total_s:int) -> Dict[float,float]:
pcts=[0.10,0.20,0.30,0.40,0.50,0.60,0.70,0.80,0.90]
skips=[max(10,int(total_s*p)) for p in pcts]
clips=[f'/tmp/v80_seg{i}.wav' for i in range(len(skips))]
procs=[subprocess.Popen(['ffmpeg','-y','-i',input_path,
'-ss',str(sk),'-t','20','-f','s16le','-ac','1',
'-ar',str(SR),cl,'-loglevel','error'])
for sk,cl in zip(skips,clips)]
for p in procs: p.wait()
all_bands:List[Dict]=[]
for cl in clips:
try:
if not os.path.exists(cl) or os.path.getsize(cl)<SR*2: continue
a=np.frombuffer(open(cl,'rb').read(),np.int16).astype(np.float32)/32768.0
if len(a)<SR*3: continue
all_bands.append(third_octave(a,a_weighted=False))
except: pass
if not all_bands: return {}
result={}
for fc in CENTERS_31:
vals=[b.get(fc) for b in all_bands if b.get(fc) is not None]
if len(vals)>=2: result[fc]=float(np.median(vals))
return result
# ══════════════════════════════════════════════════════════════════════════════
# REFERENCE FINGERPRINT
# ══════════════════════════════════════════════════════════════════════════════
def get_reference_fingerprint() -> ReferenceFingerprint:
primary=REF_FILES[0]
if os.path.exists(REF_CACHE):
try:
mref=os.path.getmtime(primary) if os.path.exists(primary) else 0
if os.path.getmtime(REF_CACHE)>=mref:
with open(REF_CACHE,'r',encoding='utf-8') as f: d=json.load(f)
if d.get('version')=='v8.0':
fp=ReferenceFingerprint()
fp.third_oct ={float(k):v for k,v in d['third_oct'].items()}
fp.a_weighted ={float(k):v for k,v in d.get('a_weighted',{}).items()}
fp.rms=d['rms']; fp.peak=d['peak']
fp.crest=d['crest']
fp.lra=d.get('lra',TARGET['lra'])
fp.lra_clip=d.get('lra_clip',2.94)
fp.tilt_slope=d['tilt_slope']
fp.warmth_ratio=d['warmth_ratio']
fp.sfm=d.get('sfm',TARGET['sfm'])
fp.dr=d.get('dr',TARGET['dr'])
fp.n_files=d.get('n_files',1)
return fp
except: pass
SAFE_PCT=[0.12,0.22,0.33,0.44,0.55,0.66,0.77,0.88]
all_fp:List[Dict]=[]
for idx,path in enumerate(REF_FILES):
if not os.path.exists(path): continue
try:
p_info=probe(path)
total_s=int(float(p_info.get('format',{}).get('duration',300)))
skips=[max(15,int(total_s*r)) for r in SAFE_PCT]
clips=[f'/tmp/ref80_f{idx}_s{i}.wav' for i in range(len(skips))]
procs=[subprocess.Popen(['ffmpeg','-y','-i',path,
'-ss',str(sk),'-t','30','-f','s16le','-ac','1',
'-ar',str(SR),cl,'-loglevel','error'])
for sk,cl in zip(skips,clips)]
for p in procs: p.wait()
segs_spec=[]; segs_rms=[]; segs_crest=[]; segs_lra=[]
segs_sfm=[]; segs_dr=[]
for cl in clips:
try:
if not os.path.exists(cl) or os.path.getsize(cl)<SR*2: continue
a=np.frombuffer(open(cl,'rb').read(),np.int16).astype(np.float32)/32768.0
if len(a)<SR*3: continue
segs_spec.append(third_octave(a,a_weighted=False))
segs_rms.append(rms_db(a)); segs_crest.append(crest_factor(a))
segs_lra.append(lra_estimate(a))
segs_sfm.append(compute_sfm(a))
segs_dr.append(compute_dynamic_range(a))
except: pass
if len(segs_spec)>=4:
common=[fc for fc in CENTERS_31 if all(fc in s for s in segs_spec)]
all_fp.append({
'spec':{fc:float(np.median([s[fc] for s in segs_spec])) for fc in common},
'rms':float(np.median(segs_rms)),
'crest':float(np.median(segs_crest)),
'lra_clip':float(np.median(segs_lra)),
'sfm':float(np.median(segs_sfm)),
'dr':float(np.median(segs_dr)),
})
except: continue
if len(all_fp)<2:
fp=_build_single_ref(primary) if os.path.exists(primary) else ReferenceFingerprint()
fp.n_files=len(all_fp) or 1; return fp
ref_lvl=float(np.mean([f['rms'] for f in all_fp]))
common_all=[fc for fc in CENTERS_31 if all(fc in f['spec'] for f in all_fp)]
normed=[{fc:f['spec'][fc]+(ref_lvl-f['rms']) for fc in common_all} for f in all_fp]
multi={fc:float(np.median([s[fc] for s in normed])) for fc in common_all}
fp=ReferenceFingerprint()
fp.third_oct =multi
fp.rms =float(np.median([f['rms'] for f in all_fp]))
fp.peak =TARGET['true_peak']
fp.crest =float(np.median([f['crest'] for f in all_fp]))
fp.lra_clip =float(np.median([f['lra_clip'] for f in all_fp]))
fp.lra =TARGET['lra'] # 4.19 hardcoded full-file (corrected in v7.6)
fp.sfm =float(np.median([f['sfm'] for f in all_fp]))
fp.dr =float(np.median([f['dr'] for f in all_fp]))
fp.n_files =len(all_fp)
fp.tilt_slope =spectral_tilt(fp.third_oct)
fp.warmth_ratio=warmth_tilt(fp.third_oct)
try:
p_info=probe(primary)
ts=int(float(p_info.get('format',{}).get('duration',300)))
pa=load_audio(primary,skip=int(ts*0.35),duration=60)
fp.a_weighted=third_octave(pa,a_weighted=True)
except: pass
try:
d={
'version':'v8.0',
'third_oct':{str(k):v for k,v in fp.third_oct.items()},
'a_weighted':{str(k):v for k,v in fp.a_weighted.items()},
'rms':fp.rms,'peak':fp.peak,'crest':fp.crest,
'lra':fp.lra,'lra_clip':fp.lra_clip,
'tilt_slope':fp.tilt_slope,'warmth_ratio':fp.warmth_ratio,
'sfm':fp.sfm,'dr':fp.dr,'n_files':fp.n_files,
}
with open(REF_CACHE,'w',encoding='utf-8') as f:
json.dump(d,f,ensure_ascii=False,indent=2)
except: pass
return fp
def _build_single_ref(path:str) -> ReferenceFingerprint:
p_info=probe(path); total_s=int(float(p_info.get('format',{}).get('duration',300)))
skips=[max(10,int(total_s*r)) for r in [0.12,0.28,0.44,0.60,0.78]]
clips=[f'/tmp/ref80_s{i}.wav' for i in range(len(skips))]
procs=[subprocess.Popen(['ffmpeg','-y','-i',path,'-ss',str(sk),'-t','40',
'-f','s16le','-ac','1','-ar',str(SR),cl,'-loglevel','error'])
for sk,cl in zip(skips,clips)]
for p in procs: p.wait()
segs=[]
for cl in clips:
try:
a=np.frombuffer(open(cl,'rb').read(),np.int16).astype(np.float32)/32768.0
if len(a)>SR: segs.append(a)
except: pass
ref=np.concatenate(segs) if segs else load_audio(path,skip=30,duration=120)
chunk_n=SR*20; all_b=[]; crests=[]; lras=[]; sfms=[]; drs=[]
for ci in range(max(1,len(ref)//chunk_n)):
seg=ref[ci*chunk_n:(ci+1)*chunk_n]
if len(seg)<SR*3: continue
all_b.append(third_octave(seg)); crests.append(crest_factor(seg))
lras.append(lra_estimate(seg)); sfms.append(compute_sfm(seg))
drs.append(compute_dynamic_range(seg))
if not all_b: all_b=[third_octave(ref)]
fp=ReferenceFingerprint()
for fc in CENTERS_31:
vals=[b.get(fc) for b in all_b if b.get(fc) is not None]
if vals: fp.third_oct[fc]=float(np.median(vals))
fp.rms=rms_db(ref); fp.peak=peak_db(ref)
fp.crest =float(np.median(crests)) if crests else crest_factor(ref)
fp.lra_clip=float(np.median(lras)) if lras else lra_estimate(ref)
fp.lra =TARGET['lra']
fp.sfm =float(np.median(sfms)) if sfms else TARGET['sfm']
fp.dr =float(np.median(drs)) if drs else TARGET['dr']
fp.tilt_slope=spectral_tilt(fp.third_oct)
fp.warmth_ratio=warmth_tilt(fp.third_oct)
fp.n_files=1; return fp
# ══════════════════════════════════════════════════════════════════════════════
# LUFS SAMPLING
# ══════════════════════════════════════════════════════════════════════════════
def sample_lufs_7pt(input_path:str,total_s:int,n_ch:str,filter_str:str) -> float:
pts=[max(10,int(total_s*p)) for p in [0.08,0.18,0.32,0.50,0.65,0.78,0.90]]
for i in range(1,len(pts)):
if pts[i]-pts[i-1]<30: pts[i]=pts[i-1]+30
clips=[f'/tmp/v80_lc{i}.wav' for i in range(len(pts))]
procs=[subprocess.Popen(['ffmpeg','-y','-i',input_path,
'-ss',str(sk),'-t','22','-ar','48000','-ac',n_ch,
cl,'-loglevel','error']) for sk,cl in zip(pts,clips)]
for p in procs: p.wait()
lp=[subprocess.Popen(['ffmpeg','-y','-i',cl,'-af',
filter_str+',ebur128=peak=true','-f','null','-','-loglevel','info'],
stderr=subprocess.PIPE,stdout=subprocess.PIPE) for cl in clips]
vals:List[float]=[]
for p in lp:
_,err=p.communicate()
for line in err.decode().split('\n'):
s=line.strip()
if s.startswith('I:') and 'LUFS' in s and 'LRA' not in s:
try: vals.append(float(s.split('I:')[1].strip().split()[0])); break
except: pass
if not vals: return -12.0
gw=[0.05,0.12,0.20,0.26,0.20,0.12,0.05][:len(vals)]
gw=[w/sum(gw) for w in gw]
return float(np.average(vals,weights=gw))
# ══════════════════════════════════════════════════════════════════════════════
# FILTER CHAIN BUILDER — v8 BUG #2 + #3 FIX
# ══════════════════════════════════════════════════════════════════════════════
def build_filter_chain(eq_nodes:List[Tuple],compand_pts:str,makeup:float,
hf:str,is_mono:bool,gain_db:float,
nr_parts:List[str],bias_filter:str,
warmth_pre:List[Tuple],intensity:str,
inp_lra:float,damage:DamageProfile,
hf_rolloff:float,src_sr:int,
ref_lra_clip:float,tilt_slope:float,
correction_nodes:List[Tuple]=None,
post_warmth:List[Tuple]=None) -> str:
"""
v8 FIX #2: حذف LRA compand من الـ chain — compand رئيسي واحد فقط
v8 FIX #3: alimiter واحد ناعم (0.9997) في WAV الوسيطة بدل 0.891
"""
if correction_nodes is None: correction_nodes=[]
if post_warmth is None: post_warmth=[]
parts:List[str]=[]; is_bypass=(intensity=='BYPASS')
is_extreme=(damage.quality_label=='EXTREME')
# 1. DC blocking
parts.append('highpass=f=28:poles=2')
# 2. NR
parts.extend(nr_parts)
# 3. Tilt correction
if not is_bypass and abs(tilt_slope)>1.0:
db=min(abs(tilt_slope)*0.35,5.0)
if tilt_slope>0: parts.append(f'treble=g={db:.1f}:f=8000:width_type=o:width=2')
else: parts.append(f'bass=g={db:.1f}:f=150:width_type=o:width=2')
# 4. Perceptual EQ (scipy Bark + A-weight)
for f0,g,Q in eq_nodes:
parts.append(f'equalizer=f={f0:.0f}:width_type=q:width={Q}:g={g}')
# 5. Warmth pre-compand (Crest-aware)
for f0,g,Q in warmth_pre:
parts.append(f'equalizer=f={f0:.0f}:width_type=q:width={Q}:g={g}')
# 6. HF reconstruction
if not is_bypass:
if is_extreme:
parts.append('crystalizer=i=7')
if hf_rolloff<8000:
parts.append(f'treble=g=3.5:f={max(2000,int(hf_rolloff*0.65))}:width_type=o:width=2')
parts.append('treble=g=3.0:f=7000:width_type=o:width=1.5')
elif damage.quality_label=='VERY_POOR':
parts.append('crystalizer=i=6')
parts.append('treble=g=2.0:f=6000:width_type=o:width=2')
elif damage.quality_label in ('POOR','GOOD','FAIR'):
if hf=='weak': parts.append('crystalizer=i=4')
elif hf=='absent': parts.append('crystalizer=i=6')
# 7. Post-NR
if not is_bypass and nr_parts:
if is_extreme: parts.append('afftdn=nr=4:nf=-80:tn=0')
elif damage.quality_label in ('POOR','VERY_POOR'):
parts.append('afftdn=nr=6:nf=-74:tn=0')
elif damage.snr<40:
nr=max(3,min(10,int((40.0-damage.snr)*0.4)))
parts.append(f'afftdn=nr={nr}:nf=-74:tn=1')
# ══════════════════════════════════════════════════════════════════
# v8 FIX #2: LRA CONTROL — agate للـ LRA المنخفض فقط
#
# v7.6 كان يضيف compand ثانٍ عندما inp_lra > ref_lra_clip
# هذا كان يسبب double compression → Crest collapse
#
# v8: نحذف LRA compand تماماً
# - Low LRA (lra_deficit > 0.5): agate لتوسيع الديناميك فقط
# - High LRA (lra_deficit < -0.4): نتركها للـ compand الرئيسي
# الرعاية المتبقية تتم في Pass 3 feedback
# ══════════════════════════════════════════════════════════════════
lra_deficit=ref_lra_clip-inp_lra
if not is_bypass and lra_deficit>0.5:
# LRA منخفض جداً → agate لتوسيع النطاق الديناميكي
ratio=min(4.0 if is_extreme else 3.5,
1.0+lra_deficit*(0.40 if is_extreme else 0.28))
thr=max(0.010,min(0.045,0.022+lra_deficit*0.004))
rel=1200 if is_extreme else 800
parts.append(f'agate=threshold={thr:.3f}:ratio={ratio:.2f}'
f':attack=20:release={rel}:makeup=1.0:range=0.06')
# v8 FIX: لا يوجد elif lra_deficit<-0.4 هنا!
# LRA مرتفع → Main compand يعالجه | Pass 3 يعالج الباقي
# 8. Single clean compand (v7.0 proven architecture)
if not is_bypass:
atk={'MINIMAL':0.050,'LIGHT':0.030,'MEDIUM':0.015,'HEAVY':0.008,'EXTREME':0.020}
dcy={'MINIMAL':3.0, 'LIGHT':2.0, 'MEDIUM':1.0, 'HEAVY':0.5, 'EXTREME':0.40}
parts.append(f'compand=attacks={atk.get(intensity,0.015)}'
f':decays={dcy.get(intensity,1.0)}'
f':points={compand_pts}:gain={makeup}')
# 9. Post-compand warmth (Crest-aware)
for f0,g,Q in post_warmth:
parts.append(f'equalizer=f={f0:.0f}:width_type=q:width={Q}:g={g}')
# 10. Spectral bias (v8 corrected)
if not is_bypass and bias_filter:
parts.append(bias_filter)
# 11. Correction nodes
for f0,g,Q in correction_nodes:
parts.append(f'equalizer=f={f0:.0f}:width_type=q:width={Q}:g={g}')
# v8 FIX #3: حذف alimiter الوسيط (0.978/0.982) من هنا!
# v7.6 كان يضيف: alimiter=limit=0.978:attack=5:release=40 هنا
# هذا كان يسحق Crest قبل أن يصل للـ alimiter الرئيسي
# 12. Volume
if abs(gain_db)>0.05: parts.append(f'volume={gain_db:.3f}dB')
# 13. Stereo
if is_mono: parts.append('aformat=channel_layouts=stereo')
# v8 FIX #3: alimiter ناعم جداً للـ WAV الوسيطة (يحمي من overflow فقط)
# True Peak الحقيقي (0.891) يُطبَّق في Pass 4 فقط
if is_bypass:
parts.append('alimiter=limit=0.999:level=false:attack=5:release=15')
else:
parts.append('alimiter=limit=0.9997:level=false:attack=10:release=100')
return ','.join(f'\n {p}' for p in parts)
# ══════════════════════════════════════════════════════════════════════════════
# MAIN ENHANCE — v8.0
# ══════════════════════════════════════════════════════════════════════════════
def enhance(input_path:str,output_path:str,
max_iterations:int=3,target_score:float=96.0) -> Dict:
log:List[str]=[]; t0=time.time()
def L(m:str='') -> None: print(m); log.append(m)
_input_tmp=None
try: input_path.encode('ascii')
except UnicodeEncodeError:
import uuid as _u2; ext=os.path.splitext(input_path)[1] or '.mp3'
_input_tmp=f'/tmp/v80_in_{_u2.uuid4().hex[:8]}{ext}'
shutil.copy2(input_path,_input_tmp); input_path=_input_tmp
L(f"╔{'═'*70}╗")
L(f"║ Audio Enhancement Engine v8.0 — \"Calibrated Precision\" ║")
L(f"║ المرجع: الشيخ ياسر الدوسري — 1425H ║")
L(f"║ الإصلاحات: BIAS_SIGN ✓ | NO_STACKING ✓ | SINGLE_LIMITER ✓ ║")
L(f"╚{'═'*70}╝")
L(f" الملف: {os.path.basename(input_path)}")
# ── Reference ────────────────────────────────────────────────────────────
L(f"\n[١] بصمة المرجع v8.0 (MDS: SFM + DR + Spectral Distance)...")
ref_fp=get_reference_fingerprint()
L(f" ✓ {ref_fp.n_files} سورة | RMS={ref_fp.rms:.2f} Crest={ref_fp.crest:.2f}"
f" LRA={ref_fp.lra:.2f}(full)/{ref_fp.lra_clip:.2f}(clip)")
L(f" ✓ SFM={ref_fp.sfm:.4f} DR={ref_fp.dr:.1f}dB"
f" Warmth={ref_fp.warmth_ratio:.2f} Tilt={ref_fp.tilt_slope:.2f}")
# ── File Analysis ─────────────────────────────────────────────────────────
L(f"\n[٢] تحليل شامل (MDS: 4 مقاييس)...")
pr=probe(input_path)
stream=pr.get('streams',[{}])[0]
is_mono=stream.get('channels',2)==1
src_sr=int(stream.get('sample_rate',44100))
src_br=int(stream.get('bit_rate',128000))
total_s=int(float(pr.get('format',{}).get('duration',300)))
n_ch='1' if is_mono else '2'; skip_s=min(30,total_s//4)
full_b=analyze_full_spectrum(input_path,total_s)
inp=load_audio(input_path,skip=skip_s,duration=45)
inp_b=full_b if full_b else third_octave(inp,a_weighted=False)
inp_rms=rms_db(inp); inp_crest=crest_factor(inp)
inp_lra=lra_estimate(inp); inp_hf=hf_status(inp_b)
hf_freqs=[fc for fc in inp_b if fc>=8000]
hf_avg=float(np.mean([inp_b[fc] for fc in hf_freqs])) if hf_freqs else -80.0
ref_hf=float(np.mean([ref_fp.third_oct.get(fc,-60) for fc in hf_freqs])) if hf_freqs else -40.0
hf_deficit=ref_hf-hf_avg
hf_rolloff=max(detect_hf_rolloff(inp_b,12.0),2000.0)
# Step 1a: SFM
inp_sfm=compute_sfm(inp)
sfm_ratio=inp_sfm/(ref_fp.sfm+1e-6)
L(f" Step 1a — SFM={inp_sfm:.4f} (ref={ref_fp.sfm:.4f}, ratio={sfm_ratio:.1f}x)")
# Step 1b: DR
inp_dr=compute_dynamic_range(inp)
L(f" Step 1b — DR={inp_dr:.1f}dB (ref={ref_fp.dr:.1f}dB, excess={inp_dr-ref_fp.dr:+.1f})")
# Step 1c: Spectral distance
spec_dist=compute_spectral_distance(inp_b,ref_fp,hf_rolloff)
L(f" Step 1c — Spectral distance=±{spec_dist:.2f}dB from 1425H")
# Step 1d: Per-band SNR
band_snr=compute_band_snr(inp)
snr_global=float(np.mean(list(band_snr.values()))) if band_snr else 30.0
# MDS
mds=compute_mds(snr_global,inp_sfm,inp_dr,hf_deficit,spec_dist,src_br,
ref_fp.sfm,ref_fp.dr)
quality_label=mds_to_label(mds)
has_ringing=(src_br<65000)
br_rolloff=15500.0 if src_br<65000 else 16500.0 if src_br<97000 else hf_rolloff
damage=DamageProfile(
snr=snr_global,sfm=inp_sfm,dr=inp_dr,hf_deficit=hf_deficit,
spectral_dist=spec_dist,crest=inp_crest,src_br=src_br,
band_snr=band_snr,mds=mds,has_ringing=has_ringing,
rolloff_hz=br_rolloff,quality_label=quality_label
)
L(f"\n MDS={mds:.1f}/100 → {quality_label}")
L(f" Crest={inp_crest:.2f} LRA={inp_lra:.2f} BR={src_br//1000}kbps"
f" Ringing={'✓' if has_ringing else '✗'}")
# ── STEP 2: MDS-Driven Processing ─────────────────────────────────────────
L(f"\n[٣] Step 2a — SFM-Adaptive NR...")
use_nr=(src_br>=96000) and (snr_global>=8.0)
nr_parts=build_nr_sfm(damage) if use_nr or has_ringing else (
[f'lowpass=f={int(br_rolloff*0.97)}:poles=2'] if has_ringing else [])
L(f"\n[٤] Step 2b — MDS-Calibrated Compand (v8 FIX: LRA delta صحيح)...")
# v8 FIX #4: نمرر inp_lra للدالة
compand_pts,makeup,intensity,calib=build_compand_mds(damage,ref_fp,inp_lra)
lra_delta_actual=max(0.0,inp_lra-ref_fp.lra_clip)
L(f" Compand={intensity} Makeup=+{makeup:.1f}dB calib={calib:+.1f}dB"
f" (LRA_delta={lra_delta_actual:.2f} | MDS={mds:.0f})")
L(f"\n[٥] Step 2d — Spectral Distance EQ (scipy Bark)...")
max_eq_db=4.0 if quality_label=='EXTREME' else 5.0 if quality_label=='VERY_POOR' else 6.0
n_eq=12 if quality_label=='EXTREME' else 10
eq_nodes=optimize_eq(inp_b,ref_fp,n_nodes=n_eq,max_db=max_eq_db,
shape_only=(quality_label=='EXTREME'),hf_rolloff=hf_rolloff)
warmth_pre=build_warmth_nodes(inp_b,ref_fp,hf_rolloff,
post_compand=False,current_crest=inp_crest)
eq_all=merge_eq(sorted(eq_nodes+warmth_pre,key=lambda x:x[0]),60.0)
eq_all=[(f,float(np.clip(g,-max_eq_db,max_eq_db)),q) for f,g,q in eq_all]
eq_final=[]
for f0,g,q in eq_all:
if f0>=hf_rolloff and g>0:
if f0<hf_rolloff*1.5: eq_final.append((f0,min(-0.5,g*-0.3),q))
else: eq_final.append((f0,g,q))
eq_nodes_cur=eq_final
L(f" {len(eq_nodes_cur)} نقطة EQ (±{max_eq_db}dB)")
inp_tilt=spectral_tilt(inp_b); tilt_corr=ref_fp.tilt_slope-inp_tilt
# v8 FIX #1: build_bias_filter يستخدم SPECTRAL_BIAS_V8 المُصحَّح
bias_filter=build_bias_filter(hf_rolloff)
L(f" Bias: 250Hz→+{abs(-(-7.00)*BIAS_SCALE):.2f}dB↑ | "
f"4kHz→{-(5.00*BIAS_SCALE):.2f}dB↓ | "
f"8kHz→{-(8.00*BIAS_SCALE):.2f}dB↓ (v8 FIX)")
# ── STEP 3: Do-No-Harm Convergence ────────────────────────────────────────
L(f"\n{'─'*70}")
L(f" STEP 3 — Do-No-Harm Convergence (v8 — Single Compand + Single Limiter)")
L(f"{'─'*70}")
best_score=0.0; best_path=None
for iteration in range(max_iterations):
L(f"\n ◆ تكرار {iteration+1}/{max_iterations}")
# LUFS 7-point
L(f" [٦] LUFS 7-point Gaussian...")
chain0=build_filter_chain(
eq_nodes_cur,compand_pts,makeup,inp_hf,is_mono,0.0,
nr_parts,bias_filter,[],intensity,inp_lra,damage,
hf_rolloff,src_sr,ref_fp.lra_clip,tilt_corr
).replace('\n','').replace(' ','')
l0=sample_lufs_7pt(input_path,total_s,n_ch,chain0)
gain_needed=float(np.clip(TARGET['lufs']-l0-calib,-18,12))
L(f" LUFS_7pt={l0:.2f} gain={gain_needed:+.2f}dB")
# Pass 1 → WAV
L(f" [٧] Pass 1 → WAV (single compand, soft limiter)...")
tmp_p1='/tmp/v80_p1.wav'
chain1=build_filter_chain(
eq_nodes_cur,compand_pts,makeup,inp_hf,is_mono,gain_needed,
nr_parts,bias_filter,[],intensity,inp_lra,damage,
hf_rolloff,src_sr,ref_fp.lra_clip,tilt_corr
).replace('\n','').replace(' ','')
r1=subprocess.run(['ffmpeg','-y','-i',input_path,'-af',
chain1+',ebur128=peak=true',
'-ar','48000','-ac','2',tmp_p1,'-loglevel','info'],
capture_output=True,text=True)
lufs_p1=-99.0
for line in r1.stderr.split('\n'):
s=line.strip()
if s.startswith('I:') and 'LUFS' in s and 'LRA' not in s:
try: lufs_p1=float(s.split('I:')[1].strip().split()[0]); break
except: pass
if lufs_p1==-99.0: lufs_p1=gain_needed+l0
p1_a=load_audio(tmp_p1,skip=skip_s,duration=45)
p1_b=third_octave(p1_a)
p1_crest=crest_factor(p1_a)
p1_m={'lufs':lufs_p1,'rms':rms_db(p1_a),'crest':p1_crest,'lra':lra_estimate(p1_a)}
s1,_=quality_score(p1_b,ref_fp,p1_m,hf_rolloff)
L(f" LUFS={lufs_p1:.2f} RMS={p1_m['rms']:.2f} Crest={p1_crest:.2f}"
f" LRA={p1_m['lra']:.2f} Score={s1}")
# Pass 2 → WAV (spectral correction + warmth)
L(f" [٨] Pass 2 → WAV (Crest-aware spectral correction)...")
lufs_corr=TARGET['lufs']-lufs_p1
p1_adj={fc:v+lufs_corr for fc,v in p1_b.items()}
max_c2=2.5 if quality_label=='EXTREME' else 3.0
corr2=spectral_correction(p1_adj,ref_fp,hf_rolloff,max_c2,2,p1_crest)
pw2=build_warmth_nodes(p1_adj,ref_fp,hf_rolloff,True,p1_crest)
p2_parts=[f'volume={lufs_corr:.3f}dB']
for f0,g,Q in corr2:
p2_parts.append(f'equalizer=f={f0:.0f}:width_type=q:width={Q}:g={g}')
for f0,g,Q in pw2:
p2_parts.append(f'equalizer=f={f0:.0f}:width_type=q:width={Q}:g={g}')
# v8 FIX #3: WAV وسيطة = limit ناعم جداً (يحفظ Crest)
p2_parts.append('alimiter=limit=0.9997:level=false:attack=10:release=100')
tmp_p2='/tmp/v80_p2.wav'
subprocess.run(['ffmpeg','-y','-i',tmp_p1,'-af',','.join(p2_parts),
'-ar','48000','-ac','2',tmp_p2,'-loglevel','error'],
capture_output=True)
p2_a=load_audio(tmp_p2,skip=skip_s,duration=45)
p2_b=third_octave(p2_a)
p2_crest=crest_factor(p2_a); p2_lra=lra_estimate(p2_a); p2_rms=rms_db(p2_a)
p2_m={'lufs':TARGET['lufs'],'rms':p2_rms,'crest':p2_crest,'lra':p2_lra}
s2,_=quality_score(p2_b,ref_fp,p2_m,hf_rolloff)
L(f" RMS={p2_rms:.2f} Crest={p2_crest:.2f} LRA={p2_lra:.2f} Score={s2}")
L(f" Crest P1→P2: {p1_crest:.2f}{p2_crest:.2f} (Δ={p1_crest-p2_crest:+.2f}LU)")
# ══════════════════════════════════════════════════════════════
# v8 FIX #5: Quality Gate مع حارس Crest مستقل
# ══════════════════════════════════════════════════════════════
crest_collapsed=(p2_crest < p1_crest-1.5 and
p2_crest < TARGET['crest']-0.8)
score_regressed=(s2 < s1-1.0)
if score_regressed or crest_collapsed:
reason=('Crest انهار' if crest_collapsed else f'Score P2({s2})<P1({s1})')
L(f" ⚠ Quality Gate v8 [{reason}] → رجوع لـ P1")
shutil.copy(tmp_p1,tmp_p2); p2_b=p1_b
p2_crest=p1_crest; p2_lra=p1_m['lra']
p2_rms=p1_m['rms']; p2_m=p1_m; s2=s1
# Pass 3 → WAV (LRA + RMS feedback)
L(f" [٩] Pass 3 → WAV (LRA+RMS feedback)...")
p3_parts=[]
lra_gap=ref_fp.lra-p2_lra # target=4.19
rms_gap=ref_fp.rms-p2_rms
# v8: LRA gate هنا فقط (ليس في Pass 1) + حارس Crest
if lra_gap>0.3 and p2_crest>TARGET['crest']-1.5:
if lra_gap<0.8: thr,ratio,rel=0.020,1.8,700
elif lra_gap<1.5: thr,ratio,rel=0.026,2.2,580
else: thr,ratio,rel=0.032,2.6,450
p3_parts.append(f'agate=threshold={thr:.3f}:ratio={ratio:.1f}'
f':attack=15:release={rel}:makeup=1.0:range=0.08')
rms_leak=lra_gap*0.14 if lra_gap>0.3 else 0.0
rms_adj=float(np.clip((rms_gap+rms_leak)*0.45,-1.2,1.2))
if abs(rms_adj)>0.12:
p3_parts.append(f'volume={rms_adj:.3f}dB')
corr3=spectral_correction(p2_b,ref_fp,hf_rolloff,1.8,3,p2_crest)
for f0,g,Q in corr3:
p3_parts.append(f'equalizer=f={f0:.0f}:width_type=q:width={Q}:g={g}')
# v8 FIX #3: WAV وسيطة = limit ناعم
p3_parts.append('alimiter=limit=0.9997:level=false:attack=10:release=100')
tmp_p3='/tmp/v80_p3.wav'
if len(p3_parts)>1:
subprocess.run(['ffmpeg','-y','-i',tmp_p2,'-af',','.join(p3_parts),
'-ar','48000','-ac','2',tmp_p3,'-loglevel','error'],
capture_output=True)
else:
shutil.copy(tmp_p2,tmp_p3)
p3_a=load_audio(tmp_p3,skip=skip_s,duration=45)
p3_b=third_octave(p3_a)
p3_crest=crest_factor(p3_a); p3_lufs=measure_lufs(tmp_p3)
p3_m={'lufs':p3_lufs,'rms':rms_db(p3_a),'crest':p3_crest,'lra':lra_estimate(p3_a)}
s3,_=quality_score(p3_b,ref_fp,p3_m,hf_rolloff)
if s3<s2-1.0:
L(f" ⚠ Quality Gate: P3({s3})<P2({s2}) → رجوع لـ P2")
shutil.copy(tmp_p2,tmp_p3); p3_b=p2_b
p3_lufs=TARGET['lufs']; p3_crest=p2_crest; s3=s2
# Pass 4 → MP3 (True Peak Compliance)
# v8 FIX #3: alimiter=0.891 هنا فقط — التطبيق الوحيد الحقيقي
lufs_trim=TARGET['lufs']-p3_lufs
p4_parts=[]
if abs(lufs_trim)>0.08: p4_parts.append(f'volume={lufs_trim:.3f}dB')
if s3<93.0:
c4=spectral_correction(p3_b,ref_fp,hf_rolloff,0.8,4,p3_crest)
for f0,g,Q in c4:
p4_parts.append(f'equalizer=f={f0:.0f}:width_type=q:width={Q}:g={g}')
# التطبيق الوحيد الحقيقي لـ True Peak limiter
p4_parts.append('alimiter=limit=0.891:level=false:attack=1:release=15')
tmp_out=f'/tmp/v80_out_{iteration}.mp3'
subprocess.run(['ffmpeg','-y','-i',tmp_p3,'-af',','.join(p4_parts),
'-b:a','320k','-ar','48000','-ac','2',
tmp_out,'-loglevel','error'],capture_output=True)
out_a=load_audio(tmp_out,skip=skip_s,duration=45)
out_b_=third_octave(out_a)
out_lufs=measure_lufs(tmp_out)
out_m={'lufs':out_lufs,'rms':rms_db(out_a),
'crest':crest_factor(out_a),'lra':lra_estimate(out_a)}
fs,fbd=quality_score(out_b_,ref_fp,out_m,hf_rolloff)
L(f"\n ★ P1={s1}→P2={s2}→P3={s3}→Final={fs}/100")
L(f" LUFS={out_m['lufs']:.2f} RMS={out_m['rms']:.2f}"
f" Crest={out_m['crest']:.2f} LRA={out_m['lra']:.2f}")
if fs>best_score: best_score=fs; best_path=tmp_out
if fs>=target_score: L(f" ✅ هدف {target_score} محقق!"); break
# Adaptive EQ
if iteration<max_iterations-1:
rv={fc:ref_fp.third_oct[fc] for fc in ref_fp.third_oct
if fc in out_b_ and 100<=fc<=10000}
ov={fc:out_b_[fc] for fc in rv}
loff_i=float(np.mean([rv[fc]-ov[fc] for fc in rv]))
sr_={fc:(rv[fc]-ov[fc])-loff_i for fc in rv}
avg_se=float(np.mean(np.abs(list(sr_.values()))))
scale=0.20 if avg_se>3.0 else 0.13 if avg_se>1.5 else 0.07
big=sorted([(fc,v) for fc,v in sr_.items() if abs(v)>2.5],
key=lambda x:-abs(x[1]))[:3]
if big:
corr=[(fc,round(v*scale,2),1.5) for fc,v in big]
eq_nodes_cur=merge_eq(list(eq_nodes_cur)+corr,60.0)
eq_nodes_cur=[(f,float(np.clip(g,-max_eq_db,max_eq_db)),q)
for f,g,q in eq_nodes_cur]
L(f" 🔄 {len(corr)} EQ adaptive (avg_error={avg_se:.1f}dB, scale={scale:.0%})")
# Finalize
shutil.copy(best_path if best_path else tmp_out,output_path)
if _input_tmp and os.path.exists(_input_tmp):
try: os.remove(_input_tmp)
except: pass
fin_a=load_audio(output_path,skip=skip_s,duration=45)
fin_b=third_octave(fin_a)
fin_lufs=measure_lufs(output_path)
fin_m={'lufs':fin_lufs,'rms':rms_db(fin_a),
'crest':crest_factor(fin_a),'lra':lra_estimate(fin_a)}
top_s,top_bd=quality_score(fin_b,ref_fp,fin_m,hf_rolloff)
elapsed=time.time()-t0
L(f"\n{'═'*70}")
L(f" FINAL REPORT — v8.0 ({elapsed:.0f}s)")
L(f"{'═'*70}")
L(f" {'المقياس':<18} {'المدخل':>8} {'المخرج':>8} {'الهدف 1425H':>12}")
L(f" {'─'*52}")
L(f" {'LUFS':<18} {'N/A':>8} {fin_m['lufs']:>8.2f} {TARGET['lufs']:>12.2f}")
L(f" {'RMS (dBFS)':<18} {inp_rms:>8.2f} {fin_m['rms']:>8.2f} {ref_fp.rms:>12.2f}")
L(f" {'Crest (LU)':<18} {inp_crest:>8.2f} {fin_m['crest']:>8.2f} {TARGET['crest']:>12.2f}")
L(f" {'LRA (LU)':<18} {inp_lra:>8.2f} {fin_m['lra']:>8.2f} {ref_fp.lra:>12.2f}")
L(f" {'MDS Score':<18} {mds:>8.1f} {'→':>8} {'0 (perfect)':>12}")
bar='█'*int(top_s/5)+'░'*(20-int(top_s/5))
L()
L(f" ★ {bar} {top_s}/100"
f" {'✅ EXCELLENT' if top_s>=96 else '✅ PASS' if top_s>=92 else '✓' if top_s>=88 else '⚠'}")
L(f" Spectral:{top_bd.spectral} LUFS:{top_bd.lufs} Crest:{top_bd.crest}"
f" LRA:{top_bd.lra} Warmth:{top_bd.warmth} HF:{top_bd.hf}")
L(f" خطأ طيفي:±{top_bd.avg_err}dB MDS:{mds:.1f}/100({quality_label})")
for n in top_bd.notes: L(f" ⚠ {n}")
# v8 Bug fixes summary in log
L()
L(f" v8 Fixes Applied:")
L(f" ✓ BIAS 250Hz: +1.75dB boost (was -2.75dB cut in v7.6)")
L(f" ✓ BIAS 4kHz: -1.25dB cut (was +0.375dB boost in v7.6)")
L(f" ✓ BIAS 8kHz: -2.00dB cut (was +1.00dB boost in v7.6)")
L(f" ✓ No LRA stacking (single compand only)")
L(f" ✓ Single True Peak limiter in Pass 4 only")
L(f" ✓ LRA delta uses inp_lra vs lra_clip (was DR in v7.6)")
L(); L(f" ✅ {output_path}"); L(f"{'═'*70}\n")
return {
'score':top_s,'breakdown':top_bd,'final_metrics':fin_m,
'input_metrics':{'rms':inp_rms,'crest':inp_crest,'lra':inp_lra,
'snr':snr_global,'sfm':inp_sfm,'dr':inp_dr,'mds':mds},
'quality_tier':quality_label,'mds':mds,
'hf_rolloff_hz':hf_rolloff,'ref_lra':ref_fp.lra,
'iterations':iteration+1,'log':log,
}
def enhance_auto(input_path:str,output_path:str,
max_iterations:int=3,target_score:float=96.0) -> Dict:
return enhance(input_path,output_path,max_iterations,target_score)
def process_batch(input_dir:str,output_dir:str) -> None:
in_p=Path(input_dir); out_p=Path(output_dir); out_p.mkdir(parents=True,exist_ok=True)
files=sorted([f for f in in_p.iterdir()
if f.suffix.lower() in {'.mp3','.wav','.m4a','.flac'}])
if not files: print("لا توجد ملفات"); return
scores=[]; results=[]
for i,f in enumerate(files,1):
dst=out_p/(f.stem+'_1425h_v8.mp3'); print(f"\n[{i}/{len(files)}] {f.name}")
try:
r=enhance_auto(str(f),str(dst))
sc=r.get('score',0); scores.append(sc)
results.append({'file':f.name,'score':sc,'status':'ok'}); print(f" ✅ {sc}/100")
except Exception as e:
results.append({'file':f.name,'score':0,'status':'error','error':str(e)}); print(f" ❌ {e}")
avg=sum(scores)/len(scores) if scores else 0
print(f"\n Batch: {len(scores)}/{len(files)} avg={avg:.1f}/100")
try:
with open(out_p/'_batch_v80.json','w',encoding='utf-8') as jf:
json.dump({'results':results,'avg_score':round(avg,1)},jf,ensure_ascii=False,indent=2)
except: pass
def main() -> int:
if not NUMPY_OK or not SCIPY_OK:
print("pip install numpy scipy"); return 1
p=argparse.ArgumentParser(description='Audio Enhancement Engine v8.0 — 1425H')
p.add_argument('-i','--input'); p.add_argument('-o','--output')
p.add_argument('--iterations',type=int,default=3)
p.add_argument('--target',type=float,default=96.0)
p.add_argument('--batch-in'); p.add_argument('--batch-out')
p.add_argument('--serve',action='store_true')
p.add_argument('--port',type=int,default=5000)
p.add_argument('--clear-cache',action='store_true')
args=p.parse_args()
if args.clear_cache:
if os.path.exists(REF_CACHE): os.remove(REF_CACHE); print("✅ Cache v8 حُذف")
return 0
if args.serve:
try: from flask import Flask,request,send_file,jsonify
except: print("pip install flask"); return 1
import threading, uuid as _uuid, re
app=Flask(__name__); app.jobs={}
@app.route('/')
def index():
return (Path(__file__).parent/'templates'/'index.html').read_text()
@app.route('/upload',methods=['POST'])
def upload():
f=request.files.get('file')
if not f: return jsonify(error='لم يُرسَل ملف'),400
data=f.read()
if len(data)>300*1024*1024: return jsonify(error='الحد الأقصى 300MB'),400
jid=str(_uuid.uuid4())[:8]; home=Path(os.environ.get('HOME','/tmp'))
(home/'uploads').mkdir(exist_ok=True); (home/'outputs').mkdir(exist_ok=True)
ext=Path(f.filename or 'a.mp3').suffix or '.mp3'
in_p=home/'uploads'/f'{jid}{ext}'; out_p=home/'outputs'/f'{jid}.mp3'
in_p.write_bytes(data); stem=Path(f.filename or 'audio').stem
app.jobs[jid]={'status':'processing','progress':5,'label':'جارٍ المعالجة...','new_log':[],
'in':str(in_p),'out':str(out_p),'filename':f'{stem}_v8.0.mp3'}
def run(jid=jid,in_p=in_p,out_p=out_p):
j=app.jobs[jid]
try:
PHASES={'[١]':10,'[٢]':18,'[٣]':26,'[٤]':32,'[٥]':38,
'[٦]':50,'[٧]':62,'[٨]':74,'[٩]':86}
proc=subprocess.Popen(
['python',__file__,'-i',str(in_p),'-o',str(out_p),'--iterations','3'],
stdout=subprocess.PIPE,stderr=subprocess.STDOUT,text=True,bufsize=1)
for line in proc.stdout:
line=line.rstrip()
if not line: continue
j.setdefault('new_log',[]).append(line)
for tag,pct in PHASES.items():
if tag in line: j['progress']=pct; j['label']=line.strip()[:60]; break
if '★' in line:
m=re.search(r'([\d.]+)/100',line)
if m: j['score']=m.group(1)
for k,pat in [('crest',r'Crest'),('lra',r'LRA'),('rms',r'RMS')]:
if re.search(pat+r'\s*[=:]',line):
nums=re.findall(r'-?[\d.]+',line)
if len(nums)>=2: j.setdefault(k,nums[-2])
proc.wait()
if proc.returncode==0 and out_p.exists():
j.update({'status':'done','progress':100,'label':'اكتملت'})
else:
j.update({'status':'error','error':'فشلت المعالجة'})
except Exception as e:
j.update({'status':'error','error':str(e)})
finally:
try: in_p.unlink()
except: pass
threading.Thread(target=run,daemon=True).start()
return jsonify(job_id=jid)
@app.route('/status/<jid>')
def status(jid):
j=app.jobs.get(jid)
if not j: return jsonify(error='not found'),404
r={'status':j['status'],'progress':j['progress'],
'label':j.get('label',''),'log':j.pop('new_log',[])}
if j['status']=='done':
for k in ['score','crest','lra','rms','filename']: r[k]=j.get(k)
r['job_id']=jid
if j['status']=='error': r['error']=j.get('error','خطأ')
return jsonify(r)
@app.route('/download/<jid>')
def download(jid):
j=app.jobs.get(jid)
if not j or not Path(j['out']).exists(): return 'Not found',404
return send_file(j['out'],as_attachment=True,
download_name=j.get('filename','v8.0.mp3'))
print(f"\n محسّن التلاوة v8.0 — http://localhost:{args.port}\n")
app.run(host='0.0.0.0',port=args.port,debug=False,threaded=True)
return 0
if args.batch_in and args.batch_out:
process_batch(args.batch_in,args.batch_out); return 0
if not args.input or not args.output:
p.print_help(); return 1
try:
r=enhance_auto(args.input,args.output,args.iterations,args.target)
print(f"\n ★ {r['score']}/100 MDS={r['mds']:.0f}{args.output}")
return 0 if r['score']>=85 else 1
except Exception as e:
print(f"❌ {e}"); return 1
if __name__=='__main__':
sys.exit(main())