Spaces:
Running
Running
Tilawa Enhancer commited on
Commit Β·
7fba2c5
1
Parent(s): e278256
S175: safaa v4 (DF3 path fix, nara_wpe, --tier/--mujawwad, rm v3_fixed)
Browse files- app.py +4 -2
- engine_safaa_v3_fixed.py β engine_safaa_v4.py +320 -136
- requirements.txt +1 -0
app.py
CHANGED
|
@@ -33,7 +33,7 @@ for _d in [UPLOAD_DIR, CHUNK_DIR, OUTPUT_DIR]:
|
|
| 33 |
BASE = Path(__file__).parent
|
| 34 |
|
| 35 |
ENGINE_SCRIPTS = {
|
| 36 |
-
"v11.0": BASE / "
|
| 37 |
"v11.1": BASE / "engine_itiqan_v6_official.py", # S163
|
| 38 |
"v11.2": BASE / "engine_isteidad_v21.py", # S162-SV-B1
|
| 39 |
"v10.0": BASE / "engine_safaa_v3_fixed.py", # S149
|
|
@@ -334,7 +334,9 @@ def _run_engine(job_id):
|
|
| 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 |
else:
|
| 339 |
cmd = ["python3", str(script),
|
| 340 |
"-i", job["in_path"], "-o", _o_path,
|
|
|
|
| 33 |
BASE = Path(__file__).parent
|
| 34 |
|
| 35 |
ENGINE_SCRIPTS = {
|
| 36 |
+
"v11.0": BASE / "engine_safaa_v4.py", # S175 # S149
|
| 37 |
"v11.1": BASE / "engine_itiqan_v6_official.py", # S163
|
| 38 |
"v11.2": BASE / "engine_isteidad_v21.py", # S162-SV-B1
|
| 39 |
"v10.0": BASE / "engine_safaa_v3_fixed.py", # S149
|
|
|
|
| 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,
|
engine_safaa_v3_fixed.py β engine_safaa_v4.py
RENAMED
|
@@ -39,9 +39,9 @@ KB REFS: Β§3 Β§28 Β§35 Β§36 Β§52 Β§79 Β§109 Β§138 Β§140 Β§143 Β§145 Β§151 Β§152
|
|
| 39 |
"""
|
| 40 |
from __future__ import annotations
|
| 41 |
|
| 42 |
-
__version__ = '
|
| 43 |
|
| 44 |
-
import os, shutil, subprocess, tempfile,
|
| 45 |
from concurrent.futures import ThreadPoolExecutor, as_completed
|
| 46 |
from dataclasses import dataclass, field
|
| 47 |
from pathlib import Path
|
|
@@ -70,9 +70,12 @@ except ImportError:
|
|
| 70 |
|
| 71 |
# DF3 binary
|
| 72 |
_DF3_CLI_BIN = ''
|
| 73 |
-
for _c in [
|
| 74 |
-
|
| 75 |
-
|
|
|
|
|
|
|
|
|
|
| 76 |
_DF3_CLI_BIN = _c; break
|
| 77 |
DF3_OK = bool(_DF3_CLI_BIN)
|
| 78 |
|
|
@@ -94,12 +97,12 @@ RT60_TAILNR = 0.30
|
|
| 94 |
RT60_AGGR = 1.50
|
| 95 |
MUJ_RT60_FLOOR = 1.20 # Β§145.3
|
| 96 |
|
| 97 |
-
# Β§79 per-style DF attenuation hard limits
|
| 98 |
-
_DF3_LIM_MURATTAL =
|
| 99 |
-
_DF3_LIM_MUJAWWAD =
|
| 100 |
-
_DF3_SPEECH = 12
|
| 101 |
-
_DF3_TRANS = 20
|
| 102 |
-
_DF3_TAIL = 28
|
| 103 |
|
| 104 |
_CHUNK_S = 0.100
|
| 105 |
_XFADE_N = 960
|
|
@@ -107,7 +110,7 @@ _XFADE_N = 960
|
|
| 107 |
_RMS_MAX_DELTA = 1.0
|
| 108 |
_LRA_MAX_DELTA = 0.5 # Β§109.4
|
| 109 |
|
| 110 |
-
DRR_ALREADY_DRY =
|
| 111 |
|
| 112 |
|
| 113 |
# βββ State ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
|
@@ -154,8 +157,7 @@ def _run(cmd, timeout=600):
|
|
| 154 |
return r.returncode, r.stdout, r.stderr
|
| 155 |
|
| 156 |
def _tmp(tag, st=None):
|
| 157 |
-
|
| 158 |
-
p = os.path.join(_TMP, f'safaa3_{tag}_{os.getpid()}_{time.time_ns()}.wav')
|
| 159 |
if st is not None:
|
| 160 |
st._tmps.append(p)
|
| 161 |
return p
|
|
@@ -180,7 +182,7 @@ def _decode(path, st=None):
|
|
| 180 |
try:
|
| 181 |
if SF_OK:
|
| 182 |
# ffmpeg β pcm_f32le temp, then soundfile reads it natively
|
| 183 |
-
t = os.path.join(_TMP, f'safaa3_dec_{os.getpid()}
|
| 184 |
rc, _, _ = _run(['ffmpeg', '-y', '-i', path,
|
| 185 |
'-acodec', 'pcm_f32le',
|
| 186 |
'-ar', str(SR), '-ac', '1',
|
|
@@ -191,7 +193,7 @@ def _decode(path, st=None):
|
|
| 191 |
return data
|
| 192 |
elif SCIPY_WAV_OK:
|
| 193 |
# scipy.io.wavfile can read pcm_f32le natively
|
| 194 |
-
t = os.path.join(_TMP, f'safaa3_dec_{os.getpid()}
|
| 195 |
rc, _, _ = _run(['ffmpeg', '-y', '-i', path,
|
| 196 |
'-acodec', 'pcm_f32le',
|
| 197 |
'-ar', str(SR), '-ac', '1',
|
|
@@ -204,7 +206,7 @@ def _decode(path, st=None):
|
|
| 204 |
return data.copy()
|
| 205 |
else:
|
| 206 |
# Final fallback: pcm_s16le β int16 β float32 normalised
|
| 207 |
-
t = os.path.join(_TMP, f'safaa3_dec_{os.getpid()}
|
| 208 |
rc, _, _ = _run(['ffmpeg', '-y', '-i', path,
|
| 209 |
'-acodec', 'pcm_s16le',
|
| 210 |
'-ar', str(SR), '-ac', '1',
|
|
@@ -446,115 +448,227 @@ def _wpe(wav, rt60, st):
|
|
| 446 |
shutil.rmtree(d, ignore_errors=True)
|
| 447 |
|
| 448 |
|
| 449 |
-
# βββ Stage 4: DF3
|
| 450 |
-
|
| 451 |
-
|
| 452 |
-
|
| 453 |
-
|
| 454 |
-
|
| 455 |
-
|
| 456 |
-
|
| 457 |
-
|
| 458 |
-
|
| 459 |
-
|
| 460 |
-
|
| 461 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 462 |
|
| 463 |
def _df3(wav, samples, rt60, st):
|
| 464 |
if not DF3_OK:
|
| 465 |
_L(st, ' [S4-DF3] deep-filter not found β skip'); return wav
|
| 466 |
|
| 467 |
-
#
|
| 468 |
-
|
| 469 |
-
|
| 470 |
-
|
| 471 |
-
|
| 472 |
-
#
|
| 473 |
-
if st.mujawwad_conf > 0.6:
|
| 474 |
-
lim = _DF3_LIM_MUJAWWAD
|
| 475 |
-
else:
|
| 476 |
-
lim = _DF3_LIM_MURATTAL
|
| 477 |
-
sa = min(sa, lim)
|
| 478 |
-
ra = min(ra, lim + 8) # transition/tail may exceed speech limit but respect style spirit
|
| 479 |
-
ta = min(ta, lim + 16)
|
| 480 |
if st.mujawwad_conf > 0.6:
|
| 481 |
-
|
|
|
|
|
|
|
|
|
|
| 482 |
|
| 483 |
-
import wave as _w
|
| 484 |
d = tempfile.mkdtemp(prefix='safaa3_df3_')
|
| 485 |
try:
|
|
|
|
| 486 |
fi = os.path.join(d, 'in.wav')
|
| 487 |
rc, _, _ = _run(['ffmpeg', '-y', '-i', wav,
|
| 488 |
'-acodec', 'pcm_s16le', '-ar', str(SR), '-ac', '1',
|
| 489 |
'-loglevel', 'error', fi])
|
| 490 |
if rc or not os.path.exists(fi):
|
| 491 |
return wav
|
| 492 |
-
|
| 493 |
-
|
| 494 |
-
|
| 495 |
-
cn = int(_CHUNK_S * SR); nc = len(s16) // cn
|
| 496 |
-
if nc < 1:
|
| 497 |
return wav
|
| 498 |
-
|
| 499 |
-
|
| 500 |
-
|
| 501 |
-
|
| 502 |
-
|
| 503 |
-
|
| 504 |
-
|
| 505 |
-
|
| 506 |
-
|
| 507 |
-
|
| 508 |
-
|
| 509 |
-
|
| 510 |
-
|
| 511 |
-
|
| 512 |
-
|
| 513 |
-
|
| 514 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 515 |
for fut in as_completed(futs):
|
| 516 |
-
|
| 517 |
-
|
| 518 |
-
|
| 519 |
-
|
| 520 |
-
|
| 521 |
-
|
| 522 |
-
|
| 523 |
-
|
| 524 |
-
|
| 525 |
-
|
| 526 |
-
|
| 527 |
-
|
| 528 |
-
|
| 529 |
-
|
| 530 |
-
|
| 531 |
-
|
| 532 |
-
|
| 533 |
-
|
| 534 |
-
|
| 535 |
-
|
| 536 |
-
|
| 537 |
-
|
| 538 |
-
|
| 539 |
-
|
| 540 |
-
|
| 541 |
-
|
| 542 |
-
|
| 543 |
-
|
| 544 |
-
|
| 545 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 546 |
out = _tmp('s4', st)
|
| 547 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 548 |
return wav
|
| 549 |
|
| 550 |
-
delta = _rmsdb(
|
| 551 |
-
if abs(delta) > _RMS_MAX_DELTA * 2:
|
| 552 |
-
_cleanup(out); st.guard_reverts += 1
|
| 553 |
-
_L(st, f' [S4-DF3] RMS Ξ={delta:+.2f}dB β REVERT'); return wav
|
| 554 |
st.df3 = True
|
| 555 |
-
_L(st, f' [S4-
|
| 556 |
-
|
| 557 |
-
f'tail={int(np.sum(labels==2))}')
|
| 558 |
return out
|
| 559 |
except Exception as e:
|
| 560 |
_L(st, f' [S4-DF3] exception: {e}'); return wav
|
|
@@ -595,18 +709,29 @@ def _jalaa(wav, samples, rt60, drr_before, st):
|
|
| 595 |
if not nf_vals:
|
| 596 |
_L(st, ' [S5-JALAA] no reverb frames β skip'); return wav
|
| 597 |
|
| 598 |
-
nf = float(np.clip(float(np.median(nf_vals)) + 3, -72, -
|
| 599 |
-
nr =
|
| 600 |
if st.mujawwad_conf > 0.6: nr = max(1, nr - 1)
|
| 601 |
|
| 602 |
-
|
|
|
|
| 603 |
rc, _, _ = _run(['ffmpeg', '-y', '-i', wav,
|
| 604 |
-
'-af', f'afftdn=nr={nr}:nf={nf:.0f}:tn=1',
|
| 605 |
'-acodec', WAV_CODEC, '-ar', str(SR), '-ac', '1',
|
| 606 |
'-loglevel', 'error', out])
|
| 607 |
if rc or not os.path.exists(out):
|
| 608 |
return wav
|
| 609 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 610 |
post = _decode(out)
|
| 611 |
if post is not None:
|
| 612 |
d = _rmsdb(post) - _rmsdb(samples)
|
|
@@ -619,35 +744,96 @@ def _jalaa(wav, samples, rt60, drr_before, st):
|
|
| 619 |
return out
|
| 620 |
|
| 621 |
|
| 622 |
-
# βββ Stage 6: Tail floor NR βββββββββββββββββββββ
|
| 623 |
def _tailnr(wav, samples, rt60, st):
|
| 624 |
"""
|
|
|
|
|
|
|
|
|
|
|
|
|
| 625 |
[I3] If JALAA already ran, reduce nr by 1 to avoid double-attenuation.
|
|
|
|
| 626 |
"""
|
| 627 |
if rt60 < RT60_TAILNR or samples is None or not NUMPY_OK:
|
| 628 |
return wav
|
| 629 |
fn = int(0.200 * SR)
|
| 630 |
overall = _rmsdb(samples)
|
| 631 |
-
fdb = np.array([float(20 * np.log10(np.sqrt(np.mean(samples[i:i
|
| 632 |
for i in range(0, len(samples) - fn, fn)])
|
| 633 |
quiet = fdb[fdb < overall - 10]
|
| 634 |
if len(quiet) == 0:
|
| 635 |
return wav
|
| 636 |
-
|
| 637 |
-
|
|
|
|
|
|
|
| 638 |
if st.mujawwad_conf > 0.6: nr = max(1, nr - 1)
|
| 639 |
-
if st.jalaa: nr = max(1, nr - 1) # [I3]
|
| 640 |
-
|
|
|
|
|
|
|
| 641 |
rc, _, _ = _run(['ffmpeg', '-y', '-i', wav,
|
| 642 |
-
'-af', f'afftdn=nr={nr}:nf={nf:.0f}:
|
| 643 |
-
'-acodec', WAV_CODEC, '-ar', str(SR), '-ac', '
|
| 644 |
-
'-loglevel', 'error',
|
| 645 |
-
if rc or not os.path.exists(
|
| 646 |
return wav
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 647 |
st.tail_nr = True
|
| 648 |
-
jalaa_note = ' (JALAA-
|
| 649 |
-
_L(st, f' [S6-tailNR] β nr={nr}{jalaa_note} nf={nf:.0f}dB'
|
| 650 |
-
|
|
|
|
| 651 |
|
| 652 |
|
| 653 |
# βββ Stage 7: Arabic phoneme guards (Β§35/Β§52/Β§143/Β§152) ββββββββββββββββββββββ
|
|
@@ -804,14 +990,16 @@ def process(input_path, output_path, source_tier='TIER_UNKNOWN',
|
|
| 804 |
# S7 Arabic guards
|
| 805 |
cur = _arabic_guards(s_orig, cur, st)
|
| 806 |
|
| 807 |
-
# Final stereo encode
|
| 808 |
rc, _, err = _run(['ffmpeg', '-y', '-i', cur,
|
| 809 |
-
'-
|
| 810 |
-
|
|
|
|
|
|
|
|
|
|
| 811 |
'-loglevel', 'error', output_path])
|
| 812 |
if rc:
|
| 813 |
-
_L(st, f' ERROR: final encode: {err[:80]}')
|
| 814 |
-
sys.exit(1) # S169b: non-zero rc so app.py detects failure
|
| 815 |
|
| 816 |
sf_ = _decode(output_path)
|
| 817 |
if sf_ is not None: st.drr_after = _drr(sf_)
|
|
@@ -834,15 +1022,11 @@ def process(input_path, output_path, source_tier='TIER_UNKNOWN',
|
|
| 834 |
if __name__ == '__main__':
|
| 835 |
import argparse, json
|
| 836 |
ap = argparse.ArgumentParser(description=f'Ψ§ΩΨ΅ΩΨ§Ψ‘ {__version__} β Dereverberation Engine')
|
| 837 |
-
|
| 838 |
-
|
| 839 |
-
ap.add_argument('-
|
| 840 |
-
ap.add_argument('-
|
| 841 |
-
ap.add_argument('--
|
| 842 |
-
ap.add_argument('--ref', action='append', default=[], metavar='REF') # accepted, ignored
|
| 843 |
-
ap.add_argument('--tier', default='TIER_UNKNOWN')
|
| 844 |
-
ap.add_argument('--mujawwad', type=float, default=0.0)
|
| 845 |
-
ap.add_argument('--rt60', type=float, default=0.0, help='Force RT60 (0=auto)')
|
| 846 |
a = ap.parse_args()
|
| 847 |
st = process(a.input, a.output,
|
| 848 |
source_tier=a.tier, mujawwad_conf=a.mujawwad, force_rt60=a.rt60)
|
|
|
|
| 39 |
"""
|
| 40 |
from __future__ import annotations
|
| 41 |
|
| 42 |
+
__version__ = 'v4'
|
| 43 |
|
| 44 |
+
import os, shutil, subprocess, tempfile, warnings
|
| 45 |
from concurrent.futures import ThreadPoolExecutor, as_completed
|
| 46 |
from dataclasses import dataclass, field
|
| 47 |
from pathlib import Path
|
|
|
|
| 70 |
|
| 71 |
# DF3 binary
|
| 72 |
_DF3_CLI_BIN = ''
|
| 73 |
+
for _c in [
|
| 74 |
+
str(Path(__file__).parent / 'deep-filter'), # S175: /app/deep-filter on HF β
|
| 75 |
+
'/home/claude/deep-filter', # local dev fallback
|
| 76 |
+
'deep-filter', 'deepfilter', 'deep_filter',
|
| 77 |
+
]:
|
| 78 |
+
if Path(_c).exists() or shutil.which(_c):
|
| 79 |
_DF3_CLI_BIN = _c; break
|
| 80 |
DF3_OK = bool(_DF3_CLI_BIN)
|
| 81 |
|
|
|
|
| 97 |
RT60_AGGR = 1.50
|
| 98 |
MUJ_RT60_FLOOR = 1.20 # Β§145.3
|
| 99 |
|
| 100 |
+
# Β§79 per-style DF attenuation hard limits β v4: raised for deeper cleaning
|
| 101 |
+
_DF3_LIM_MURATTAL = 24 # dB maximum (was 18)
|
| 102 |
+
_DF3_LIM_MUJAWWAD = 8 # dB maximum (was 6)
|
| 103 |
+
_DF3_SPEECH = 15 # was 12
|
| 104 |
+
_DF3_TRANS = 24 # was 20
|
| 105 |
+
_DF3_TAIL = 32 # was 28
|
| 106 |
|
| 107 |
_CHUNK_S = 0.100
|
| 108 |
_XFADE_N = 960
|
|
|
|
| 110 |
_RMS_MAX_DELTA = 1.0
|
| 111 |
_LRA_MAX_DELTA = 0.5 # Β§109.4
|
| 112 |
|
| 113 |
+
DRR_ALREADY_DRY = 3.0 # v4: lower threshold β JALAA runs on more material (was 6.0)
|
| 114 |
|
| 115 |
|
| 116 |
# βββ State ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
|
|
|
| 157 |
return r.returncode, r.stdout, r.stderr
|
| 158 |
|
| 159 |
def _tmp(tag, st=None):
|
| 160 |
+
p = os.path.join(_TMP, f'safaa3_{tag}_{os.getpid()}.wav')
|
|
|
|
| 161 |
if st is not None:
|
| 162 |
st._tmps.append(p)
|
| 163 |
return p
|
|
|
|
| 182 |
try:
|
| 183 |
if SF_OK:
|
| 184 |
# ffmpeg β pcm_f32le temp, then soundfile reads it natively
|
| 185 |
+
t = os.path.join(_TMP, f'safaa3_dec_{os.getpid()}.wav')
|
| 186 |
rc, _, _ = _run(['ffmpeg', '-y', '-i', path,
|
| 187 |
'-acodec', 'pcm_f32le',
|
| 188 |
'-ar', str(SR), '-ac', '1',
|
|
|
|
| 193 |
return data
|
| 194 |
elif SCIPY_WAV_OK:
|
| 195 |
# scipy.io.wavfile can read pcm_f32le natively
|
| 196 |
+
t = os.path.join(_TMP, f'safaa3_dec_{os.getpid()}.wav')
|
| 197 |
rc, _, _ = _run(['ffmpeg', '-y', '-i', path,
|
| 198 |
'-acodec', 'pcm_f32le',
|
| 199 |
'-ar', str(SR), '-ac', '1',
|
|
|
|
| 206 |
return data.copy()
|
| 207 |
else:
|
| 208 |
# Final fallback: pcm_s16le β int16 β float32 normalised
|
| 209 |
+
t = os.path.join(_TMP, f'safaa3_dec_{os.getpid()}.wav')
|
| 210 |
rc, _, _ = _run(['ffmpeg', '-y', '-i', path,
|
| 211 |
'-acodec', 'pcm_s16le',
|
| 212 |
'-ar', str(SR), '-ac', '1',
|
|
|
|
| 448 |
shutil.rmtree(d, ignore_errors=True)
|
| 449 |
|
| 450 |
|
| 451 |
+
# βββ Stage 4: DF3 smooth adaptive VAD (Β§22.5 / Β§106 / RL-01 / RL-16) βββββββββ
|
| 452 |
+
_ADF_LOUD_T = -15.0 # dBFS β projecting voice β protect
|
| 453 |
+
_ADF_QUIET_T = -25.0 # dBFS β soft/breath β clean hard
|
| 454 |
+
_ADF_SNR_GATE = 10.0 # v4: lower gate β more chunks get cleaned (was 12)
|
| 455 |
+
_ADF_CHUNK_S = 0.050 # 50ms chunks β finer granularity than isteidad 100ms
|
| 456 |
+
_ADF_XFADE = int(0.120 * 48000) # v4: 120ms crossfade (was 40ms) β much smoother seams
|
| 457 |
+
# RL-16: guard bands β restore these from original after blend
|
| 458 |
+
_RL16_BANDS = [(220, 290), (950, 1100)] # Hz: Ghunnah nasal pole + formant zone
|
| 459 |
+
|
| 460 |
+
def _df3_pipe_decode(path, sr):
|
| 461 |
+
"""Decode WAV to float32 mono via ffmpeg pipe β avoids wave/soundfile dep."""
|
| 462 |
+
r = subprocess.run(
|
| 463 |
+
['ffmpeg', '-nostdin', '-y', '-hide_banner', '-loglevel', 'error',
|
| 464 |
+
'-i', path, '-ar', str(sr), '-ac', '1', '-f', 'f32le', '-'],
|
| 465 |
+
capture_output=True, timeout=120)
|
| 466 |
+
if r.returncode or len(r.stdout) < 4:
|
| 467 |
+
return None
|
| 468 |
+
return np.frombuffer(r.stdout, dtype=np.float32).copy()
|
| 469 |
+
|
| 470 |
+
def _stft_restore_bands(processed, original, sr, bands):
|
| 471 |
+
"""
|
| 472 |
+
RL-16: after DF3 blend, restore specified Hz bands from original.
|
| 473 |
+
Uses STFT with 50% overlap Hann window β preserves phase coherence.
|
| 474 |
+
"""
|
| 475 |
+
from numpy.fft import rfft, irfft
|
| 476 |
+
N = 2048; HOP = N // 2
|
| 477 |
+
freqs = np.fft.rfftfreq(N, d=1.0/sr) # Hz per bin
|
| 478 |
+
restore_mask = np.zeros(len(freqs), dtype=bool)
|
| 479 |
+
for lo, hi in bands:
|
| 480 |
+
restore_mask |= (freqs >= lo) & (freqs <= hi)
|
| 481 |
+
|
| 482 |
+
win = np.hanning(N).astype(np.float32)
|
| 483 |
+
min_n = min(len(processed), len(original))
|
| 484 |
+
p = processed[:min_n].astype(np.float64)
|
| 485 |
+
o = original[:min_n].astype(np.float64)
|
| 486 |
+
out = np.zeros(min_n, dtype=np.float64)
|
| 487 |
+
norm = np.zeros(min_n, dtype=np.float64)
|
| 488 |
+
|
| 489 |
+
for s in range(0, min_n - N, HOP):
|
| 490 |
+
Pf = rfft(p[s:s+N] * win)
|
| 491 |
+
Of = rfft(o[s:s+N] * win)
|
| 492 |
+
Pf[restore_mask] = Of[restore_mask] # restore original in guard bands
|
| 493 |
+
frame = irfft(Pf) * win
|
| 494 |
+
out[s:s+N] += frame; norm[s:s+N] += win**2
|
| 495 |
+
|
| 496 |
+
valid = norm > 1e-8
|
| 497 |
+
out[valid] /= norm[valid]
|
| 498 |
+
out[~valid] = p[~valid]
|
| 499 |
+
return out.astype(np.float32)
|
| 500 |
|
| 501 |
def _df3(wav, samples, rt60, st):
|
| 502 |
if not DF3_OK:
|
| 503 |
_L(st, ' [S4-DF3] deep-filter not found β skip'); return wav
|
| 504 |
|
| 505 |
+
# Β§106.3 + Β§79 atten limits β v4: stronger per-class
|
| 506 |
+
lim = _DF3_LIM_MUJAWWAD if st.mujawwad_conf > 0.6 else _DF3_LIM_MURATTAL
|
| 507 |
+
a_loud = min(10, lim) # was 8
|
| 508 |
+
a_mid = min(18, lim) # was 15
|
| 509 |
+
a_quiet = min(24, lim + 10) # was 20
|
| 510 |
+
a_trans = min(14, lim) # v4: new transition class between loud/mid
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 511 |
if st.mujawwad_conf > 0.6:
|
| 512 |
+
a_loud = min(a_loud, 8)
|
| 513 |
+
a_mid = min(a_mid, 10)
|
| 514 |
+
a_quiet = min(a_quiet, 14)
|
| 515 |
+
a_trans = min(a_trans, 9)
|
| 516 |
|
|
|
|
| 517 |
d = tempfile.mkdtemp(prefix='safaa3_df3_')
|
| 518 |
try:
|
| 519 |
+
# ββ Prepare pcm_s16le mono input ββββββββββββββββββββββββββββββββββ
|
| 520 |
fi = os.path.join(d, 'in.wav')
|
| 521 |
rc, _, _ = _run(['ffmpeg', '-y', '-i', wav,
|
| 522 |
'-acodec', 'pcm_s16le', '-ar', str(SR), '-ac', '1',
|
| 523 |
'-loglevel', 'error', fi])
|
| 524 |
if rc or not os.path.exists(fi):
|
| 525 |
return wav
|
| 526 |
+
mono = _df3_pipe_decode(fi, SR)
|
| 527 |
+
if mono is None:
|
|
|
|
|
|
|
|
|
|
| 528 |
return wav
|
| 529 |
+
total = len(mono)
|
| 530 |
+
|
| 531 |
+
# ββ VAD β 50ms chunks with SNR gate (Β§22.5) ββββββββββββββββββββββ
|
| 532 |
+
CHUNK = int(_ADF_CHUNK_S * SR)
|
| 533 |
+
nc = max(1, total // CHUNK)
|
| 534 |
+
# Estimate noise floor from quietest 10% of chunks
|
| 535 |
+
rms_db = np.array([
|
| 536 |
+
20.0 * np.log10(np.sqrt(np.mean(mono[i*CHUNK:(i+1)*CHUNK]**2)) + 1e-12)
|
| 537 |
+
for i in range(nc)])
|
| 538 |
+
noise_floor = float(np.percentile(rms_db, 10))
|
| 539 |
+
|
| 540 |
+
loud_m = rms_db > _ADF_LOUD_T
|
| 541 |
+
quiet_m = rms_db <= _ADF_QUIET_T
|
| 542 |
+
mid_m = ~loud_m & ~quiet_m
|
| 543 |
+
# Β§22.5 SNR gate: chunks already clean enough β skip DF3, use original
|
| 544 |
+
chunk_snr = rms_db - noise_floor
|
| 545 |
+
clean_m = chunk_snr >= _ADF_SNR_GATE
|
| 546 |
+
|
| 547 |
+
# ββ Label smoothing β mode filter over 7-chunk window (v4: was 3) ββββββ
|
| 548 |
+
# Wider window prevents micro-class flicker that causes audible texture changes
|
| 549 |
+
raw_cls = np.where(loud_m, 0, np.where(mid_m, 1, 2))
|
| 550 |
+
smoothed_cls = raw_cls.copy()
|
| 551 |
+
half = 3
|
| 552 |
+
for i in range(half, nc - half):
|
| 553 |
+
window = raw_cls[i-half:i+half+1]
|
| 554 |
+
counts = np.bincount(window, minlength=3)
|
| 555 |
+
smoothed_cls[i] = int(np.argmax(counts))
|
| 556 |
+
loud_m = smoothed_cls == 0
|
| 557 |
+
mid_m = smoothed_cls == 1
|
| 558 |
+
quiet_m = smoothed_cls == 2
|
| 559 |
+
|
| 560 |
+
n_loud = int(loud_m.sum()); n_mid = int(mid_m.sum())
|
| 561 |
+
n_quiet = int(quiet_m.sum()); n_clean = int(clean_m.sum())
|
| 562 |
+
_L(st, f' [S4-ADF] 50ms chunks: LOUD={n_loud} MID={n_mid} '
|
| 563 |
+
f'QUIET={n_quiet} CLEAN(skip)={n_clean} nf={noise_floor:.1f}dBFS')
|
| 564 |
+
|
| 565 |
+
# ββ 3 parallel DF passes with per-class flags βββββββββββββββββββββ
|
| 566 |
+
df_out = {}
|
| 567 |
+
def _run_pass(cls, atten, pf=False, pf_beta=0.02):
|
| 568 |
+
od = os.path.join(d, cls); os.makedirs(od, exist_ok=True)
|
| 569 |
+
cmd = [_DF3_CLI_BIN, '--atten-lim-db', str(atten)]
|
| 570 |
+
if pf:
|
| 571 |
+
cmd += ['--pf', '--pf-beta', str(pf_beta)]
|
| 572 |
+
cmd += ['-o', od, fi]
|
| 573 |
+
r2 = subprocess.run(cmd, capture_output=True, timeout=600)
|
| 574 |
+
wp = os.path.join(od, 'in.wav')
|
| 575 |
+
if r2.returncode or not os.path.exists(wp):
|
| 576 |
+
_L(st, f' [S4-ADF] {cls} pass failed β using source')
|
| 577 |
+
return cls, mono.copy()
|
| 578 |
+
arr = _df3_pipe_decode(wp, SR)
|
| 579 |
+
if arr is None:
|
| 580 |
+
_L(st, f' [S4-ADF] {cls} decode failed β using source')
|
| 581 |
+
return cls, mono.copy()
|
| 582 |
+
pf_tag = ' +PF' if pf else ''
|
| 583 |
+
_L(st, f' [S4-ADF] {cls:6s} {atten:2d}dB{pf_tag} β '
|
| 584 |
+
f'max={np.max(np.abs(arr)):.4f}')
|
| 585 |
+
return cls, arr
|
| 586 |
+
|
| 587 |
+
with ThreadPoolExecutor(max_workers=4) as ex:
|
| 588 |
+
futs = [
|
| 589 |
+
ex.submit(_run_pass, 'loud', a_loud, False),
|
| 590 |
+
ex.submit(_run_pass, 'mid', a_mid, False),
|
| 591 |
+
ex.submit(_run_pass, 'quiet', a_quiet, True, 0.04), # PF quiet only
|
| 592 |
+
ex.submit(_run_pass, 'trans', a_trans, False), # v4: transition class
|
| 593 |
+
]
|
| 594 |
for fut in as_completed(futs):
|
| 595 |
+
cls, arr = fut.result()
|
| 596 |
+
df_out[cls] = arr
|
| 597 |
+
|
| 598 |
+
# ββ Expand masks to sample level ββββββββββββββββββββββββββββββββββ
|
| 599 |
+
def _expand(mask):
|
| 600 |
+
full = np.zeros(total, dtype=bool)
|
| 601 |
+
for i, v in enumerate(mask):
|
| 602 |
+
if v:
|
| 603 |
+
s = i * CHUNK
|
| 604 |
+
full[s:min(s + CHUNK, total)] = True
|
| 605 |
+
return full
|
| 606 |
+
|
| 607 |
+
l_s = _expand(loud_m); m_s = _expand(mid_m)
|
| 608 |
+
q_s = _expand(quiet_m); c_s = _expand(clean_m)
|
| 609 |
+
|
| 610 |
+
min_n = min(total, *(len(df_out[c]) for c in ('loud', 'mid', 'quiet')))
|
| 611 |
+
blended = np.zeros(min_n, dtype=np.float64)
|
| 612 |
+
blended[l_s[:min_n]] = df_out['loud'][:min_n][l_s[:min_n]]
|
| 613 |
+
blended[m_s[:min_n]] = df_out['mid'][:min_n][m_s[:min_n]]
|
| 614 |
+
blended[q_s[:min_n]] = df_out['quiet'][:min_n][q_s[:min_n]]
|
| 615 |
+
# Β§22.5 clean gate: restore original on already-clean chunks
|
| 616 |
+
blended[c_s[:min_n]] = mono[:min_n][c_s[:min_n]]
|
| 617 |
+
|
| 618 |
+
# ββ 120ms sigmoid crossfade at every class boundary (v4: was cosine 40ms) ββ
|
| 619 |
+
# Sigmoid gives perceptually flatter transition β no "click" at midpoint
|
| 620 |
+
cls_map = (l_s[:min_n].astype(np.int8) * 1 +
|
| 621 |
+
m_s[:min_n].astype(np.int8) * 2 +
|
| 622 |
+
q_s[:min_n].astype(np.int8) * 3)
|
| 623 |
+
bounds = np.where(np.diff(cls_map) != 0)[0]
|
| 624 |
+
FADE = _ADF_XFADE
|
| 625 |
+
for b in bounds:
|
| 626 |
+
s_cf = max(0, b - FADE // 2)
|
| 627 |
+
e_cf = min(min_n, b + FADE // 2)
|
| 628 |
+
if e_cf <= s_cf: continue
|
| 629 |
+
ln_ = e_cf - s_cf
|
| 630 |
+
# Sigmoid: x from -6..+6 β smooth S-curve
|
| 631 |
+
x = np.linspace(-6.0, 6.0, ln_)
|
| 632 |
+
fi_ = 1.0 / (1.0 + np.exp(-x)) # sigmoid fade-in
|
| 633 |
+
fo = 1.0 - fi_ # sigmoid fade-out
|
| 634 |
+
cb = int(cls_map[s_cf])
|
| 635 |
+
ca = int(cls_map[min(e_cf, min_n - 1)])
|
| 636 |
+
cb_n = 'loud' if cb==1 else ('mid' if cb==2 else 'quiet')
|
| 637 |
+
ca_n = 'loud' if ca==1 else ('mid' if ca==2 else 'quiet')
|
| 638 |
+
# v4: blend through 'trans' class at midpoint for 3-way smooth
|
| 639 |
+
mid_blend = (
|
| 640 |
+
df_out[cb_n][:min_n][s_cf:e_cf].astype(np.float64) * fo * 0.5 +
|
| 641 |
+
df_out['trans'][:min_n][s_cf:e_cf].astype(np.float64) * 0.5 +
|
| 642 |
+
df_out[ca_n][:min_n][s_cf:e_cf].astype(np.float64) * fi_ * 0.5
|
| 643 |
+
)
|
| 644 |
+
blended[s_cf:e_cf] = mid_blend
|
| 645 |
+
_L(st, f' [S4-ADF] {len(bounds)} sigmoid crossfades @ {FADE} samp (120ms)')
|
| 646 |
+
|
| 647 |
+
# ββ RL-16: restore Ghunnah guard bands from original ββββββββββββββ
|
| 648 |
+
blended_f32 = np.where(np.isfinite(blended), blended, 0.0).astype(np.float32)
|
| 649 |
+
blended_f32 = _stft_restore_bands(blended_f32, mono[:min_n], SR, _RL16_BANDS)
|
| 650 |
+
_L(st, f' [S4-ADF] RL-16 bands restored: '
|
| 651 |
+
f'{", ".join(f"{lo}-{hi}Hz" for lo,hi in _RL16_BANDS)}')
|
| 652 |
+
|
| 653 |
+
# ββ Silence guard βββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 654 |
+
if float(np.max(np.abs(blended_f32))) < 1e-4:
|
| 655 |
+
_L(st, ' [S4-ADF] β silent result β fallback to mid pass')
|
| 656 |
+
blended_f32 = df_out['mid'][:min_n].astype(np.float32)
|
| 657 |
+
|
| 658 |
+
# ββ Encode to pcm_s24le stereo ββββββββββββββββββββββββββββββββββββ
|
| 659 |
out = _tmp('s4', st)
|
| 660 |
+
r3 = subprocess.run(
|
| 661 |
+
['ffmpeg', '-nostdin', '-y', '-hide_banner', '-loglevel', 'error',
|
| 662 |
+
'-f', 'f32le', '-ar', str(SR), '-ac', '1', '-i', '-',
|
| 663 |
+
'-ar', str(SR), '-ac', '2', '-acodec', 'pcm_s24le', out],
|
| 664 |
+
input=blended_f32.tobytes(), capture_output=True, timeout=120)
|
| 665 |
+
if r3.returncode or not os.path.exists(out):
|
| 666 |
return wav
|
| 667 |
|
| 668 |
+
delta = _rmsdb(blended_f32) - _rmsdb(mono)
|
|
|
|
|
|
|
|
|
|
| 669 |
st.df3 = True
|
| 670 |
+
_L(st, f' [S4-ADF] β loud={a_loud}dB mid={a_mid}dB quiet={a_quiet}dB '
|
| 671 |
+
f'RMS Ξ={delta:+.2f}dB')
|
|
|
|
| 672 |
return out
|
| 673 |
except Exception as e:
|
| 674 |
_L(st, f' [S4-DF3] exception: {e}'); return wav
|
|
|
|
| 709 |
if not nf_vals:
|
| 710 |
_L(st, ' [S5-JALAA] no reverb frames β skip'); return wav
|
| 711 |
|
| 712 |
+
nf = float(np.clip(float(np.median(nf_vals)) + 3, -72, -20)) # v4: floor raised -25β-20
|
| 713 |
+
nr = 3 if rt60 > RT60_AGGR else 2 # v4: base raised (was 2/1)
|
| 714 |
if st.mujawwad_conf > 0.6: nr = max(1, nr - 1)
|
| 715 |
|
| 716 |
+
# v4: two-pass JALAA β first pass stationary, second adaptive tracking
|
| 717 |
+
out = _tmp('s5a', st)
|
| 718 |
rc, _, _ = _run(['ffmpeg', '-y', '-i', wav,
|
| 719 |
+
'-af', f'afftdn=nr={nr}:nf={nf:.0f}:nt=w:tn=1',
|
| 720 |
'-acodec', WAV_CODEC, '-ar', str(SR), '-ac', '1',
|
| 721 |
'-loglevel', 'error', out])
|
| 722 |
if rc or not os.path.exists(out):
|
| 723 |
return wav
|
| 724 |
|
| 725 |
+
out2 = _tmp('s5b', st)
|
| 726 |
+
rc2, _, _ = _run(['ffmpeg', '-y', '-i', out,
|
| 727 |
+
'-af', f'afftdn=nr={max(1,nr-1)}:nf={nf+3:.0f}:nt=w:tn=1',
|
| 728 |
+
'-acodec', WAV_CODEC, '-ar', str(SR), '-ac', '1',
|
| 729 |
+
'-loglevel', 'error', out2])
|
| 730 |
+
if rc2 or not os.path.exists(out2):
|
| 731 |
+
pass # keep single-pass result
|
| 732 |
+
else:
|
| 733 |
+
_cleanup(out); out = out2
|
| 734 |
+
|
| 735 |
post = _decode(out)
|
| 736 |
if post is not None:
|
| 737 |
d = _rmsdb(post) - _rmsdb(samples)
|
|
|
|
| 744 |
return out
|
| 745 |
|
| 746 |
|
| 747 |
+
# βββ Stage 6: Tail floor NR β 2-stage (afftdn + anlmdn) βββββββββββββββββββββ
|
| 748 |
def _tailnr(wav, samples, rt60, st):
|
| 749 |
"""
|
| 750 |
+
2-stage NR pipeline (Β§83.3 order: stationary β non-stationary):
|
| 751 |
+
Stage A: afftdn with nt=w (adaptive tracking) for stationary noise floor.
|
| 752 |
+
Stage B: anlmdn (s=7:p=3:r=15) for transient/non-stationary residuals.
|
| 753 |
+
RT60-scaled nr: base=2, +1 per 1.5s RT60 above threshold, cap=5 (Β§5.3: 20dB/nr max).
|
| 754 |
[I3] If JALAA already ran, reduce nr by 1 to avoid double-attenuation.
|
| 755 |
+
RL-16: sibilant SNR guard β revert if Safir/Tafasshi band drops > 2dB.
|
| 756 |
"""
|
| 757 |
if rt60 < RT60_TAILNR or samples is None or not NUMPY_OK:
|
| 758 |
return wav
|
| 759 |
fn = int(0.200 * SR)
|
| 760 |
overall = _rmsdb(samples)
|
| 761 |
+
fdb = np.array([float(20 * np.log10(np.sqrt(np.mean(samples[i:i+fn]**2)) + 1e-10))
|
| 762 |
for i in range(0, len(samples) - fn, fn)])
|
| 763 |
quiet = fdb[fdb < overall - 10]
|
| 764 |
if len(quiet) == 0:
|
| 765 |
return wav
|
| 766 |
+
|
| 767 |
+
nf = float(np.clip(float(np.median(quiet)) + 4, -72, -25))
|
| 768 |
+
# v4: RT60-scaled nr β higher cap (8 was 5), stronger base
|
| 769 |
+
nr = min(8, 3 + int((rt60 - RT60_TAILNR) / 1.2)) # was cap=5, base=2, step=1.5
|
| 770 |
if st.mujawwad_conf > 0.6: nr = max(1, nr - 1)
|
| 771 |
+
if st.jalaa: nr = max(1, nr - 1) # [I3]
|
| 772 |
+
|
| 773 |
+
# ββ Stage A: afftdn with adaptive noise tracking ββββββββββββββββββββββ
|
| 774 |
+
out_a = _tmp('s6a', st)
|
| 775 |
rc, _, _ = _run(['ffmpeg', '-y', '-i', wav,
|
| 776 |
+
'-af', f'afftdn=nr={nr}:nf={nf:.0f}:nt=w:om=o',
|
| 777 |
+
'-acodec', WAV_CODEC, '-ar', str(SR), '-ac', '2',
|
| 778 |
+
'-loglevel', 'error', out_a])
|
| 779 |
+
if rc or not os.path.exists(out_a):
|
| 780 |
return wav
|
| 781 |
+
|
| 782 |
+
# ββ Stage B: anlmdn for non-stationary residuals ββββββββββββββββββββββ
|
| 783 |
+
# anlmdn: s=patch size, p=context, r=search radius (Β§95 KB)
|
| 784 |
+
# Scale strength with nr: gentle (s=5) at nr<=2, standard (s=7) at nr>2
|
| 785 |
+
patch = 7 if nr > 2 else 5
|
| 786 |
+
out_b = _tmp('s6b', st)
|
| 787 |
+
rc2, _, _ = _run(['ffmpeg', '-y', '-i', out_a,
|
| 788 |
+
'-af', f'anlmdn=s={patch}:p=3:r=15:m=1',
|
| 789 |
+
'-acodec', WAV_CODEC, '-ar', str(SR), '-ac', '2',
|
| 790 |
+
'-loglevel', 'error', out_b])
|
| 791 |
+
if rc2 or not os.path.exists(out_b):
|
| 792 |
+
# Stage B failed β commit Stage A only
|
| 793 |
+
st.tail_nr = True
|
| 794 |
+
jalaa_note = ' (JALAA-adj)' if st.jalaa else ''
|
| 795 |
+
_L(st, f' [S6-tailNR] β 1-stage nr={nr}{jalaa_note} nf={nf:.0f}dB '
|
| 796 |
+
f'[anlmdn failed β Stage A only]')
|
| 797 |
+
return out_a
|
| 798 |
+
|
| 799 |
+
# ββ Sibilant SNR guard (RL-16 spirit) βββββββββββββββββββββββββββββββββ
|
| 800 |
+
post_s = _decode(out_b)
|
| 801 |
+
if post_s is not None:
|
| 802 |
+
sib_orig = _band_energy(samples, 3500, 8000)
|
| 803 |
+
sib_post = _band_energy(post_s, 3500, 8000)
|
| 804 |
+
sib_drop = sib_post - sib_orig
|
| 805 |
+
if sib_drop < -2.0:
|
| 806 |
+
_L(st, f' [S6-tailNR] sibilant drop {sib_drop:+.1f}dB β '
|
| 807 |
+
f'revert Stage B, keep Stage A')
|
| 808 |
+
_cleanup(out_b)
|
| 809 |
+
st.tail_nr = True
|
| 810 |
+
_L(st, f' [S6-tailNR] β nr={nr} nf={nf:.0f}dB [Stage A only]')
|
| 811 |
+
return out_a
|
| 812 |
+
|
| 813 |
+
# ββ Stage C: v4 β gentle anlmdn second pass for reverb tail residual ββββ
|
| 814 |
+
out_c = _tmp('s6c', st)
|
| 815 |
+
rc3, _, _ = _run(['ffmpeg', '-y', '-i', out_b,
|
| 816 |
+
'-af', f'anlmdn=s=5:p=3:r=10:m=1',
|
| 817 |
+
'-acodec', WAV_CODEC, '-ar', str(SR), '-ac', '2',
|
| 818 |
+
'-loglevel', 'error', out_c])
|
| 819 |
+
if rc3 or not os.path.exists(out_c):
|
| 820 |
+
pass # keep Stage B
|
| 821 |
+
else:
|
| 822 |
+
post_c = _decode(out_c)
|
| 823 |
+
if post_c is not None:
|
| 824 |
+
sib_c = _band_energy(post_c, 3500, 8000)
|
| 825 |
+
sib_drop_c = sib_c - _band_energy(samples, 3500, 8000)
|
| 826 |
+
if sib_drop_c >= -2.5:
|
| 827 |
+
_cleanup(out_b); out_b = out_c
|
| 828 |
+
else:
|
| 829 |
+
_cleanup(out_c)
|
| 830 |
+
|
| 831 |
+
_cleanup(out_a)
|
| 832 |
st.tail_nr = True
|
| 833 |
+
jalaa_note = ' (JALAA-adj)' if st.jalaa else ''
|
| 834 |
+
_L(st, f' [S6-tailNR] β 3-stage nr={nr}{jalaa_note} nf={nf:.0f}dB '
|
| 835 |
+
f'patch={patch} rt60={rt60:.1f}s')
|
| 836 |
+
return out_b
|
| 837 |
|
| 838 |
|
| 839 |
# βββ Stage 7: Arabic phoneme guards (Β§35/Β§52/Β§143/Β§152) ββββββββββββββββββββββ
|
|
|
|
| 990 |
# S7 Arabic guards
|
| 991 |
cur = _arabic_guards(s_orig, cur, st)
|
| 992 |
|
| 993 |
+
# Final stereo encode + S8 volume boost + v4: presence lift 2kHz +1.5dB
|
| 994 |
rc, _, err = _run(['ffmpeg', '-y', '-i', cur,
|
| 995 |
+
'-af', ('volume=1.85,'
|
| 996 |
+
'equalizer=f=2000:width_type=o:width=1.0:g=1.5,'
|
| 997 |
+
'equalizer=f=300:width_type=o:width=1.0:g=-1.0,'
|
| 998 |
+
'alimiter=level_in=1:level_out=1:limit=0.89:attack=5:release=50'),
|
| 999 |
+
'-acodec', WAV_CODEC, '-ar', str(SR), '-ac', '2',
|
| 1000 |
'-loglevel', 'error', output_path])
|
| 1001 |
if rc:
|
| 1002 |
+
_L(st, f' ERROR: final encode: {err[:80]}'); return st
|
|
|
|
| 1003 |
|
| 1004 |
sf_ = _decode(output_path)
|
| 1005 |
if sf_ is not None: st.drr_after = _drr(sf_)
|
|
|
|
| 1022 |
if __name__ == '__main__':
|
| 1023 |
import argparse, json
|
| 1024 |
ap = argparse.ArgumentParser(description=f'Ψ§ΩΨ΅ΩΨ§Ψ‘ {__version__} β Dereverberation Engine')
|
| 1025 |
+
ap.add_argument('input')
|
| 1026 |
+
ap.add_argument('output')
|
| 1027 |
+
ap.add_argument('--tier', default='TIER_UNKNOWN')
|
| 1028 |
+
ap.add_argument('--mujawwad', type=float, default=0.0)
|
| 1029 |
+
ap.add_argument('--rt60', type=float, default=0.0, help='Force RT60 (0=auto)')
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1030 |
a = ap.parse_args()
|
| 1031 |
st = process(a.input, a.output,
|
| 1032 |
source_tier=a.tier, mujawwad_conf=a.mujawwad, force_rt60=a.rt60)
|
requirements.txt
CHANGED
|
@@ -5,3 +5,4 @@ scipy==1.13.1
|
|
| 5 |
gunicorn==22.0.0
|
| 6 |
librosa
|
| 7 |
soundfile
|
|
|
|
|
|
| 5 |
gunicorn==22.0.0
|
| 6 |
librosa
|
| 7 |
soundfile
|
| 8 |
+
nara_wpe
|