Spaces:
Paused
Paused
File size: 30,880 Bytes
3503dff | 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 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 443 444 445 446 447 448 449 450 451 452 453 454 455 456 457 458 459 460 461 462 463 464 465 466 467 468 469 470 471 472 473 474 475 476 477 478 479 480 481 482 483 484 485 486 487 488 489 490 491 492 493 494 495 496 497 498 499 500 501 502 503 504 505 506 507 508 509 510 511 512 513 514 515 516 517 518 519 520 521 522 523 524 525 526 527 528 529 530 531 532 533 534 535 536 537 538 539 540 541 542 543 544 545 546 547 548 549 550 551 552 553 554 555 556 557 558 559 560 561 562 563 564 565 566 567 568 569 570 571 572 573 574 575 576 577 578 579 580 581 582 583 584 585 586 587 588 589 590 591 592 593 594 595 596 597 598 599 600 601 602 603 604 605 606 607 608 609 610 611 612 613 614 615 616 617 618 619 620 621 622 623 624 625 626 627 628 629 630 631 632 633 634 635 636 637 638 639 640 641 642 643 644 645 646 647 648 649 650 651 652 653 654 655 656 657 658 659 660 661 662 663 664 665 666 667 668 669 670 | #!/usr/bin/env python3
# miraat_ref_v2.py β Ψ§ΩΩ
Ψ±Ψ’Ψ© reference-guided enhancement (v2: real neural backends)
#
# ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
# β v2 replaces the imaginary AnyEnhance/Amphion stub with real, β
# β pip-installable neural components. β
# β β
# β Architecture (mirrors AnyEnhance's two-stage intent): β
# β β
# β Stage 1 β Reference speaker verification β
# β SpeechBrain ECAPA-TDNN: encode reference + degraded β speaker cosine β
# β Gate: sim β₯ 0.70 β same speaker β proceed. β
# β Source: speechbrain/spkrec-ecapa-voxceleb (MIT, ~15MB) β
# β β
# β Stage 2 β Reference-guided spectral normalisation β
# β a. Extract 1/3-octave spectral profile from the 1425H reference β
# β b. Run Resemble Enhance (denoise + HF reconstruct) on the degraded β
# β c. Apply a reference-matched EQ correction (5-band parametric) β
# β This steers the enhanced output toward the Sheikh's tonal signature β
# β β the same goal as AnyEnhance's acoustic decoding stage. β
# β β
# β Stage 3 β 4-gate validation (same as v1) β
# β Gate-a: speaker cosine similarity β₯ 0.70 post-enhancement β
# β Gate-b: emphatic consonant ratio β€ 1.5 dB β
# β Gate-c: Madd shortening β€ 5% β
# β Gate-d: LUFS shift β€ 3.0 LU β
# β β
# β Install: β
# β pip install speechbrain resemble-enhance β
# β # (Resemble shared with hakim_gen_v2 β install once) β
# ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
#
# KB References: Β§40 (AnyEnhance), Β§40.2 (two-stage), Β§40.5 (Arabic bias)
from __future__ import annotations
import os
import math
import subprocess
import warnings
from dataclasses import dataclass, field
from pathlib import Path
from typing import Dict, List, Optional, Tuple, Callable
_TMP = os.environ.get('ISTEIDAD_TMP', '/tmp')
SR = 48000
# ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
# Optional heavy imports
# ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
try:
import numpy as np
from scipy.fft import rfft, rfftfreq
_NP_OK = True
except ImportError:
_NP_OK = False
try:
import torch as _torch
import torchaudio as _ta
_TORCH_OK = True
except ImportError:
_TORCH_OK = False
# ββ Stage 1: ECAPA-TDNN speaker embeddings (SpeechBrain) βββββββββββββββββββββ
try:
from speechbrain.pretrained import SpeakerRecognition as _SpeakerRec # type: ignore
_ECAPA_OK = True
except ImportError:
_ECAPA_OK = False
# ββ Stage 2: Resemble Enhance (shared backend with hakim_gen_v2) ββββββββββββββ
try:
from resemble_enhance.enhancer.inference import enhance as _resemble_enhance_fn # type: ignore
_RESEMBLE_OK = True
except ImportError:
_RESEMBLE_OK = False
# ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
# Constants
# ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
# Trigger tiers (only damaged/critical need reference-guided repair)
_TRIGGER_TIERS = {'TIER_DAMAGED', 'TIER_CRITICAL'}
_TRIGGER_STYLES = {'MURATTAL'} # MUJAWWAD ornaments not yet validated
# Speaker similarity gate (ECAPA cosine)
_SIM_THRESHOLD = 0.70 # < 0.70 β different speaker or severe degradation
# Tajweed / loudness gates (same as v1)
_GATE_EMPHATIC_DELTA_DB = 1.5
_GATE_MADD_SHORTENING = 0.05
_GATE_LUFS_DELTA_MAX = 3.0
# Reference EQ correction strength (0.0 = off, 1.0 = full match)
_REF_EQ_BLEND = 0.65 # partial correction β preserve some of the input character
# Model cache
_MODELS_DIR = Path.home() / '.hakim_models'
_ECAPA_DIR = _MODELS_DIR / 'ecapa-voxceleb'
_ECAPA_HF_ID = 'speechbrain/spkrec-ecapa-voxceleb'
# 1/3-octave centre frequencies for reference profiling
_CENTERS_3OCT = [125, 160, 200, 250, 315, 400, 500, 630, 800,
1000, 1250, 1600, 2000, 2500, 3150, 4000, 5000, 6300, 8000]
# ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
# Result dataclass
# ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
@dataclass
class MiraatResult:
output_wav: str = ''
status: str = 'UNAVAILABLE'
reason: str = ''
style_gate: str = 'UNKNOWN'
speaker_sim_before: float = 0.0
speaker_sim_after: float = 0.0
gate_speaker_pass: bool = False
gate_emphatic_pass: bool = False
gate_madd_pass: bool = False
gate_lufs_pass: bool = False
lufs_before: float = 0.0
lufs_after: float = 0.0
ref_eq_applied: bool = False
# ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
# Audio helpers
# ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
def _load_wav_mono(path: str, max_s: float = 30.0) -> Optional['np.ndarray']:
tmp = os.path.join(_TMP, f'miraat_load_{os.getpid()}.f32')
try:
r = subprocess.run(
['ffmpeg', '-y', '-i', path,
'-af', 'aformat=channel_layouts=mono',
'-t', str(max_s), '-ar', str(SR),
'-f', 'f32le', '-loglevel', 'error', tmp],
capture_output=True
)
if r.returncode != 0 or not os.path.exists(tmp):
return None
audio = np.frombuffer(open(tmp, 'rb').read(), dtype=np.float32).copy()
return audio
except Exception:
return None
finally:
try:
os.remove(tmp)
except Exception:
pass
def _measure_lufs_simple(path: str) -> float:
"""Quick LUFS proxy using ffmpeg loudnorm stats."""
try:
r = subprocess.run(
['ffmpeg', '-i', path, '-af',
'loudnorm=print_format=summary', '-f', 'null', '-'],
capture_output=True, text=True
)
for line in r.stderr.split('\n'):
if 'Input Integrated' in line:
return float(line.split(':')[-1].strip().replace(' LUFS', ''))
except Exception:
pass
return -23.0
def _convert_to_16k(wav_path: str) -> Optional[str]:
out = os.path.join(_TMP, f'miraat_16k_{os.getpid()}.wav')
r = subprocess.run(
['ffmpeg', '-y', '-i', wav_path,
'-af', 'aformat=channel_layouts=mono',
'-ar', '16000', '-c:a', 'pcm_s16le', '-loglevel', 'error', out],
capture_output=True
)
return out if (r.returncode == 0 and os.path.exists(out)) else None
def _upsample_to_48k(wav_path: str) -> Optional[str]:
out = os.path.join(_TMP, f'miraat_48k_{os.getpid()}.wav')
r = subprocess.run(
['ffmpeg', '-y', '-i', wav_path,
'-ar', str(SR), '-ac', '1', '-c:a', 'pcm_s24le',
'-loglevel', 'error', out],
capture_output=True
)
return out if (r.returncode == 0 and os.path.exists(out)) else None
# ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
# Stage 1 β Speaker embedding + similarity
# ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
_ecapa_model_cache = None
def _get_ecapa_model(log_fn: Callable) -> Optional[object]:
"""Load ECAPA-TDNN model (lazy, cached)."""
global _ecapa_model_cache
if not _ECAPA_OK:
return None
if _ecapa_model_cache is not None:
return _ecapa_model_cache
try:
log_fn(' [Ψ§ΩΩ
Ψ±Ψ’Ψ©/ECAPA] loading speaker model (auto-downloads ~15MB) ...')
_ecapa_model_cache = _SpeakerRec.from_hparams( # type: ignore
source = _ECAPA_HF_ID,
savedir = str(_ECAPA_DIR),
run_opts = {'device': 'cpu'},
)
return _ecapa_model_cache
except Exception as exc:
log_fn(f' [Ψ§ΩΩ
Ψ±Ψ’Ψ©/ECAPA] load failed: {exc}')
return None
def _speaker_cosine_similarity(wav_a: str, wav_b: str,
model: object,
log_fn: Callable) -> float:
"""
Compute ECAPA-TDNN speaker cosine similarity between two WAV files.
Returns 0.0 on error. Higher = more similar speaker.
"""
try:
wav_a_16k = _convert_to_16k(wav_a)
wav_b_16k = _convert_to_16k(wav_b)
if wav_a_16k is None or wav_b_16k is None:
return 0.0
score, pred = model.verify_files(wav_a_16k, wav_b_16k) # type: ignore
sim = float(score.squeeze())
return sim
except Exception as exc:
log_fn(f' [Ψ§ΩΩ
Ψ±Ψ’Ψ©/ECAPA] similarity error: {exc}')
return 0.0
finally:
for p in (wav_a_16k, wav_b_16k):
try:
if p and os.path.exists(p):
os.remove(p)
except Exception:
pass
# ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
# Stage 2 β Reference spectral profile + Resemble Enhance
# ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
def _third_octave_profile(audio: 'np.ndarray', sr: int = SR
) -> Dict[int, float]:
"""Compute 1/3-octave band energy profile (dBFS) from audio array."""
if not _NP_OK or len(audio) < sr:
return {}
chunk = audio[:sr * 10] if len(audio) > sr * 10 else audio
N = len(chunk)
win = np.hanning(N)
norm = float(np.sqrt(np.sum(win ** 2) / N))
if norm < 1e-12:
return {}
spec = np.abs(rfft(chunk * win)) / (norm * N)
freqs = rfftfreq(N, 1.0 / sr)
out = {}
for fc in _CENTERS_3OCT:
if fc >= sr / 2:
continue
fl = fc / (2 ** (1 / 6))
fh = fc * (2 ** (1 / 6))
mask = (freqs >= fl) & (freqs < fh)
if mask.sum() > 0:
out[fc] = float(20 * np.log10(np.mean(spec[mask]) + 1e-10))
return out
def _build_reference_eq_nodes(ref_profile: Dict[int, float],
src_profile: Dict[int, float],
blend: float = _REF_EQ_BLEND
) -> List[Tuple[float, float, float]]:
"""
Compute EQ nodes to nudge src_profile toward ref_profile.
Returns list of (freq_hz, gain_db, Q) parametric nodes.
Limited to bands 250Hzβ8kHz and Β±6dB per node.
Q=1.41 (broad shelf per band) β prevents narrow resonances.
"""
nodes = []
for fc in _CENTERS_3OCT:
if fc < 250 or fc > 8000:
continue
if fc not in ref_profile or fc not in src_profile:
continue
delta = (ref_profile[fc] - src_profile[fc]) * blend
# Clamp: never more than Β±6dB, never boost below 250Hz identity zone
if 250 <= fc <= 800:
delta = float(max(-3.0, min(3.0, delta))) # voice identity zone
else:
delta = float(max(-6.0, min(6.0, delta)))
if abs(delta) >= 0.5: # ignore sub-0.5dB corrections (noise floor)
nodes.append((float(fc), round(delta, 2), 1.41))
return nodes
def _apply_eq_nodes(wav_path: str,
nodes: List[Tuple[float, float, float]],
log_fn: Callable) -> Optional[str]:
"""Apply parametric EQ nodes via ffmpeg equalizer filter."""
if not nodes:
return wav_path
parts = [f'equalizer=f={f:.0f}:width_type=q:width={q:.2f}:g={g:.2f}'
for f, g, q in nodes if abs(g) >= 0.5]
if not parts:
return wav_path
af_str = ','.join(parts)
out = os.path.join(_TMP, f'miraat_eq_{os.getpid()}.wav')
r = subprocess.run(
['ffmpeg', '-y', '-i', wav_path, '-af', af_str,
'-ar', str(SR), '-ac', '1', '-c:a', 'pcm_s24le',
'-loglevel', 'error', out],
capture_output=True
)
if r.returncode != 0 or not os.path.exists(out):
log_fn(' [Ψ§ΩΩ
Ψ±Ψ’Ψ©/RefEQ] ffmpeg EQ failed β skipping correction')
return wav_path
gain_str = ' '.join(f'{f:.0f}Hz{g:+.1f}dB' for f, g, _ in nodes[:6])
log_fn(f' [Ψ§ΩΩ
Ψ±Ψ’Ψ©/RefEQ] applied {len(nodes)}-band correction: {gain_str}')
return out
def _run_resemble_enhance_miraat(wav_path: str,
log_fn: Callable) -> Tuple[str, bool]:
"""
Resemble Enhance in 'enhancer' mode for Ψ§ΩΩ
Ψ±Ψ’Ψ©.
Ξ»=0.4: lean toward denoising (TIER_DAMAGED/CRITICAL source β don't
add speculative HF content; let the reference EQ handle spectral steering).
"""
if not _RESEMBLE_OK or not _TORCH_OK:
log_fn(' [Ψ§ΩΩ
Ψ±Ψ’Ψ©/Resemble] not installed β '
'run: pip install resemble-enhance')
return wav_path, False
device = 'cuda' if _torch.cuda.is_available() else 'cpu'
log_fn(f' [Ψ§ΩΩ
Ψ±Ψ’Ψ©/Resemble] enhancing (nfe=32 Ξ»=0.4 device={device}) ...')
tmp_enh = os.path.join(_TMP, f'miraat_resemble_{os.getpid()}.wav')
tmp_48k = os.path.join(_TMP, f'miraat_resemble48k_{os.getpid()}.wav')
try:
dwav, sr = _ta.load(wav_path)
if dwav.shape[0] > 1:
dwav = dwav.mean(dim=0, keepdim=True)
with _torch.no_grad():
enhanced, new_sr = _resemble_enhance_fn( # type: ignore
dwav, sr, device=device,
nfe=32, solver='midpoint', lambd=0.4, tau=0.5,
)
if enhanced.dim() == 1:
enhanced = enhanced.unsqueeze(0)
_ta.save(tmp_enh, enhanced.cpu(), new_sr)
if new_sr != SR:
r = subprocess.run(
['ffmpeg', '-y', '-i', tmp_enh,
'-ar', str(SR), '-ac', '1', '-c:a', 'pcm_s24le',
'-loglevel', 'error', tmp_48k],
capture_output=True
)
if r.returncode != 0 or not os.path.exists(tmp_48k):
log_fn(' [Ψ§ΩΩ
Ψ±Ψ’Ψ©/Resemble] 48kHz conversion failed')
return wav_path, False
try:
os.remove(tmp_enh)
except Exception:
pass
return tmp_48k, True
return tmp_enh, True
except Exception as exc:
log_fn(f' [Ψ§ΩΩ
Ψ±Ψ’Ψ©/Resemble] error: {exc}')
return wav_path, False
# ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
# Tajweed + LUFS gates
# ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
def _compute_emphatic_ratio_db(audio: 'np.ndarray', sr: int = SR) -> float:
if not _NP_OK or len(audio) < sr:
return 0.0
chunk = audio[:sr * 10] if len(audio) > sr * 10 else audio
N = len(chunk)
spec = np.abs(rfft(chunk * np.hanning(N))) ** 2
freqs = rfftfreq(N, 1.0 / sr)
def _b(lo: float, hi: float) -> float:
m = (freqs >= lo) & (freqs < hi)
return float(10 * np.log10(np.mean(spec[m]) + 1e-30)) if m.sum() > 0 else -60.0
return _b(600.0, 900.0) - _b(1200.0, 2400.0)
def _detect_sustained_vowels_ms(audio: 'np.ndarray', sr: int = SR) -> List[float]:
frame_len = int(sr * 0.020)
if not _NP_OK or len(audio) < frame_len * 3:
return []
rms = [float(np.sqrt(np.mean(audio[i:i+frame_len]**2) + 1e-30))
for i in range(0, len(audio) - frame_len, frame_len)]
if not rms:
return []
thr = float(np.percentile(rms, 60)) * 0.6
durs: List[float] = []
in_v = False
s = 0
for i, r in enumerate(rms):
if r >= thr and not in_v:
in_v = True
s = i
elif r < thr and in_v:
d = (i - s) * 20.0
if 80 <= d <= 800:
durs.append(d)
in_v = False
return durs
def _validate_all_gates(
wav_before: str,
wav_after: str,
ref_wav: str,
ecapa_model,
lufs_before: float,
log_fn: Callable
) -> Tuple[bool, str, MiraatResult]:
"""
4-gate validation post-enhancement.
Gate-a: speaker similarity β₯ 0.70
Gate-b: emphatic ratio delta β€ 1.5 dB
Gate-c: Madd shortening β€ 5%
Gate-d: LUFS shift β€ 3.0 LU
"""
partial = MiraatResult()
# Gate-a: speaker similarity (ref vs enhanced)
if ecapa_model is not None:
sim_after = _speaker_cosine_similarity(ref_wav, wav_after, ecapa_model, log_fn)
partial.speaker_sim_after = sim_after
partial.gate_speaker_pass = sim_after >= _SIM_THRESHOLD
log_fn(f' [Ψ§ΩΩ
Ψ±Ψ’Ψ©/gate-a] speaker sim={sim_after:.3f} '
f'{"β" if partial.gate_speaker_pass else "β (<0.70)"}')
if not partial.gate_speaker_pass:
return False, f'speaker_sim={sim_after:.3f} < {_SIM_THRESHOLD}', partial
else:
partial.gate_speaker_pass = True # gate bypassed β no ECAPA
log_fn(' [Ψ§ΩΩ
Ψ±Ψ’Ψ©/gate-a] ECAPA not available β speaker gate bypassed')
# Gate-b: emphatic
if _NP_OK:
ab = _load_wav_mono(wav_before, max_s=30.0)
aa = _load_wav_mono(wav_after, max_s=30.0)
if ab is not None and aa is not None:
emp_b = _compute_emphatic_ratio_db(ab)
emp_a = _compute_emphatic_ratio_db(aa)
emp_d = emp_a - emp_b
partial.gate_emphatic_pass = abs(emp_d) <= _GATE_EMPHATIC_DELTA_DB
log_fn(f' [Ψ§ΩΩ
Ψ±Ψ’Ψ©/gate-b] emphatic Ξ={emp_d:+.2f}dB '
f'{"β" if partial.gate_emphatic_pass else "β"}')
if not partial.gate_emphatic_pass:
return False, f'emphatic_delta={emp_d:.2f}dB', partial
# Gate-c: Madd
if ab is not None and aa is not None:
madd_b = _detect_sustained_vowels_ms(ab)
madd_a = _detect_sustained_vowels_ms(aa)
if madd_b and madd_a:
db_m = float(np.mean(madd_b))
da_m = float(np.mean(madd_a))
frac = max(0.0, (db_m - da_m) / max(db_m, 1.0))
partial.gate_madd_pass = frac <= _GATE_MADD_SHORTENING
log_fn(f' [Ψ§ΩΩ
Ψ±Ψ’Ψ©/gate-c] Madd Ξ={frac:.1%} '
f'{"β" if partial.gate_madd_pass else "β"}')
if not partial.gate_madd_pass:
return False, f'madd_shortening={frac:.1%}', partial
else:
partial.gate_madd_pass = True
log_fn(' [Ψ§ΩΩ
Ψ±Ψ’Ψ©/gate-c] Madd gate bypassed (no vowel segments)')
else:
partial.gate_emphatic_pass = True
partial.gate_madd_pass = True
log_fn(' [Ψ§ΩΩ
Ψ±Ψ’Ψ©/gate-bc] audio load failed β Tajweed gates bypassed')
else:
partial.gate_emphatic_pass = True
partial.gate_madd_pass = True
# Gate-d: LUFS
lufs_after = _measure_lufs_simple(wav_after)
partial.lufs_after = lufs_after
lufs_shift = abs(lufs_after - lufs_before)
partial.gate_lufs_pass = lufs_shift <= _GATE_LUFS_DELTA_MAX
log_fn(f' [Ψ§ΩΩ
Ψ±Ψ’Ψ©/gate-d] LUFS {lufs_before:.1f}β{lufs_after:.1f}LU '
f'(Ξ={lufs_shift:.1f}LU) '
f'{"β" if partial.gate_lufs_pass else "β (>{_GATE_LUFS_DELTA_MAX}LU)"}')
if not partial.gate_lufs_pass:
return False, f'lufs_shift={lufs_shift:.1f}LU', partial
return True, 'passed', partial
# ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
# Main orchestrator β API-compatible with miraat_ref_v1.apply_miraat()
# ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
def apply_miraat(
wav_path: str,
state,
ref_files: List[str],
log_fn: Optional[Callable] = None
) -> Tuple[str, MiraatResult]:
"""
Ψ§ΩΩ
Ψ±Ψ’Ψ© v2 β The Mirror (reference-guided enhancement).
Phase B-ref: ECAPA speaker check β Resemble Enhance β reference EQ β 4-gate.
Returns (output_wav_path, MiraatResult).
Falls back to wav_path on any failure or gate rejection.
"""
result = MiraatResult(output_wav=wav_path)
def _log(msg: str):
if log_fn:
log_fn(msg)
source_tier = getattr(state, 'source_tier', 'TIER_CLEAN')
style_class = getattr(state, 'style_class', 'MURATTAL')
_log('\nPhase B-ref β Ψ§ΩΩ
Ψ±Ψ’Ψ© v2 (ECAPA + Resemble + RefEQ)')
_log(f' tier={source_tier} style={style_class}')
_log(f' backends β ecapa={_ECAPA_OK} resemble={_RESEMBLE_OK}')
# ββ Tier / style gate βββββββββββββββββββββββββββββββββββββββββββββββββββββ
if source_tier not in _TRIGGER_TIERS:
result.status = 'SKIPPED'
result.reason = f'tier={source_tier} not in {_TRIGGER_TIERS}'
_log(f' [Ψ§ΩΩ
Ψ±Ψ’Ψ©] skip β {result.reason}')
return wav_path, result
if style_class == 'MUJAWWAD':
result.status = 'SKIPPED'
result.style_gate = 'MUJAWWAD_UNVALIDATED'
result.reason = 'MUJAWWAD ornamental sweeps not validated β bypass'
_log(f' [Ψ§ΩΩ
Ψ±Ψ’Ψ©] {result.reason}')
return wav_path, result
result.style_gate = 'PASSED'
# ββ Reference selection βββββββββββββββββββββββββββββββββββββββββββββββββββ
ref_wav = None
for rf in (ref_files or []):
if rf and os.path.exists(rf):
ref_wav = rf
break
if ref_wav is None:
result.status = 'SKIPPED'
result.reason = 'no reference file available'
_log(' [Ψ§ΩΩ
Ψ±Ψ’Ψ©] no reference file β bypass')
return wav_path, result
# Extract a short clean clip from reference (first 20s, skip 2s silence)
ref_clip = os.path.join(_TMP, f'miraat_refclip_{os.getpid()}.wav')
r = subprocess.run(
['ffmpeg', '-y', '-i', ref_wav, '-ss', '2', '-t', '20',
'-ar', str(SR), '-ac', '1', '-c:a', 'pcm_s24le',
'-loglevel', 'error', ref_clip],
capture_output=True
)
if r.returncode != 0 or not os.path.exists(ref_clip):
result.status = 'SKIPPED'
result.reason = 'reference clip extraction failed'
_log(' [Ψ§ΩΩ
Ψ±Ψ’Ψ©] reference clip extraction failed β bypass')
return wav_path, result
lufs_before = _measure_lufs_simple(wav_path)
result.lufs_before = lufs_before
# ββ Stage 1: Baseline speaker similarity (ref vs degraded) βββββββββββββββ
ecapa = _get_ecapa_model(_log)
if ecapa is not None:
sim_before = _speaker_cosine_similarity(ref_clip, wav_path, ecapa, _log)
result.speaker_sim_before = sim_before
_log(f' [Ψ§ΩΩ
Ψ±Ψ’Ψ©/stage-1] speaker sim (ref vs input) = {sim_before:.3f}')
if sim_before < _SIM_THRESHOLD * 0.6:
# Very low similarity β likely wrong speaker or too damaged
_log(f' [Ψ§ΩΩ
Ψ±Ψ’Ψ©/stage-1] sim={sim_before:.3f} < '
f'{_SIM_THRESHOLD * 0.6:.2f} β input too damaged for ref-guided path')
result.status = 'SKIPPED'
result.reason = f'input speaker sim too low ({sim_before:.3f})'
return wav_path, result
else:
_log(' [Ψ§ΩΩ
Ψ±Ψ’Ψ©/stage-1] ECAPA not installed β '
'run: pip install speechbrain (speaker gate bypassed)')
result.speaker_sim_before = 0.0
# ββ Stage 2a: Resemble Enhance ββββββββββββββββββββββββββββββββββββββββββββ
_log(' [Ψ§ΩΩ
Ψ±Ψ’Ψ©/stage-2a] running Resemble Enhance (denoiser + HF reconstruct) ...')
enh_out, enh_ok = _run_resemble_enhance_miraat(wav_path, _log)
current = enh_out if enh_ok else wav_path
# ββ Stage 2b: Reference spectral profile β EQ correction βββββββββββββββββ
ref_eq_out = current
if _NP_OK:
ref_audio = _load_wav_mono(ref_clip, max_s=20.0)
src_audio = _load_wav_mono(current, max_s=20.0)
if ref_audio is not None and src_audio is not None:
ref_prof = _third_octave_profile(ref_audio)
src_prof = _third_octave_profile(src_audio)
eq_nodes = _build_reference_eq_nodes(ref_prof, src_prof)
if eq_nodes:
_log(f' [Ψ§ΩΩ
Ψ±Ψ’Ψ©/stage-2b] applying {len(eq_nodes)}-band '
f'reference EQ correction (blend={_REF_EQ_BLEND}) ...')
eq_result = _apply_eq_nodes(current, eq_nodes, _log)
if eq_result and eq_result != current:
ref_eq_out = eq_result
result.ref_eq_applied = True
# Clean up intermediate enhanced file if EQ replaced it
if enh_ok and current != wav_path:
try:
os.remove(current)
except Exception:
pass
else:
_log(' [Ψ§ΩΩ
Ψ±Ψ’Ψ©/stage-2b] reference already matched β no EQ needed')
else:
_log(' [Ψ§ΩΩ
Ψ±Ψ’Ψ©/stage-2b] audio load failed β ref EQ skipped')
else:
_log(' [Ψ§ΩΩ
Ψ±Ψ’Ψ©/stage-2b] numpy unavailable β ref EQ skipped')
# ββ Stage 3: 4-gate validation ββββββββββββββββββββββββββββββββββββββββββββ
_log(' [Ψ§ΩΩ
Ψ±Ψ’Ψ©/stage-3] 4-gate validation ...')
gate_pass, gate_reason, gate_partial = _validate_all_gates(
wav_before = wav_path,
wav_after = ref_eq_out,
ref_wav = ref_clip,
ecapa_model = ecapa,
lufs_before = lufs_before,
log_fn = _log,
)
result.gate_speaker_pass = gate_partial.gate_speaker_pass
result.gate_emphatic_pass = gate_partial.gate_emphatic_pass
result.gate_madd_pass = gate_partial.gate_madd_pass
result.gate_lufs_pass = gate_partial.gate_lufs_pass
result.speaker_sim_after = gate_partial.speaker_sim_after
result.lufs_after = gate_partial.lufs_after
# Clean up ref clip
try:
if os.path.exists(ref_clip):
os.remove(ref_clip)
except Exception:
pass
if gate_pass:
result.status = 'OK'
result.reason = 'all 4 gates passed'
result.output_wav = ref_eq_out
_log(f' Ψ§ΩΩ
Ψ±Ψ’Ψ© β speaker_sim={result.speaker_sim_after:.3f} '
f'lufs={result.lufs_before:.1f}β{result.lufs_after:.1f}LU '
f'ref_eq={result.ref_eq_applied}')
return ref_eq_out, result
else:
result.status = 'REVERTED'
result.reason = gate_reason
result.output_wav = wav_path
_log(f' Ψ§ΩΩ
Ψ±Ψ’Ψ© REVERTED β {gate_reason}')
# Clean up intermediate files
for f in (ref_eq_out, enh_out):
try:
if f and f != wav_path and os.path.exists(f):
os.remove(f)
except Exception:
pass
return wav_path, result
|