Spaces:
Sleeping
Sleeping
S214: remove إحياء v4 entirely, isteidad-server takes v11.2; clean requirements.txt (cache-bust + voicefixer2 gone)
Browse files- apbwe_vendor/checkpoints/8kto48k/config.json +0 -29
- apbwe_vendor/checkpoints/8kto48k/g_8kto48k +0 -3
- app.py +3 -15
- engine_ihya_v4.py +0 -1074
- engine_isteidad_server.py +24 -0
- requirements.txt +0 -2
apbwe_vendor/checkpoints/8kto48k/config.json
DELETED
|
@@ -1,29 +0,0 @@
|
|
| 1 |
-
{
|
| 2 |
-
"num_gpus": 0,
|
| 3 |
-
"batch_size": 16,
|
| 4 |
-
"learning_rate": 0.0002,
|
| 5 |
-
"adam_b1": 0.8,
|
| 6 |
-
"adam_b2": 0.99,
|
| 7 |
-
"lr_decay": 0.999,
|
| 8 |
-
"seed": 1234,
|
| 9 |
-
|
| 10 |
-
"ConvNeXt_channels": 512,
|
| 11 |
-
"ConvNeXt_layers": 8,
|
| 12 |
-
|
| 13 |
-
"segment_size": 8000,
|
| 14 |
-
"n_fft": 1024,
|
| 15 |
-
"hop_size": 80,
|
| 16 |
-
"win_size": 320,
|
| 17 |
-
|
| 18 |
-
"hr_sampling_rate": 48000,
|
| 19 |
-
"lr_sampling_rate": 8000,
|
| 20 |
-
"subsampling_rate": 6,
|
| 21 |
-
|
| 22 |
-
"num_workers": 4,
|
| 23 |
-
|
| 24 |
-
"dist_config": {
|
| 25 |
-
"dist_backend": "nccl",
|
| 26 |
-
"dist_url": "tcp://localhost:54321",
|
| 27 |
-
"world_size": 1
|
| 28 |
-
}
|
| 29 |
-
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
apbwe_vendor/checkpoints/8kto48k/g_8kto48k
DELETED
|
@@ -1,3 +0,0 @@
|
|
| 1 |
-
version https://git-lfs.github.com/spec/v1
|
| 2 |
-
oid sha256:8f0387a4f1cdf1ec5dc557a9e8a303326b45b7706b37843f771f24ee94d2e521
|
| 3 |
-
size 119097961
|
|
|
|
|
|
|
|
|
|
|
|
app.py
CHANGED
|
@@ -35,7 +35,7 @@ BASE = Path(__file__).parent
|
|
| 35 |
ENGINE_SCRIPTS = {
|
| 36 |
"v11.0": BASE / "engine_safaa_v5.py", # S191: v5 continuous DF3 blend + S190 WPE guard # S175 # S149
|
| 37 |
"v11.1": BASE / "engine_itiqan_v6_server.py", # S200: server-optimised الإتقان
|
| 38 |
-
"v11.2": BASE / "
|
| 39 |
"v10.0": BASE / "engine_safaa_v3_fixed.py", # S149
|
| 40 |
"v9.0": BASE / "engine_v90.py",
|
| 41 |
"v8.9": BASE / "engine_v89.py",
|
|
@@ -326,29 +326,17 @@ def _run_engine(job_id):
|
|
| 326 |
ref_files = _get_ref_files()
|
| 327 |
# S165: safaa has no --iterations / --ref — skip them
|
| 328 |
_is_safaa = "safaa" in str(script).lower()
|
| 329 |
-
# S201: ihya v3 has no --iterations / --ref either, and forces a
|
| 330 |
-
# WAV codec on write regardless of output extension — give it a
|
| 331 |
-
# WAV temp path like safaa, not a raw .mp3 path.
|
| 332 |
-
_is_ihya = "ihya" in str(script).lower()
|
| 333 |
# S173 Bug2: safaa uses soundfile → cannot write MP3
|
| 334 |
# → write WAV first, then convert to MP3 with ffmpeg
|
| 335 |
_o_path = (
|
| 336 |
job["out_path"].replace(".mp3", "_safaa_tmp.wav")
|
| 337 |
-
if _is_safaa else
|
| 338 |
-
job["out_path"].replace(".mp3", "_ihya_tmp.wav")
|
| 339 |
-
if _is_ihya else job["out_path"]
|
| 340 |
)
|
| 341 |
# S174-SV-C1: safaa uses positional args (input output), not -i/-o flags
|
| 342 |
if _is_safaa:
|
| 343 |
cmd = ["python3", str(script), job["in_path"], _o_path,
|
| 344 |
"--tier", job.get("tier", "TIER_UNKNOWN"),
|
| 345 |
"--mujawwad", str(job.get("mujawwad_conf", 0.0))] # S175
|
| 346 |
-
elif _is_ihya:
|
| 347 |
-
# S204: v4 has NO --aggressive flag (argparse would crash).
|
| 348 |
-
# v4's full power is its default with no --skip-* flags —
|
| 349 |
-
# nothing extra to pass.
|
| 350 |
-
cmd = ["python3", str(script),
|
| 351 |
-
"-i", job["in_path"], "-o", _o_path]
|
| 352 |
else:
|
| 353 |
cmd = ["python3", str(script),
|
| 354 |
"-i", job["in_path"], "-o", _o_path,
|
|
@@ -424,7 +412,7 @@ def _run_engine(job_id):
|
|
| 424 |
_out = Path(_o_path)
|
| 425 |
_clean = (not _timed_out) and proc.returncode == 0 # S185
|
| 426 |
if _clean and _out.exists() and _out.stat().st_size > 0:
|
| 427 |
-
if
|
| 428 |
subprocess.run(
|
| 429 |
["ffmpeg", "-y", "-i", str(_out),
|
| 430 |
"-b:a", "320k", "-ar", "48000", job["out_path"]],
|
|
|
|
| 35 |
ENGINE_SCRIPTS = {
|
| 36 |
"v11.0": BASE / "engine_safaa_v5.py", # S191: v5 continuous DF3 blend + S190 WPE guard # S175 # S149
|
| 37 |
"v11.1": BASE / "engine_itiqan_v6_server.py", # S200: server-optimised الإتقان
|
| 38 |
+
"v11.2": BASE / "engine_isteidad_server.py", # S214: server-hardened isteidad (إحياء v4 removed)
|
| 39 |
"v10.0": BASE / "engine_safaa_v3_fixed.py", # S149
|
| 40 |
"v9.0": BASE / "engine_v90.py",
|
| 41 |
"v8.9": BASE / "engine_v89.py",
|
|
|
|
| 326 |
ref_files = _get_ref_files()
|
| 327 |
# S165: safaa has no --iterations / --ref — skip them
|
| 328 |
_is_safaa = "safaa" in str(script).lower()
|
|
|
|
|
|
|
|
|
|
|
|
|
| 329 |
# S173 Bug2: safaa uses soundfile → cannot write MP3
|
| 330 |
# → write WAV first, then convert to MP3 with ffmpeg
|
| 331 |
_o_path = (
|
| 332 |
job["out_path"].replace(".mp3", "_safaa_tmp.wav")
|
| 333 |
+
if _is_safaa else job["out_path"]
|
|
|
|
|
|
|
| 334 |
)
|
| 335 |
# S174-SV-C1: safaa uses positional args (input output), not -i/-o flags
|
| 336 |
if _is_safaa:
|
| 337 |
cmd = ["python3", str(script), job["in_path"], _o_path,
|
| 338 |
"--tier", job.get("tier", "TIER_UNKNOWN"),
|
| 339 |
"--mujawwad", str(job.get("mujawwad_conf", 0.0))] # S175
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 340 |
else:
|
| 341 |
cmd = ["python3", str(script),
|
| 342 |
"-i", job["in_path"], "-o", _o_path,
|
|
|
|
| 412 |
_out = Path(_o_path)
|
| 413 |
_clean = (not _timed_out) and proc.returncode == 0 # S185
|
| 414 |
if _clean and _out.exists() and _out.stat().st_size > 0:
|
| 415 |
+
if _is_safaa and _o_path != job["out_path"]:
|
| 416 |
subprocess.run(
|
| 417 |
["ffmpeg", "-y", "-i", str(_out),
|
| 418 |
"-b:a", "320k", "-ar", "48000", job["out_path"]],
|
engine_ihya_v4.py
DELETED
|
@@ -1,1074 +0,0 @@
|
|
| 1 |
-
#!/usr/bin/env python3
|
| 2 |
-
"""
|
| 3 |
-
╔══════════════════════════════════════════════════════════════════════════════╗
|
| 4 |
-
║ ║
|
| 5 |
-
║ الإحياء v4 — PARAMETRIC RESYNTHESIS + QURAN PHONEME PROTECTION ║
|
| 6 |
-
║ "وَنَفَخَ فِيهِ مِن رُّوحِهِ" ║
|
| 7 |
-
║ ║
|
| 8 |
-
║ WHY v4 OVER v3: ║
|
| 9 |
-
║ v3 is pure DSP — excellent, but DSP cannot reconstruct a signal whose ║
|
| 10 |
-
║ information has been genuinely destroyed (heavy codec, telephone, hiss). ║
|
| 11 |
-
║ v4 adds AI parametric resynthesis (SIDON / Resemble Enhance) which ║
|
| 12 |
-
║ extracts clean speech FEATURES from the degraded signal then synthesises ║
|
| 13 |
-
║ new audio. Features survive noise where waveforms don't. ║
|
| 14 |
-
║ ║
|
| 15 |
-
║ WHAT VANILLA SIDON CANNOT DO (and why it's dangerous for Quran): ║
|
| 16 |
-
║ • Its vocoder can swallow glottal stops (ء) and qalqalah pops ║
|
| 17 |
-
║ • Emphatic consonants (ض/ط/ظ/ص) lose their spectral signature ║
|
| 18 |
-
║ • The resynthesised voice can drift from the original Sheikh's timbre ║
|
| 19 |
-
║ • Muffled (كتام) input biases the feature predictor toward wrong F0 ║
|
| 20 |
-
║ ║
|
| 21 |
-
║ v4 SOLUTIONS — THREE PROTECTION LAYERS: ║
|
| 22 |
-
║ P1 PhonemeGuard — fingerprints 8 Arabic-critical bands pre-resynthesis ║
|
| 23 |
-
║ and restores any frame where letter energy drops > 8 dB ║
|
| 24 |
-
║ P2 SpeakerAnchor — MFCC cosine similarity gate; if voice drifts ║
|
| 25 |
-
║ post-resynthesis, blends original back to anchor the Sheikh ║
|
| 26 |
-
║ P3 كتام-Aware BWE — detects bandwidth cutoff before resynthesis and ║
|
| 27 |
-
║ informs the model rather than letting it guess; applies natural ║
|
| 28 |
-
║ spectral extension post-resynthesis for any remaining darkening ║
|
| 29 |
-
║ ║
|
| 30 |
-
║ AI MODEL CASCADE (tried in order): ║
|
| 31 |
-
║ 1. SIDON pip install sidon (sarulab-speech/sidon-v0.1) ║
|
| 32 |
-
║ 2. Resemble Enhance pip install resemble-enhance (FREE, open source) ║
|
| 33 |
-
║ 3. VoiceFixer pip install voicefixer (already in Aetherion) ║
|
| 34 |
-
║ 4. DSP fallback الإحياء v3 phases (no AI needed) ║
|
| 35 |
-
║ ║
|
| 36 |
-
║ PROMISES: ║
|
| 37 |
-
║ حروف لا تُؤكل · الشيخ يبقى شيخًا · الكتام يُكشف ║
|
| 38 |
-
║ ║
|
| 39 |
-
║ KB REFS: §4 §35 §36 §52 §79 §85 §102 §133 §143 §152 §160 §173 ║
|
| 40 |
-
║ وما التوفيق إلا بالله ║
|
| 41 |
-
║ ║
|
| 42 |
-
╚══════════════════════════════════════════════════════════════════════════════╝
|
| 43 |
-
"""
|
| 44 |
-
__version__ = 'v4.0'
|
| 45 |
-
|
| 46 |
-
import argparse, json, os, shutil, subprocess, sys, tempfile, time, warnings
|
| 47 |
-
from dataclasses import dataclass, field
|
| 48 |
-
from pathlib import Path
|
| 49 |
-
from typing import Dict, List, Optional, Tuple
|
| 50 |
-
warnings.filterwarnings('ignore')
|
| 51 |
-
|
| 52 |
-
_TMP = tempfile.gettempdir()
|
| 53 |
-
|
| 54 |
-
# ══════════════════════════════════════════════════════════════════════════════
|
| 55 |
-
# OPTIONAL IMPORTS
|
| 56 |
-
# ══════════════════════════════════════════════════════════════════════════════
|
| 57 |
-
|
| 58 |
-
try:
|
| 59 |
-
import numpy as np
|
| 60 |
-
from scipy.fft import rfft, rfftfreq
|
| 61 |
-
from scipy.signal import butter, sosfilt, medfilt
|
| 62 |
-
NUMPY_OK = True
|
| 63 |
-
except Exception:
|
| 64 |
-
NUMPY_OK = False
|
| 65 |
-
|
| 66 |
-
try:
|
| 67 |
-
import soundfile as SF
|
| 68 |
-
SF_OK = True
|
| 69 |
-
except Exception:
|
| 70 |
-
SF_OK = False
|
| 71 |
-
|
| 72 |
-
# AI model availability flags
|
| 73 |
-
SIDON_OK = False
|
| 74 |
-
RESEMBLE_OK = False
|
| 75 |
-
VOICEFIXER_OK = False
|
| 76 |
-
AUDIOSR_OK = False
|
| 77 |
-
TORCH_OK = False
|
| 78 |
-
|
| 79 |
-
try:
|
| 80 |
-
import torch
|
| 81 |
-
TORCH_OK = True
|
| 82 |
-
except Exception:
|
| 83 |
-
# S207: broadened from ImportError — a broken/incompatible install
|
| 84 |
-
# (wrong torch build, CUDA mismatch, etc.) should degrade this flag,
|
| 85 |
-
# not crash the whole module before the cascade even starts.
|
| 86 |
-
pass
|
| 87 |
-
|
| 88 |
-
try:
|
| 89 |
-
from sidon import SidonRestorer # type: ignore
|
| 90 |
-
SIDON_OK = True
|
| 91 |
-
except Exception:
|
| 92 |
-
try:
|
| 93 |
-
from sidon.inference import restore as _sidon_restore_fn # type: ignore
|
| 94 |
-
SIDON_OK = True
|
| 95 |
-
except Exception:
|
| 96 |
-
pass
|
| 97 |
-
|
| 98 |
-
try:
|
| 99 |
-
from resemble_enhance.enhancer.inference import denoise as _re_denoise, enhance as _re_enhance # type: ignore
|
| 100 |
-
import torchaudio # type: ignore
|
| 101 |
-
RESEMBLE_OK = True
|
| 102 |
-
except Exception:
|
| 103 |
-
pass
|
| 104 |
-
|
| 105 |
-
try:
|
| 106 |
-
from voicefixer import VoiceFixer as _VoiceFixer # type: ignore
|
| 107 |
-
VOICEFIXER_OK = True
|
| 108 |
-
except Exception:
|
| 109 |
-
# S207: THIS is the one that was actually crashing the script. voicefixer
|
| 110 |
-
# pulls in an old librosa internally that still uses the long-removed
|
| 111 |
-
# np.complex alias — that raises AttributeError deep in librosa's own
|
| 112 |
-
# import chain, not ImportError, so the old `except ImportError:` here
|
| 113 |
-
# never caught it. The exception propagated all the way up, killed the
|
| 114 |
-
# module at import time, and every v11.2 job fell back to plain ffmpeg
|
| 115 |
-
# loudnorm (score=75) before _try_sidon (now AP-BWE) ever got a chance
|
| 116 |
-
# to run. Root cause (voicefixer 0.1.2 is unmaintained and genuinely
|
| 117 |
-
# incompatible with modern numpy/librosa) is NOT fixed by this — VOICEFIXER_OK
|
| 118 |
-
# will still end up False — but that's fine, since AP-BWE doesn't depend
|
| 119 |
-
# on voicefixer at all and now actually gets to run.
|
| 120 |
-
pass
|
| 121 |
-
|
| 122 |
-
try:
|
| 123 |
-
import audiosr as _audiosr # type: ignore
|
| 124 |
-
AUDIOSR_OK = True
|
| 125 |
-
except Exception:
|
| 126 |
-
pass
|
| 127 |
-
|
| 128 |
-
|
| 129 |
-
# ══════════════════════════════════════════════════════════════════════════════
|
| 130 |
-
# CONSTANTS
|
| 131 |
-
# ══════════════════════════════════════════════════════════════════════════════
|
| 132 |
-
|
| 133 |
-
SR = 48000
|
| 134 |
-
WAV_CODEC = 'pcm_s24le'
|
| 135 |
-
|
| 136 |
-
# Arabic-critical spectral bands for PhonemeGuard (§4, §152)
|
| 137 |
-
# Each band must survive resynthesis or a letter is lost
|
| 138 |
-
PHONEME_BANDS_HZ: List[Tuple[str, float, float]] = [
|
| 139 |
-
('glottal_hamza', 60, 150), # ء — glottal energy
|
| 140 |
-
('nasal_ghunnah', 200, 350), # ن/م — nasal murmur (§152.3)
|
| 141 |
-
('F1_vowel', 350, 800), # vowel height — أ/إ/و differentiation
|
| 142 |
-
('F2_back', 600, 1400), # back vowels — emphatic context
|
| 143 |
-
('F2_front', 1400, 2500), # front vowels — imala, thin letters
|
| 144 |
-
('emphatic_3k', 2500, 4000), # ض/ط/ظ/ص emphatic signature
|
| 145 |
-
('sibilant_safir', 4000, 8000), # ص/س/ز (Safir §152.3)
|
| 146 |
-
('sibilant_taf', 3000, 6000), # ش (Tafasshi §152.3)
|
| 147 |
-
]
|
| 148 |
-
|
| 149 |
-
# PhonemeGuard thresholds
|
| 150 |
-
GUARD_FRAME_MS = 20 # ms per analysis frame
|
| 151 |
-
GUARD_DROP_DB = 8.0 # dB drop triggers letter repair
|
| 152 |
-
GUARD_BLEND_RATIO = 0.55 # original blend when repair triggered (0=all_restored, 1=all_original)
|
| 153 |
-
|
| 154 |
-
# SpeakerAnchor thresholds
|
| 155 |
-
ANCHOR_MFCC_N = 13 # number of MFCC coefficients
|
| 156 |
-
ANCHOR_SIM_MIN = 0.82 # cosine similarity floor — below = voice has drifted
|
| 157 |
-
ANCHOR_BLEND_RATIO = 0.35 # how much original to blend back when anchor fires
|
| 158 |
-
|
| 159 |
-
# كتام (muffled) detection
|
| 160 |
-
KATAM_BW_THRESH_HZ = 5000 # Hz: bandwidth below this = كتام
|
| 161 |
-
KATAM_BWE_GAIN_DB = 4.0 # dB: gentle presence boost for muffled audio
|
| 162 |
-
KATAM_PRESENCE_BAND = (3000, 8000) # Hz: the darkened region to open up
|
| 163 |
-
|
| 164 |
-
# SNR / bandwidth
|
| 165 |
-
SNR_NOISY_THRESH = 15.0 # dB: below this = noisy
|
| 166 |
-
BANDWIDTH_FLOOR_DB = -60.0 # dB: minimum energy for "active" band
|
| 167 |
-
|
| 168 |
-
# Silence detection
|
| 169 |
-
SILENCE_THRESH_DB = -50.0
|
| 170 |
-
|
| 171 |
-
# ══════════════════════════════════════════════════════════════════════════════
|
| 172 |
-
# STATE
|
| 173 |
-
# ══════════════════════════════════════════════════════════════════════════════
|
| 174 |
-
|
| 175 |
-
@dataclass
|
| 176 |
-
class V4State:
|
| 177 |
-
# Analysis
|
| 178 |
-
snr_db: float = 0.0
|
| 179 |
-
active_bw_hz: float = 48000.0
|
| 180 |
-
is_katam: bool = False
|
| 181 |
-
katam_cutoff_hz: float = 0.0
|
| 182 |
-
is_noisy: bool = False
|
| 183 |
-
f0_median_hz: float = 150.0
|
| 184 |
-
# PhonemeGuard
|
| 185 |
-
guard_frames_checked: int = 0
|
| 186 |
-
guard_frames_repaired: int = 0
|
| 187 |
-
guard_bands_lost: List[str] = field(default_factory=list)
|
| 188 |
-
# SpeakerAnchor
|
| 189 |
-
anchor_similarity: float = 1.0
|
| 190 |
-
anchor_fired: bool = False
|
| 191 |
-
# AI model used
|
| 192 |
-
ai_model_used: str = 'none'
|
| 193 |
-
# Temp files
|
| 194 |
-
_tmpfiles: List[str] = field(default_factory=list)
|
| 195 |
-
|
| 196 |
-
def tmpfile(self, suffix='.wav') -> str:
|
| 197 |
-
p = os.path.join(_TMP, f'ihya4_{os.getpid()}_{len(self._tmpfiles)}{suffix}')
|
| 198 |
-
self._tmpfiles.append(p)
|
| 199 |
-
return p
|
| 200 |
-
|
| 201 |
-
def cleanup(self):
|
| 202 |
-
for p in self._tmpfiles:
|
| 203 |
-
try:
|
| 204 |
-
if os.path.exists(p):
|
| 205 |
-
os.remove(p)
|
| 206 |
-
except Exception:
|
| 207 |
-
pass
|
| 208 |
-
|
| 209 |
-
|
| 210 |
-
# ══════════════════════════════════════════════════════════════════════════════
|
| 211 |
-
# AUDIO I/O HELPERS
|
| 212 |
-
# ══════════════════════════════════════════════════════════════════════════════
|
| 213 |
-
|
| 214 |
-
def _decode_to_wav(src: str, dst: str, sr: int = SR) -> bool:
|
| 215 |
-
"""Convert any audio format to WAV via ffmpeg."""
|
| 216 |
-
cmd = [
|
| 217 |
-
'ffmpeg', '-y', '-hide_banner', '-loglevel', 'error',
|
| 218 |
-
'-i', src,
|
| 219 |
-
'-ar', str(sr), '-ac', '1', '-c:a', WAV_CODEC, dst
|
| 220 |
-
]
|
| 221 |
-
try:
|
| 222 |
-
r = subprocess.run(cmd, capture_output=True, timeout=120)
|
| 223 |
-
return r.returncode == 0 and os.path.exists(dst)
|
| 224 |
-
except Exception:
|
| 225 |
-
return False
|
| 226 |
-
|
| 227 |
-
|
| 228 |
-
def _load_wav(path: str) -> Tuple[Optional['np.ndarray'], int]:
|
| 229 |
-
"""Load WAV as float32 numpy array."""
|
| 230 |
-
if not SF_OK:
|
| 231 |
-
return None, SR
|
| 232 |
-
try:
|
| 233 |
-
data, sr = SF.read(path, dtype='float32', always_2d=False)
|
| 234 |
-
if data.ndim > 1:
|
| 235 |
-
data = data.mean(axis=1)
|
| 236 |
-
return data, int(sr)
|
| 237 |
-
except Exception:
|
| 238 |
-
return None, SR
|
| 239 |
-
|
| 240 |
-
|
| 241 |
-
def _save_wav(path: str, data: 'np.ndarray', sr: int = SR) -> bool:
|
| 242 |
-
"""Save float32 array as 24-bit WAV."""
|
| 243 |
-
if not SF_OK:
|
| 244 |
-
return False
|
| 245 |
-
try:
|
| 246 |
-
SF.write(path, data.astype('float32'), sr, subtype='PCM_24')
|
| 247 |
-
return True
|
| 248 |
-
except Exception:
|
| 249 |
-
return False
|
| 250 |
-
|
| 251 |
-
|
| 252 |
-
def _band_energy_db(sig: 'np.ndarray', sr: int, lo: float, hi: float) -> float:
|
| 253 |
-
"""RMS energy in a frequency band, in dB."""
|
| 254 |
-
n = len(sig)
|
| 255 |
-
mag = np.abs(rfft(sig))
|
| 256 |
-
freqs = rfftfreq(n, 1.0 / sr)
|
| 257 |
-
mask = (freqs >= lo) & (freqs <= hi)
|
| 258 |
-
if not mask.any():
|
| 259 |
-
return -120.0
|
| 260 |
-
rms = np.sqrt(np.mean(mag[mask] ** 2) + 1e-12)
|
| 261 |
-
return 20.0 * np.log10(rms + 1e-12)
|
| 262 |
-
|
| 263 |
-
|
| 264 |
-
# ══════════════════════════════════════════════════════════════════════════════
|
| 265 |
-
# PHASE 0 — ANALYSIS
|
| 266 |
-
# ══════════════════════════════════════════════════════════════════════════════
|
| 267 |
-
|
| 268 |
-
def _analyze(sig: 'np.ndarray', sr: int, st: V4State) -> None:
|
| 269 |
-
"""Lightweight pre-analysis: SNR, bandwidth, كتام detection, F0."""
|
| 270 |
-
n = len(sig)
|
| 271 |
-
if n < 512:
|
| 272 |
-
return
|
| 273 |
-
|
| 274 |
-
# Broadband SNR (speech vs silence frames)
|
| 275 |
-
frame_len = int(0.02 * sr)
|
| 276 |
-
rms_frames = []
|
| 277 |
-
for i in range(0, n - frame_len, frame_len):
|
| 278 |
-
rms_frames.append(float(np.sqrt(np.mean(sig[i:i + frame_len] ** 2) + 1e-12)))
|
| 279 |
-
if not rms_frames:
|
| 280 |
-
return
|
| 281 |
-
rms_arr = np.array(rms_frames)
|
| 282 |
-
db_arr = 20.0 * np.log10(rms_arr + 1e-12)
|
| 283 |
-
speech_mask = db_arr > (np.max(db_arr) - 40.0)
|
| 284 |
-
noise_mask = db_arr < (np.percentile(db_arr, 20))
|
| 285 |
-
s_rms = float(np.mean(rms_arr[speech_mask])) if speech_mask.any() else 1e-6
|
| 286 |
-
n_rms = float(np.mean(rms_arr[noise_mask])) if noise_mask.any() else 1e-9
|
| 287 |
-
st.snr_db = float(20.0 * np.log10(s_rms / (n_rms + 1e-12)))
|
| 288 |
-
st.is_noisy = st.snr_db < SNR_NOISY_THRESH
|
| 289 |
-
|
| 290 |
-
# Active bandwidth detection (§173 TIER_TELEPHONE logic)
|
| 291 |
-
mag = np.abs(rfft(sig))
|
| 292 |
-
freqs = rfftfreq(n, 1.0 / sr)
|
| 293 |
-
ref_db = _band_energy_db(sig, sr, 1000, 3000) # speech reference band
|
| 294 |
-
bw = 0.0
|
| 295 |
-
for fq in np.arange(500, sr // 2, 200):
|
| 296 |
-
e_db = _band_energy_db(sig, sr, fq, fq + 200)
|
| 297 |
-
if e_db > (ref_db - 40.0):
|
| 298 |
-
bw = float(fq + 200)
|
| 299 |
-
st.active_bw_hz = bw
|
| 300 |
-
st.is_katam = bw < KATAM_BW_THRESH_HZ and bw > 0
|
| 301 |
-
st.katam_cutoff_hz = bw if st.is_katam else 0.0
|
| 302 |
-
|
| 303 |
-
# Rough F0 via autocorrelation (just median pitch)
|
| 304 |
-
frame = sig[:min(n, 2048)]
|
| 305 |
-
ac = np.correlate(frame, frame, mode='full')[len(frame) - 1:]
|
| 306 |
-
min_lag = max(1, int(sr / 400)) # 400 Hz max pitch
|
| 307 |
-
max_lag = int(sr / 60) # 60 Hz min pitch
|
| 308 |
-
if max_lag < len(ac) and max_lag > min_lag:
|
| 309 |
-
peak = int(np.argmax(ac[min_lag:max_lag]) + min_lag)
|
| 310 |
-
if peak > 0:
|
| 311 |
-
st.f0_median_hz = float(sr / peak)
|
| 312 |
-
|
| 313 |
-
|
| 314 |
-
# ══════════════════════════════════════════════════════════════════════════════
|
| 315 |
-
# LAYER 1 — PHONEME GUARD
|
| 316 |
-
# ══════════════════════════════════════════════════════════════════════════════
|
| 317 |
-
|
| 318 |
-
class PhonemeGuard:
|
| 319 |
-
"""
|
| 320 |
-
Fingerprints Arabic-critical frequency bands frame-by-frame before
|
| 321 |
-
AI resynthesis and repairs any frame where letter energy is lost.
|
| 322 |
-
|
| 323 |
-
حروف لا تُؤكل — letters shall not be eaten.
|
| 324 |
-
"""
|
| 325 |
-
|
| 326 |
-
def __init__(self, sig: 'np.ndarray', sr: int):
|
| 327 |
-
self.sr = sr
|
| 328 |
-
self.orig_sig = sig.copy()
|
| 329 |
-
self.frame_n = int(GUARD_FRAME_MS * 1e-3 * sr)
|
| 330 |
-
self._fingerprint: Optional['np.ndarray'] = None
|
| 331 |
-
self._compute_fingerprint(sig)
|
| 332 |
-
|
| 333 |
-
def _frame_band_db(self, frame: 'np.ndarray', lo: float, hi: float) -> float:
|
| 334 |
-
return _band_energy_db(frame, self.sr, lo, hi)
|
| 335 |
-
|
| 336 |
-
def _compute_fingerprint(self, sig: 'np.ndarray') -> 'np.ndarray':
|
| 337 |
-
"""Returns [n_frames × n_bands] matrix of band energies in dB."""
|
| 338 |
-
n = len(sig)
|
| 339 |
-
frames = []
|
| 340 |
-
for i in range(0, n - self.frame_n, self.frame_n):
|
| 341 |
-
frame = sig[i:i + self.frame_n]
|
| 342 |
-
row = []
|
| 343 |
-
for (_, lo, hi) in PHONEME_BANDS_HZ:
|
| 344 |
-
row.append(self._frame_band_db(frame, lo, hi))
|
| 345 |
-
frames.append(row)
|
| 346 |
-
self._fingerprint = np.array(frames, dtype='float32') if frames else np.zeros((1, len(PHONEME_BANDS_HZ)), dtype='float32')
|
| 347 |
-
return self._fingerprint
|
| 348 |
-
|
| 349 |
-
def repair(self, restored: 'np.ndarray', st: V4State) -> 'np.ndarray':
|
| 350 |
-
"""
|
| 351 |
-
Compare restored signal against original fingerprint.
|
| 352 |
-
For any frame where a letter band has dropped > GUARD_DROP_DB:
|
| 353 |
-
- blend original back into that frame
|
| 354 |
-
Returns repaired signal.
|
| 355 |
-
"""
|
| 356 |
-
if self._fingerprint is None:
|
| 357 |
-
return restored
|
| 358 |
-
|
| 359 |
-
restored_fp = self._compute_fingerprint(restored)
|
| 360 |
-
n_frames = min(len(self._fingerprint), len(restored_fp))
|
| 361 |
-
out = restored.copy()
|
| 362 |
-
n = len(out)
|
| 363 |
-
orig = self.orig_sig
|
| 364 |
-
|
| 365 |
-
bands_lost: List[str] = []
|
| 366 |
-
frames_repaired = 0
|
| 367 |
-
|
| 368 |
-
for fi in range(n_frames):
|
| 369 |
-
pre = self._fingerprint[fi] # band energies before AI
|
| 370 |
-
post = restored_fp[fi] # band energies after AI
|
| 371 |
-
drop = pre - post # positive = loss
|
| 372 |
-
|
| 373 |
-
# Check if any critical band lost energy above threshold
|
| 374 |
-
repair_needed = False
|
| 375 |
-
for bi, (band_name, _, _) in enumerate(PHONEME_BANDS_HZ):
|
| 376 |
-
# Only flag if pre-signal had meaningful energy there
|
| 377 |
-
if pre[bi] > -80.0 and drop[bi] > GUARD_DROP_DB:
|
| 378 |
-
repair_needed = True
|
| 379 |
-
if band_name not in bands_lost:
|
| 380 |
-
bands_lost.append(band_name)
|
| 381 |
-
|
| 382 |
-
if repair_needed:
|
| 383 |
-
i_start = fi * self.frame_n
|
| 384 |
-
i_end = min(i_start + self.frame_n, n, len(orig))
|
| 385 |
-
if i_end > i_start:
|
| 386 |
-
r_frame = out[i_start:i_end]
|
| 387 |
-
o_frame = orig[i_start:i_end]
|
| 388 |
-
if len(r_frame) == len(o_frame):
|
| 389 |
-
# Blend: keep most of restored but restore letter content
|
| 390 |
-
out[i_start:i_end] = (
|
| 391 |
-
(1.0 - GUARD_BLEND_RATIO) * r_frame +
|
| 392 |
-
GUARD_BLEND_RATIO * o_frame
|
| 393 |
-
)
|
| 394 |
-
frames_repaired += 1
|
| 395 |
-
|
| 396 |
-
st.guard_frames_checked = n_frames
|
| 397 |
-
st.guard_frames_repaired = frames_repaired
|
| 398 |
-
st.guard_bands_lost = bands_lost
|
| 399 |
-
return out
|
| 400 |
-
|
| 401 |
-
|
| 402 |
-
# ══════════════════════════════════════════════════════════════════════════════
|
| 403 |
-
# LAYER 2 — SPEAKER ANCHOR
|
| 404 |
-
# ══════════════════════════════════════════════════════════════════════════════
|
| 405 |
-
|
| 406 |
-
class SpeakerAnchor:
|
| 407 |
-
"""
|
| 408 |
-
Extracts a lightweight MFCC-based speaker fingerprint before AI resynthesis.
|
| 409 |
-
After resynthesis, checks cosine similarity. If the voice has drifted
|
| 410 |
-
(similarity below ANCHOR_SIM_MIN), blends the original back to anchor
|
| 411 |
-
the Sheikh's identity.
|
| 412 |
-
|
| 413 |
-
الشيخ يبقى شيخًا — the Sheikh remains himself.
|
| 414 |
-
"""
|
| 415 |
-
|
| 416 |
-
def __init__(self, sig: 'np.ndarray', sr: int):
|
| 417 |
-
self.sr = sr
|
| 418 |
-
self.orig_sig = sig.copy()
|
| 419 |
-
self._orig_embed = self._embed(sig)
|
| 420 |
-
|
| 421 |
-
def _embed(self, sig: 'np.ndarray') -> 'np.ndarray':
|
| 422 |
-
"""Compute global mean MFCC vector (no external deps, pure numpy)."""
|
| 423 |
-
# Pre-emphasis
|
| 424 |
-
preemph = np.append(sig[0], sig[1:] - 0.97 * sig[:-1])
|
| 425 |
-
# Frame
|
| 426 |
-
frame_n = int(0.025 * self.sr)
|
| 427 |
-
hop_n = int(0.010 * self.sr)
|
| 428 |
-
frames = []
|
| 429 |
-
for i in range(0, len(preemph) - frame_n, hop_n):
|
| 430 |
-
f = preemph[i:i + frame_n] * np.hamming(frame_n)
|
| 431 |
-
frames.append(f)
|
| 432 |
-
if not frames:
|
| 433 |
-
return np.zeros(ANCHOR_MFCC_N, dtype='float32')
|
| 434 |
-
|
| 435 |
-
# Log filterbank energies (mel scale, simplified)
|
| 436 |
-
n_fft = frame_n
|
| 437 |
-
n_mel = 26
|
| 438 |
-
mag_frames = np.abs(rfft(np.array(frames), n=n_fft))
|
| 439 |
-
freqs = rfftfreq(n_fft, 1.0 / self.sr)
|
| 440 |
-
mel_lo, mel_hi = 300.0, min(8000.0, self.sr / 2.0)
|
| 441 |
-
mel_pts = np.linspace(
|
| 442 |
-
2595.0 * np.log10(1 + mel_lo / 700.0),
|
| 443 |
-
2595.0 * np.log10(1 + mel_hi / 700.0),
|
| 444 |
-
n_mel + 2
|
| 445 |
-
)
|
| 446 |
-
hz_pts = 700.0 * (10 ** (mel_pts / 2595.0) - 1.0)
|
| 447 |
-
fbank = np.zeros((n_mel, len(freqs)), dtype='float32')
|
| 448 |
-
for m in range(1, n_mel + 1):
|
| 449 |
-
f_lo, f_c, f_hi = hz_pts[m - 1], hz_pts[m], hz_pts[m + 1]
|
| 450 |
-
for k, fq in enumerate(freqs):
|
| 451 |
-
if f_lo <= fq <= f_c:
|
| 452 |
-
fbank[m - 1, k] = (fq - f_lo) / (f_c - f_lo + 1e-8)
|
| 453 |
-
elif f_c < fq <= f_hi:
|
| 454 |
-
fbank[m - 1, k] = (f_hi - fq) / (f_hi - f_c + 1e-8)
|
| 455 |
-
|
| 456 |
-
log_fbank = np.log(mag_frames @ fbank.T + 1e-6) # [n_frames, n_mel]
|
| 457 |
-
|
| 458 |
-
# DCT (type-II, simplified)
|
| 459 |
-
n = n_mel
|
| 460 |
-
k = np.arange(ANCHOR_MFCC_N)[:, None]
|
| 461 |
-
n_arr = np.arange(n)[None, :]
|
| 462 |
-
dct_matrix = np.cos(np.pi / n * (n_arr + 0.5) * k)
|
| 463 |
-
mfccs = log_fbank @ dct_matrix.T # [n_frames, ANCHOR_MFCC_N]
|
| 464 |
-
|
| 465 |
-
# Global mean vector
|
| 466 |
-
embed = mfccs.mean(axis=0).astype('float32')
|
| 467 |
-
norm = np.linalg.norm(embed) + 1e-8
|
| 468 |
-
return embed / norm
|
| 469 |
-
|
| 470 |
-
def anchor(self, restored: 'np.ndarray', st: V4State) -> 'np.ndarray':
|
| 471 |
-
"""
|
| 472 |
-
Check similarity. If voice has drifted, blend original back.
|
| 473 |
-
Returns anchored signal.
|
| 474 |
-
"""
|
| 475 |
-
restored_embed = self._embed(restored)
|
| 476 |
-
sim = float(np.dot(self._orig_embed, restored_embed))
|
| 477 |
-
st.anchor_similarity = sim
|
| 478 |
-
|
| 479 |
-
if sim < ANCHOR_SIM_MIN:
|
| 480 |
-
st.anchor_fired = True
|
| 481 |
-
# Blend: drift severity determines how much original to mix in
|
| 482 |
-
drift_ratio = min(1.0, (ANCHOR_SIM_MIN - sim) / ANCHOR_SIM_MIN)
|
| 483 |
-
blend = ANCHOR_BLEND_RATIO * (1.0 + drift_ratio)
|
| 484 |
-
blend = min(0.65, blend) # never completely override restored
|
| 485 |
-
n = min(len(restored), len(self.orig_sig))
|
| 486 |
-
out = restored.copy()
|
| 487 |
-
out[:n] = (1.0 - blend) * restored[:n] + blend * self.orig_sig[:n]
|
| 488 |
-
return out
|
| 489 |
-
|
| 490 |
-
return restored
|
| 491 |
-
|
| 492 |
-
|
| 493 |
-
# ══════════════════════════════════════════════════════════════════════════════
|
| 494 |
-
# LAYER 3 — كتام BANDWIDTH EXTENSION
|
| 495 |
-
# ══════════════════════════════════════════════════════════════════════════════
|
| 496 |
-
|
| 497 |
-
def _katam_bwe(sig: 'np.ndarray', sr: int, st: V4State) -> 'np.ndarray':
|
| 498 |
-
"""
|
| 499 |
-
Opens up muffled (كتام) audio by gently lifting the presence band
|
| 500 |
-
post-resynthesis. Uses a shelving-style boost above the cutoff frequency.
|
| 501 |
-
|
| 502 |
-
الكتام يُكشف — muffled audio is opened.
|
| 503 |
-
"""
|
| 504 |
-
if not st.is_katam:
|
| 505 |
-
return sig
|
| 506 |
-
|
| 507 |
-
lo, hi = KATAM_PRESENCE_BAND
|
| 508 |
-
# High-shelf: boost everything above the detected cutoff
|
| 509 |
-
cutoff = max(st.katam_cutoff_hz, lo)
|
| 510 |
-
cutoff = min(cutoff, 5000.0)
|
| 511 |
-
|
| 512 |
-
n = len(sig)
|
| 513 |
-
spec = rfft(sig)
|
| 514 |
-
freqs = rfftfreq(n, 1.0 / sr)
|
| 515 |
-
|
| 516 |
-
gain_lin = 10.0 ** (KATAM_BWE_GAIN_DB / 20.0)
|
| 517 |
-
|
| 518 |
-
# Gentle linear ramp from 1.0 at cutoff to gain_lin at hi
|
| 519 |
-
mask = np.zeros(len(freqs), dtype='float32')
|
| 520 |
-
for ki, fq in enumerate(freqs):
|
| 521 |
-
if fq <= cutoff:
|
| 522 |
-
mask[ki] = 1.0
|
| 523 |
-
elif fq <= hi:
|
| 524 |
-
t = (fq - cutoff) / (hi - cutoff + 1e-8)
|
| 525 |
-
mask[ki] = 1.0 + (gain_lin - 1.0) * t
|
| 526 |
-
else:
|
| 527 |
-
mask[ki] = gain_lin
|
| 528 |
-
|
| 529 |
-
spec_boosted = spec * mask
|
| 530 |
-
out = np.fft.irfft(spec_boosted, n=n).astype('float32')
|
| 531 |
-
|
| 532 |
-
# Safety: clip and maintain overall loudness
|
| 533 |
-
out = np.clip(out, -1.0, 1.0)
|
| 534 |
-
# Match RMS
|
| 535 |
-
rms_in = float(np.sqrt(np.mean(sig ** 2)) + 1e-12)
|
| 536 |
-
rms_out = float(np.sqrt(np.mean(out ** 2)) + 1e-12)
|
| 537 |
-
if rms_out > 0:
|
| 538 |
-
out *= (rms_in / rms_out) * 1.05 # slight loudness lift for muffled
|
| 539 |
-
return out.astype('float32')
|
| 540 |
-
|
| 541 |
-
|
| 542 |
-
# ══════════════════════════════════════════════════════════���═══════════════════
|
| 543 |
-
# AI MODEL CASCADE
|
| 544 |
-
# ══════════════════════════════════════════════════════════════════════════════
|
| 545 |
-
|
| 546 |
-
def _try_sidon(wav_in: str, wav_out: str) -> bool:
|
| 547 |
-
"""
|
| 548 |
-
S206: SIDON itself is not a real installable package (sarulab-speech's
|
| 549 |
-
public repo is training-only research code, no PyPI listing, GPU-only,
|
| 550 |
-
and doesn't expose SidonRestorer/sidon.inference.restore at all — those
|
| 551 |
-
names never matched anything real). This slot now runs AP-BWE
|
| 552 |
-
(yxlu-0102/AP-BWE, MIT) instead: an amplitude+phase BWE model, CPU-fast,
|
| 553 |
-
with real downloadable pretrained checkpoints. See apbwe_wrapper.py.
|
| 554 |
-
"""
|
| 555 |
-
try:
|
| 556 |
-
import apbwe_wrapper
|
| 557 |
-
return apbwe_wrapper.restore(wav_in, wav_out)
|
| 558 |
-
except Exception as e:
|
| 559 |
-
print(f"[_try_sidon/apbwe] {e}")
|
| 560 |
-
return False
|
| 561 |
-
|
| 562 |
-
|
| 563 |
-
def _try_resemble_enhance(wav_in: str, wav_out: str) -> bool:
|
| 564 |
-
"""
|
| 565 |
-
Attempt Resemble Enhance (denoiser + enhancer).
|
| 566 |
-
pip install resemble-enhance
|
| 567 |
-
Free, open-source, excellent on degraded speech.
|
| 568 |
-
"""
|
| 569 |
-
if not RESEMBLE_OK or not TORCH_OK:
|
| 570 |
-
return False
|
| 571 |
-
try:
|
| 572 |
-
import torchaudio # type: ignore
|
| 573 |
-
dwav, sr_orig = torchaudio.load(wav_in)
|
| 574 |
-
device = 'cuda' if torch.cuda.is_available() else 'cpu' # type: ignore
|
| 575 |
-
|
| 576 |
-
# Stage 1: Denoise
|
| 577 |
-
denoised, sr_dn = _re_denoise(dwav, sr_orig, device) # type: ignore
|
| 578 |
-
# Stage 2: Enhance (bandwidth extension + naturalness)
|
| 579 |
-
enhanced, sr_en = _re_enhance( # type: ignore
|
| 580 |
-
denoised, sr_dn, device,
|
| 581 |
-
nfe=64, # number of function evaluations (quality)
|
| 582 |
-
solver='midpoint',
|
| 583 |
-
lambd=0.5, # balance between denoising and hallucination
|
| 584 |
-
tau=0.5,
|
| 585 |
-
)
|
| 586 |
-
torchaudio.save(wav_out, enhanced.cpu(), sr_en)
|
| 587 |
-
return os.path.exists(wav_out)
|
| 588 |
-
except Exception:
|
| 589 |
-
return False
|
| 590 |
-
|
| 591 |
-
|
| 592 |
-
def _try_voicefixer(wav_in: str, wav_out: str, aggressive: bool = False) -> bool:
|
| 593 |
-
"""
|
| 594 |
-
Attempt VoiceFixer restoration.
|
| 595 |
-
pip install voicefixer2 (S210: replaces unmaintained voicefixer==0.1.2,
|
| 596 |
-
which pulls in an old librosa incompatible with modern numpy — see S207.
|
| 597 |
-
voicefixer2 fixes that, and installs into the SAME `voicefixer` import
|
| 598 |
-
namespace, so no import-line changes needed here.)
|
| 599 |
-
|
| 600 |
-
Mode 0 (default): light restoration, safe on most input.
|
| 601 |
-
Mode 2 (aggressive=True): per voicefixer2's own docs, "might work
|
| 602 |
-
sometimes on seriously degraded real speech" — explicitly less
|
| 603 |
-
reliable, only used when the engine's own كتام/is_katam analysis has
|
| 604 |
-
already flagged this specific file as genuinely degraded, not applied
|
| 605 |
-
blanket to everything.
|
| 606 |
-
"""
|
| 607 |
-
if not VOICEFIXER_OK:
|
| 608 |
-
return False
|
| 609 |
-
mode = 2 if aggressive else 0
|
| 610 |
-
try:
|
| 611 |
-
vf = _VoiceFixer()
|
| 612 |
-
vf.restore(
|
| 613 |
-
input=wav_in,
|
| 614 |
-
output=wav_out,
|
| 615 |
-
cuda=False,
|
| 616 |
-
mode=mode,
|
| 617 |
-
)
|
| 618 |
-
return os.path.exists(wav_out)
|
| 619 |
-
except Exception:
|
| 620 |
-
try:
|
| 621 |
-
# Some versions use different API
|
| 622 |
-
vf = _VoiceFixer()
|
| 623 |
-
vf.restore(input_path=wav_in, output_path=wav_out, cuda=False, mode=mode)
|
| 624 |
-
return os.path.exists(wav_out)
|
| 625 |
-
except Exception:
|
| 626 |
-
return False
|
| 627 |
-
|
| 628 |
-
|
| 629 |
-
def _dsp_fallback(wav_in: str, wav_out: str, st: V4State) -> bool:
|
| 630 |
-
"""
|
| 631 |
-
DSP-only fallback using light band enhancement when no AI model is
|
| 632 |
-
available. Less powerful than AI resynthesis but phoneme-safe.
|
| 633 |
-
"""
|
| 634 |
-
if not NUMPY_OK or not SF_OK:
|
| 635 |
-
return False
|
| 636 |
-
try:
|
| 637 |
-
sig, sr = _load_wav(wav_in)
|
| 638 |
-
if sig is None:
|
| 639 |
-
return False
|
| 640 |
-
|
| 641 |
-
# Gentle harmonic excitation on voiced bands
|
| 642 |
-
# 1. Light noise gate
|
| 643 |
-
frame_n = int(0.02 * sr)
|
| 644 |
-
out = sig.copy()
|
| 645 |
-
for i in range(0, len(sig) - frame_n, frame_n):
|
| 646 |
-
rms = float(np.sqrt(np.mean(sig[i:i + frame_n] ** 2)))
|
| 647 |
-
noise_floor = float(np.percentile(
|
| 648 |
-
[np.sqrt(np.mean(sig[j:j + frame_n] ** 2))
|
| 649 |
-
for j in range(0, min(len(sig), frame_n * 50), frame_n)
|
| 650 |
-
if j + frame_n <= len(sig)],
|
| 651 |
-
15
|
| 652 |
-
))
|
| 653 |
-
if rms < noise_floor * 2.0:
|
| 654 |
-
out[i:i + frame_n] *= (rms / (noise_floor * 2.0 + 1e-8))
|
| 655 |
-
|
| 656 |
-
# 2. Gentle presence lift (2-5kHz)
|
| 657 |
-
if _band_energy_db(sig, sr, 2000, 5000) < _band_energy_db(sig, sr, 500, 2000) - 12:
|
| 658 |
-
n = len(out)
|
| 659 |
-
spec = rfft(out)
|
| 660 |
-
freqs = rfftfreq(n, 1.0 / sr)
|
| 661 |
-
boost = np.ones(len(freqs), dtype='float32')
|
| 662 |
-
for ki, fq in enumerate(freqs):
|
| 663 |
-
if 2000 <= fq <= 5000:
|
| 664 |
-
boost[ki] = 10 ** (3.0 / 20.0) # +3dB
|
| 665 |
-
spec_b = spec * boost
|
| 666 |
-
out = np.fft.irfft(spec_b, n=n).astype('float32')
|
| 667 |
-
|
| 668 |
-
return _save_wav(wav_out, out, sr)
|
| 669 |
-
except Exception:
|
| 670 |
-
return False
|
| 671 |
-
|
| 672 |
-
|
| 673 |
-
def _run_ai_cascade(wav_in: str, wav_out: str, st: V4State) -> bool:
|
| 674 |
-
"""
|
| 675 |
-
Try AI models in quality order. Returns True if any succeeded.
|
| 676 |
-
Updates st.ai_model_used.
|
| 677 |
-
|
| 678 |
-
S210: for genuinely degraded audio (st.is_katam), AP-BWE's inherent
|
| 679 |
-
conservatism was confirmed (real test, real recording) to be
|
| 680 |
-
insufficient — the cascade's normal "first success wins" ordering
|
| 681 |
-
would never even reach VoiceFixer Mode 2, since AP-BWE itself
|
| 682 |
-
succeeds (just weakly). So for is_katam audio specifically, try the
|
| 683 |
-
more aggressive VoiceFixer Mode 2 FIRST, with AP-BWE as the fallback
|
| 684 |
-
if Mode 2 fails outright. This is a deliberate trade of more
|
| 685 |
-
reconstruction risk for more restoration power on the worst material,
|
| 686 |
-
not a blanket change — non-katam audio still goes through the
|
| 687 |
-
conservative path first, unchanged.
|
| 688 |
-
"""
|
| 689 |
-
tmp_out = st.tmpfile('.wav')
|
| 690 |
-
|
| 691 |
-
if st.is_katam:
|
| 692 |
-
if _try_voicefixer(wav_in, tmp_out, aggressive=True):
|
| 693 |
-
_decode_to_wav(tmp_out, wav_out, SR)
|
| 694 |
-
st.ai_model_used = 'voicefixer_mode2'
|
| 695 |
-
return True
|
| 696 |
-
# Mode 2 failing outright (not just "weak", actually erroring)
|
| 697 |
-
# falls through to the normal conservative order below.
|
| 698 |
-
|
| 699 |
-
if _try_sidon(wav_in, tmp_out):
|
| 700 |
-
shutil.copy2(tmp_out, wav_out)
|
| 701 |
-
st.ai_model_used = 'sidon'
|
| 702 |
-
return True
|
| 703 |
-
|
| 704 |
-
if _try_resemble_enhance(wav_in, tmp_out):
|
| 705 |
-
# Resemble outputs at its own SR; normalize to our SR
|
| 706 |
-
_decode_to_wav(tmp_out, wav_out, SR)
|
| 707 |
-
st.ai_model_used = 'resemble_enhance'
|
| 708 |
-
return True
|
| 709 |
-
|
| 710 |
-
if _try_voicefixer(wav_in, tmp_out, aggressive=False):
|
| 711 |
-
_decode_to_wav(tmp_out, wav_out, SR)
|
| 712 |
-
st.ai_model_used = 'voicefixer'
|
| 713 |
-
return True
|
| 714 |
-
|
| 715 |
-
if _dsp_fallback(wav_in, wav_out, st):
|
| 716 |
-
st.ai_model_used = 'dsp_fallback'
|
| 717 |
-
return True
|
| 718 |
-
|
| 719 |
-
return False
|
| 720 |
-
|
| 721 |
-
|
| 722 |
-
# ══════════════════════════════════════════════════════════════════════════════
|
| 723 |
-
# PRE-PROCESSING (light touch before AI)
|
| 724 |
-
# ══════════════════════════════════════════════════════════════════════════════
|
| 725 |
-
|
| 726 |
-
def _preprocess(sig: 'np.ndarray', sr: int, st: V4State) -> 'np.ndarray':
|
| 727 |
-
"""
|
| 728 |
-
Light pre-processing before AI resynthesis:
|
| 729 |
-
- Wind rumble HPF (if detected)
|
| 730 |
-
- Normalise to -3dBFS so AI models get well-levelled input
|
| 731 |
-
"""
|
| 732 |
-
out = sig.copy()
|
| 733 |
-
|
| 734 |
-
# Wind rumble high-pass (20-100 Hz)
|
| 735 |
-
wind_e = _band_energy_db(sig, sr, 20, 150)
|
| 736 |
-
ref_e = _band_energy_db(sig, sr, 200, 500)
|
| 737 |
-
if wind_e > ref_e + 6.0:
|
| 738 |
-
sos = butter(3, 120.0 / (sr / 2), btype='high', output='sos')
|
| 739 |
-
out = sosfilt(sos, out).astype('float32')
|
| 740 |
-
|
| 741 |
-
# Normalise to -3 dBFS peak
|
| 742 |
-
peak = float(np.max(np.abs(out)) + 1e-12)
|
| 743 |
-
target_peak = 10.0 ** (-3.0 / 20.0)
|
| 744 |
-
out = (out / peak * target_peak).astype('float32')
|
| 745 |
-
return out
|
| 746 |
-
|
| 747 |
-
|
| 748 |
-
# ══════════════════════════════════════════════════════════════════════════════
|
| 749 |
-
# POST-PROCESSING (polishing after AI + guards)
|
| 750 |
-
# ══════════════════════════════════════════════════════════════════════════════
|
| 751 |
-
|
| 752 |
-
def _postprocess(sig: 'np.ndarray', sr: int, st: V4State) -> 'np.ndarray':
|
| 753 |
-
"""
|
| 754 |
-
Light polishing after AI resynthesis + protection layers:
|
| 755 |
-
- Sibilant naturalization (ص/س/ز/ش often over-smoothed by vocoders)
|
| 756 |
-
- Gentle micro-dynamics restoration
|
| 757 |
-
- Final loudness to -16 LUFS (approximate)
|
| 758 |
-
"""
|
| 759 |
-
out = sig.copy()
|
| 760 |
-
|
| 761 |
-
# 1. Sibilant check: if 4-8kHz energy is too low post-resynthesis, lift it
|
| 762 |
-
sib_db = _band_energy_db(out, sr, 4000, 8000)
|
| 763 |
-
ref_db = _band_energy_db(out, sr, 1000, 3000)
|
| 764 |
-
sib_diff = ref_db - sib_db
|
| 765 |
-
if sib_diff > 15.0:
|
| 766 |
-
# Sibilants have been over-smoothed — gently restore
|
| 767 |
-
n = len(out)
|
| 768 |
-
spec = rfft(out)
|
| 769 |
-
freqs = rfftfreq(n, 1.0 / sr)
|
| 770 |
-
boost_db = min(sib_diff * 0.4, 6.0) # proportional, capped at 6dB
|
| 771 |
-
boost_lin = 10.0 ** (boost_db / 20.0)
|
| 772 |
-
mask = np.ones(len(freqs), dtype='float32')
|
| 773 |
-
for ki, fq in enumerate(freqs):
|
| 774 |
-
if 4000 <= fq <= 12000:
|
| 775 |
-
t = (fq - 4000) / 8000.0
|
| 776 |
-
mask[ki] = 1.0 + (boost_lin - 1.0) * (1.0 - t * 0.5)
|
| 777 |
-
spec_b = spec * mask
|
| 778 |
-
out = np.fft.irfft(spec_b, n=n).astype('float32')
|
| 779 |
-
|
| 780 |
-
# 2. Gentle micro-dynamics: if LRA is too compressed, add subtle phrase modulation
|
| 781 |
-
frame_n = int(0.1 * sr) # 100ms frames
|
| 782 |
-
rms_frames = []
|
| 783 |
-
for i in range(0, len(out) - frame_n, frame_n):
|
| 784 |
-
rms_frames.append(float(np.sqrt(np.mean(out[i:i + frame_n] ** 2) + 1e-12)))
|
| 785 |
-
if rms_frames:
|
| 786 |
-
rms_arr = np.array(rms_frames)
|
| 787 |
-
lra_db = float(np.percentile(rms_arr, 95) / (np.percentile(rms_arr, 5) + 1e-12))
|
| 788 |
-
lra_db = 20.0 * np.log10(lra_db + 1e-8)
|
| 789 |
-
if lra_db < 2.0:
|
| 790 |
-
# Over-compressed; inject subtle ±0.5dB phrase modulation
|
| 791 |
-
for fi, i in enumerate(range(0, len(out) - frame_n, frame_n)):
|
| 792 |
-
mod = 1.0 + 0.05 * np.sin(2.0 * np.pi * fi / max(len(rms_frames), 1))
|
| 793 |
-
out[i:i + frame_n] *= float(mod)
|
| 794 |
-
|
| 795 |
-
# 3. Final loudness normalization to approximately -16 LUFS
|
| 796 |
-
rms_global = float(np.sqrt(np.mean(out ** 2) + 1e-12))
|
| 797 |
-
target_rms = 10.0 ** (-16.0 / 20.0) * 0.8 # approximate
|
| 798 |
-
if rms_global > 0:
|
| 799 |
-
out = (out * (target_rms / rms_global)).astype('float32')
|
| 800 |
-
|
| 801 |
-
# 4. True peak limiter
|
| 802 |
-
out = np.clip(out, -0.98, 0.98)
|
| 803 |
-
return out
|
| 804 |
-
|
| 805 |
-
|
| 806 |
-
# ══════════════════════════════════════════════════════════════════════════════
|
| 807 |
-
# MAIN PROCESS FUNCTION
|
| 808 |
-
# ══════════════════════════════════════════════════════════════════════════════
|
| 809 |
-
|
| 810 |
-
def process(
|
| 811 |
-
input_path: str,
|
| 812 |
-
output_path: str,
|
| 813 |
-
*,
|
| 814 |
-
skip_phoneme_guard: bool = False,
|
| 815 |
-
skip_speaker_anchor: bool = False,
|
| 816 |
-
skip_bwe: bool = False,
|
| 817 |
-
verbose: bool = True,
|
| 818 |
-
) -> Dict:
|
| 819 |
-
"""
|
| 820 |
-
Full الإحياء v4 pipeline.
|
| 821 |
-
|
| 822 |
-
Returns a dict with diagnostics and phase results.
|
| 823 |
-
"""
|
| 824 |
-
if not NUMPY_OK:
|
| 825 |
-
return {'error': 'numpy/scipy required: pip install numpy scipy soundfile'}
|
| 826 |
-
|
| 827 |
-
st = V4State()
|
| 828 |
-
t0 = time.time()
|
| 829 |
-
|
| 830 |
-
try:
|
| 831 |
-
# ── Decode input ──────────────────────────────────────────────────────
|
| 832 |
-
wav_in = st.tmpfile('.wav')
|
| 833 |
-
if not _decode_to_wav(input_path, wav_in):
|
| 834 |
-
return {'error': f'Failed to decode input: {input_path}'}
|
| 835 |
-
|
| 836 |
-
sig, sr = _load_wav(wav_in)
|
| 837 |
-
if sig is None or len(sig) < 256:
|
| 838 |
-
return {'error': 'Failed to load audio or audio too short'}
|
| 839 |
-
|
| 840 |
-
if verbose:
|
| 841 |
-
dur = len(sig) / sr
|
| 842 |
-
print(f' [الإحياء v4] Input: {dur:.1f}s @ {sr}Hz '
|
| 843 |
-
f'AI stack: SIDON={SIDON_OK} RE={RESEMBLE_OK} VF={VOICEFIXER_OK}')
|
| 844 |
-
|
| 845 |
-
# ── Phase 0: Analysis ─────────────────────────────────────────────────
|
| 846 |
-
_analyze(sig, sr, st)
|
| 847 |
-
if verbose:
|
| 848 |
-
print(f' [P0] SNR={st.snr_db:.1f}dB BW={st.active_bw_hz:.0f}Hz '
|
| 849 |
-
f'كتام={st.is_katam} F0≈{st.f0_median_hz:.0f}Hz')
|
| 850 |
-
|
| 851 |
-
# ── Phase 1: Light pre-processing ─────────────────────────────────────
|
| 852 |
-
sig_pre = _preprocess(sig, sr, st)
|
| 853 |
-
|
| 854 |
-
# ── Fingerprint extraction (BEFORE AI) ───────────────────────────────
|
| 855 |
-
guard = PhonemeGuard(sig_pre, sr) if not skip_phoneme_guard else None
|
| 856 |
-
anchor = SpeakerAnchor(sig_pre, sr) if not skip_speaker_anchor else None
|
| 857 |
-
|
| 858 |
-
# ── Phase 2: AI resynthesis ───────────────────────────────────────────
|
| 859 |
-
wav_pre = st.tmpfile('.wav')
|
| 860 |
-
wav_ai = st.tmpfile('.wav')
|
| 861 |
-
_save_wav(wav_pre, sig_pre, sr)
|
| 862 |
-
|
| 863 |
-
ai_success = _run_ai_cascade(wav_pre, wav_ai, st)
|
| 864 |
-
if not ai_success:
|
| 865 |
-
return {'error': 'All AI models and DSP fallback failed'}
|
| 866 |
-
|
| 867 |
-
if verbose:
|
| 868 |
-
print(f' [P2] AI model: {st.ai_model_used}')
|
| 869 |
-
|
| 870 |
-
sig_ai, sr_ai = _load_wav(wav_ai)
|
| 871 |
-
if sig_ai is None:
|
| 872 |
-
return {'error': 'Failed to load AI output'}
|
| 873 |
-
|
| 874 |
-
# Resample if AI output has different SR
|
| 875 |
-
if sr_ai != sr:
|
| 876 |
-
wav_resampled = st.tmpfile('.wav')
|
| 877 |
-
_decode_to_wav(wav_ai, wav_resampled, sr)
|
| 878 |
-
sig_ai, _ = _load_wav(wav_resampled)
|
| 879 |
-
if sig_ai is None:
|
| 880 |
-
return {'error': 'Failed to resample AI output'}
|
| 881 |
-
|
| 882 |
-
# Ensure length compatibility
|
| 883 |
-
target_n = len(sig_pre)
|
| 884 |
-
if len(sig_ai) != target_n:
|
| 885 |
-
if len(sig_ai) > target_n:
|
| 886 |
-
sig_ai = sig_ai[:target_n]
|
| 887 |
-
else:
|
| 888 |
-
sig_ai = np.pad(sig_ai, (0, target_n - len(sig_ai)))
|
| 889 |
-
|
| 890 |
-
# ── Layer 1: PhonemeGuard ────────────────────────��────────────────────
|
| 891 |
-
if guard is not None:
|
| 892 |
-
sig_guarded = guard.repair(sig_ai, st)
|
| 893 |
-
if verbose and st.guard_frames_repaired > 0:
|
| 894 |
-
print(f' [Guard] Repaired {st.guard_frames_repaired}/{st.guard_frames_checked} frames '
|
| 895 |
-
f'Lost bands: {", ".join(st.guard_bands_lost) or "none"}')
|
| 896 |
-
else:
|
| 897 |
-
sig_guarded = sig_ai
|
| 898 |
-
|
| 899 |
-
# ── Layer 2: SpeakerAnchor ────────────────────────────────────────────
|
| 900 |
-
if anchor is not None:
|
| 901 |
-
sig_anchored = anchor.anchor(sig_guarded, st)
|
| 902 |
-
if verbose:
|
| 903 |
-
flag = '⚠ FIRED' if st.anchor_fired else 'OK'
|
| 904 |
-
print(f' [Anchor] Speaker similarity={st.anchor_similarity:.3f} {flag}')
|
| 905 |
-
else:
|
| 906 |
-
sig_anchored = sig_guarded
|
| 907 |
-
|
| 908 |
-
# ── Layer 3: كتام BWE ─────────────────────────────────────────────────
|
| 909 |
-
if not skip_bwe:
|
| 910 |
-
sig_bwe = _katam_bwe(sig_anchored, sr, st)
|
| 911 |
-
if verbose and st.is_katam:
|
| 912 |
-
print(f' [BWE] كتام detected @ {st.katam_cutoff_hz:.0f}Hz — presence lifted')
|
| 913 |
-
else:
|
| 914 |
-
sig_bwe = sig_anchored
|
| 915 |
-
|
| 916 |
-
# ── AudioSR for extreme كتام (< 3kHz bandwidth) ──────────────────────
|
| 917 |
-
if st.is_katam and st.active_bw_hz < 3200 and AUDIOSR_OK:
|
| 918 |
-
try:
|
| 919 |
-
wav_bwe_in = st.tmpfile('.wav')
|
| 920 |
-
wav_bwe_out = st.tmpfile('.wav')
|
| 921 |
-
_save_wav(wav_bwe_in, sig_bwe, sr)
|
| 922 |
-
asr_model = _audiosr.build_model(model_name='basic', device='cpu')
|
| 923 |
-
waveform = _audiosr.super_resolution(
|
| 924 |
-
asr_model, wav_bwe_in,
|
| 925 |
-
seed=42, guidance_scale=3.5, ddim_steps=50,
|
| 926 |
-
latent_t_per_second=12.8,
|
| 927 |
-
)
|
| 928 |
-
_audiosr.save_wave(waveform, wav_bwe_out, sample_rate=SR)
|
| 929 |
-
sig_asr, _ = _load_wav(wav_bwe_out)
|
| 930 |
-
if sig_asr is not None:
|
| 931 |
-
if len(sig_asr) != len(sig_bwe):
|
| 932 |
-
sig_asr = sig_asr[:len(sig_bwe)] if len(sig_asr) > len(sig_bwe) else \
|
| 933 |
-
np.pad(sig_asr, (0, len(sig_bwe) - len(sig_asr)))
|
| 934 |
-
sig_bwe = sig_asr
|
| 935 |
-
if verbose:
|
| 936 |
-
print(f' [BWE] AudioSR super-resolution applied (extreme كتام)')
|
| 937 |
-
except Exception:
|
| 938 |
-
pass # AudioSR failed silently — already got basic BWE above
|
| 939 |
-
|
| 940 |
-
# ── Phase 4: Post-processing polish ───────────────────────────────────
|
| 941 |
-
sig_final = _postprocess(sig_bwe, sr, st)
|
| 942 |
-
|
| 943 |
-
# ── Save output ───────────────────────────────────────────────────────
|
| 944 |
-
Path(output_path).parent.mkdir(parents=True, exist_ok=True)
|
| 945 |
-
if not _save_wav(output_path, sig_final, sr):
|
| 946 |
-
return {'error': f'Failed to save output: {output_path}'}
|
| 947 |
-
|
| 948 |
-
elapsed = time.time() - t0
|
| 949 |
-
rtf = (len(sig) / sr) / max(elapsed, 0.001)
|
| 950 |
-
|
| 951 |
-
if verbose:
|
| 952 |
-
print(f' ══ Done in {elapsed:.1f}s RTF={rtf:.1f}x → {output_path}')
|
| 953 |
-
|
| 954 |
-
return {
|
| 955 |
-
'success': True,
|
| 956 |
-
'ai_model': st.ai_model_used,
|
| 957 |
-
'elapsed_s': elapsed,
|
| 958 |
-
'rtf': rtf,
|
| 959 |
-
'analysis': {
|
| 960 |
-
'snr_db': st.snr_db,
|
| 961 |
-
'bandwidth_hz': st.active_bw_hz,
|
| 962 |
-
'is_katam': st.is_katam,
|
| 963 |
-
'katam_cutoff': st.katam_cutoff_hz,
|
| 964 |
-
'is_noisy': st.is_noisy,
|
| 965 |
-
'f0_median_hz': st.f0_median_hz,
|
| 966 |
-
},
|
| 967 |
-
'phoneme_guard': {
|
| 968 |
-
'frames_checked': st.guard_frames_checked,
|
| 969 |
-
'frames_repaired': st.guard_frames_repaired,
|
| 970 |
-
'bands_lost': st.guard_bands_lost,
|
| 971 |
-
},
|
| 972 |
-
'speaker_anchor': {
|
| 973 |
-
'similarity': st.anchor_similarity,
|
| 974 |
-
'fired': st.anchor_fired,
|
| 975 |
-
},
|
| 976 |
-
}
|
| 977 |
-
|
| 978 |
-
except Exception as e:
|
| 979 |
-
import traceback
|
| 980 |
-
return {'error': str(e), 'traceback': traceback.format_exc()}
|
| 981 |
-
|
| 982 |
-
finally:
|
| 983 |
-
st.cleanup()
|
| 984 |
-
|
| 985 |
-
|
| 986 |
-
# ══════════════════════════════════════════════════════════════════════════════
|
| 987 |
-
# CLI
|
| 988 |
-
# ═══════════════════════════════════════════════════════════════���══════════════
|
| 989 |
-
|
| 990 |
-
def main() -> int:
|
| 991 |
-
if not NUMPY_OK:
|
| 992 |
-
print('pip install numpy scipy soundfile')
|
| 993 |
-
return 1
|
| 994 |
-
|
| 995 |
-
p = argparse.ArgumentParser(
|
| 996 |
-
description=f'الإحياء {__version__} — Parametric Resynthesis + Quran Phoneme Protection'
|
| 997 |
-
)
|
| 998 |
-
p.add_argument('-i', '--input', required=False)
|
| 999 |
-
p.add_argument('-o', '--output', required=False)
|
| 1000 |
-
p.add_argument('--skip-phoneme-guard', action='store_true',
|
| 1001 |
-
help='Disable PhonemeGuard (not recommended for Quran)')
|
| 1002 |
-
p.add_argument('--skip-speaker-anchor', action='store_true',
|
| 1003 |
-
help='Disable SpeakerAnchor (may cause voice drift)')
|
| 1004 |
-
p.add_argument('--skip-bwe', action='store_true',
|
| 1005 |
-
help='Disable كتام bandwidth extension')
|
| 1006 |
-
p.add_argument('--models', action='store_true',
|
| 1007 |
-
help='Print available AI models and exit')
|
| 1008 |
-
args = p.parse_args()
|
| 1009 |
-
|
| 1010 |
-
if args.models:
|
| 1011 |
-
print(f'\n الإحياء {__version__} — Available AI models:')
|
| 1012 |
-
print(f' SIDON {"✓ available" if SIDON_OK else "✗ pip install sidon"}')
|
| 1013 |
-
print(f' Resemble Enh. {"✓ available" if RESEMBLE_OK else "✗ pip install resemble-enhance"}')
|
| 1014 |
-
print(f' VoiceFixer {"✓ available" if VOICEFIXER_OK else "✗ pip install voicefixer"}')
|
| 1015 |
-
print(f' AudioSR (BWE) {"✓ available" if AUDIOSR_OK else "✗ pip install audiosr"}')
|
| 1016 |
-
print(f' PyTorch {"✓ available" if TORCH_OK else "✗ pip install torch"}')
|
| 1017 |
-
print(f'\n Numpy/Scipy {"✓" if NUMPY_OK else "✗"} soundfile {"✓" if SF_OK else "✗"}')
|
| 1018 |
-
print()
|
| 1019 |
-
return 0
|
| 1020 |
-
|
| 1021 |
-
if not args.input:
|
| 1022 |
-
p.print_help()
|
| 1023 |
-
return 1
|
| 1024 |
-
|
| 1025 |
-
if not os.path.exists(args.input):
|
| 1026 |
-
print(f'Input not found: {args.input}')
|
| 1027 |
-
return 1
|
| 1028 |
-
|
| 1029 |
-
if not args.output:
|
| 1030 |
-
base = Path(args.input).stem
|
| 1031 |
-
args.output = str(Path(args.input).parent / f'{base}_ihya4.wav')
|
| 1032 |
-
|
| 1033 |
-
print(f'\n ╔══ الإحياء v4 ══╗')
|
| 1034 |
-
print(f' ║ حروف لا تُؤكل ║')
|
| 1035 |
-
print(f' ║ الشيخ يبقى ║')
|
| 1036 |
-
print(f' ╚════════════════╝\n')
|
| 1037 |
-
|
| 1038 |
-
result = process(
|
| 1039 |
-
args.input, args.output,
|
| 1040 |
-
skip_phoneme_guard=args.skip_phoneme_guard,
|
| 1041 |
-
skip_speaker_anchor=args.skip_speaker_anchor,
|
| 1042 |
-
skip_bwe=args.skip_bwe,
|
| 1043 |
-
verbose=True,
|
| 1044 |
-
)
|
| 1045 |
-
|
| 1046 |
-
if 'error' in result:
|
| 1047 |
-
print(f'\n ERROR: {result["error"]}')
|
| 1048 |
-
if 'traceback' in result:
|
| 1049 |
-
print(result['traceback'])
|
| 1050 |
-
return 2
|
| 1051 |
-
|
| 1052 |
-
ana = result.get('analysis', {})
|
| 1053 |
-
pg = result.get('phoneme_guard', {})
|
| 1054 |
-
sa = result.get('speaker_anchor', {})
|
| 1055 |
-
|
| 1056 |
-
print(f'\n ══ Results ══════════════════════════════════════')
|
| 1057 |
-
print(f' AI model used: {result.get("ai_model", "?")}')
|
| 1058 |
-
print(f' Processing time: {result.get("elapsed_s", 0):.1f}s')
|
| 1059 |
-
print(f' SNR: {ana.get("snr_db", 0):.1f} dB')
|
| 1060 |
-
print(f' Active bandwidth: {ana.get("bandwidth_hz", 0):.0f} Hz')
|
| 1061 |
-
print(f' كتام detected: {"نعم" if ana.get("is_katam") else "لا"}')
|
| 1062 |
-
if ana.get('is_katam'):
|
| 1063 |
-
print(f' كتام cutoff: {ana.get("katam_cutoff", 0):.0f} Hz')
|
| 1064 |
-
print(f' PhonemeGuard: {pg.get("frames_repaired", 0)}/{pg.get("frames_checked", 0)} frames repaired')
|
| 1065 |
-
if pg.get('bands_lost'):
|
| 1066 |
-
print(f' Lost bands rescued: {", ".join(pg["bands_lost"])}')
|
| 1067 |
-
print(f' Speaker similarity: {sa.get("similarity", 0):.3f} '
|
| 1068 |
-
f'{"⚠ Anchored" if sa.get("fired") else "✓ Stable"}')
|
| 1069 |
-
print(f' Output: {args.output}')
|
| 1070 |
-
return 0
|
| 1071 |
-
|
| 1072 |
-
|
| 1073 |
-
if __name__ == '__main__':
|
| 1074 |
-
sys.exit(main())
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
engine_isteidad_server.py
CHANGED
|
@@ -15,6 +15,10 @@
|
|
| 15 |
# ║ • single-flight lock so one process never runs 2 heavy jobs at once ║
|
| 16 |
# ║ • all limits configurable per-Space via env vars (ISTEIDAD_*) ║
|
| 17 |
# ║ • get_server_status() health-check helper for app.py / load balancer ║
|
|
|
|
|
|
|
|
|
|
|
|
|
| 18 |
# ║ ║
|
| 19 |
# ╚══════════════════════════════════════════════════════════════════════════════╝
|
| 20 |
# ╔══════════════════════════════════════════════════════════════════════════════╗
|
|
@@ -5977,6 +5981,26 @@ def analyze_input(path: str, ref: ReferenceModel) -> InputState:
|
|
| 5977 |
state.src_br, state.codec_cutoff, _snr_for_tier,
|
| 5978 |
state.noise_type, state.smear_score,
|
| 5979 |
src_sr=state.src_sr, clip_ratio=state.clip_ratio)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 5980 |
state.achievable_lufs, state.achievable_crest, state.achievable_lra = \
|
| 5981 |
_compute_achievable(state.source_tier, state.codec_cutoff,
|
| 5982 |
src_sr=state.src_sr, clip_ratio=state.clip_ratio,
|
|
|
|
| 15 |
# ║ • single-flight lock so one process never runs 2 heavy jobs at once ║
|
| 16 |
# ║ • all limits configurable per-Space via env vars (ISTEIDAD_*) ║
|
| 17 |
# ║ • get_server_status() health-check helper for app.py / load balancer ║
|
| 18 |
+
# ║ • PERMANENT: TIER_DAMAGED/TIER_CRITICAL no longer hands off to إحياء — ║
|
| 19 |
+
# ║ remapped to TIER_DEGRADED so Isteidad's own pipeline always runs. ║
|
| 20 |
+
# ║ Set ISTEIDAD_ALLOW_IHYAA=1 to restore the original tier-gated ║
|
| 21 |
+
# ║ إحياء handoff. ║
|
| 22 |
# ║ ║
|
| 23 |
# ╚══════════════════════════════════════════════════════════════════════════════╝
|
| 24 |
# ╔══════════════════════════════════════════════════════════════════════════════╗
|
|
|
|
| 5981 |
state.src_br, state.codec_cutoff, _snr_for_tier,
|
| 5982 |
state.noise_type, state.smear_score,
|
| 5983 |
src_sr=state.src_sr, clip_ratio=state.clip_ratio)
|
| 5984 |
+
|
| 5985 |
+
# ── SERVER OVERRIDE (permanent, per request): never hand off to إحياء ────
|
| 5986 |
+
# By design, TIER_DAMAGED/TIER_CRITICAL sources were routed to إحياء
|
| 5987 |
+
# structural recovery instead of Isteidad's own noor/jalal/nidaa/جوهر
|
| 5988 |
+
# pipeline (VOICE_ENHANCEMENT_NOTES_v2 — avoids "ping-pong" between
|
| 5989 |
+
# modules on badly-damaged audio). Remapping here is the single point of
|
| 5990 |
+
# control: every downstream `state.source_tier in ('TIER_DAMAGED',
|
| 5991 |
+
# 'TIER_CRITICAL')` check (the إحياء branch, the noor/jalal/nidaa skip,
|
| 5992 |
+
# several NR-strength and dynamics ceilings) now sees TIER_DEGRADED and
|
| 5993 |
+
# takes Isteidad's own path instead. This also relaxes the quality
|
| 5994 |
+
# ceiling and NR caps that existed specifically because إحياء+DF3 in
|
| 5995 |
+
# combination were judged unsafe on this class of source — badly
|
| 5996 |
+
# degraded audio now gets the same processing as merely degraded audio.
|
| 5997 |
+
# Set ISTEIDAD_ALLOW_IHYAA=1 to restore the original tier-gated handoff.
|
| 5998 |
+
if state.source_tier in ('TIER_DAMAGED', 'TIER_CRITICAL') and \
|
| 5999 |
+
os.environ.get('ISTEIDAD_ALLOW_IHYAA', '0') != '1':
|
| 6000 |
+
L(f' [server] source_tier {state.source_tier} → remapped to TIER_DEGRADED '
|
| 6001 |
+
f'(إحياء handoff disabled; set ISTEIDAD_ALLOW_IHYAA=1 to restore it)')
|
| 6002 |
+
state.source_tier = 'TIER_DEGRADED'
|
| 6003 |
+
|
| 6004 |
state.achievable_lufs, state.achievable_crest, state.achievable_lra = \
|
| 6005 |
_compute_achievable(state.source_tier, state.codec_cutoff,
|
| 6006 |
src_sr=state.src_sr, clip_ratio=state.clip_ratio,
|
requirements.txt
CHANGED
|
@@ -7,9 +7,7 @@ librosa
|
|
| 7 |
soundfile
|
| 8 |
nara_wpe
|
| 9 |
|
| 10 |
-
# S202: BWE for telephone-tier (TIER_TELEPHONE) audio in engine_ihya_v3.py
|
| 11 |
--extra-index-url https://download.pytorch.org/whl/cpu
|
| 12 |
torch==2.3.1+cpu
|
| 13 |
-
voicefixer2 @ git+https://github.com/Render-AI-Team/voicefixer2.git
|
| 14 |
torchaudio==2.3.1+cpu
|
| 15 |
psutil
|
|
|
|
| 7 |
soundfile
|
| 8 |
nara_wpe
|
| 9 |
|
|
|
|
| 10 |
--extra-index-url https://download.pytorch.org/whl/cpu
|
| 11 |
torch==2.3.1+cpu
|
|
|
|
| 12 |
torchaudio==2.3.1+cpu
|
| 13 |
psutil
|