carm5333 commited on
Commit
a6500a6
·
verified ·
1 Parent(s): a84be59

S214: remove إحياء v4 entirely, isteidad-server takes v11.2; clean requirements.txt (cache-bust + voicefixer2 gone)

Browse files
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 / "engine_ihya_v4.py", # S204: الإحياء v4 (AI cascade + guards)
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 (_is_safaa or _is_ihya) and _o_path != job["out_path"]:
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