File size: 4,898 Bytes
f88697b
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
cee1be8
 
f88697b
cee1be8
f88697b
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
cee1be8
f88697b
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
"""
apbwe_wrapper.py β€” thin adapter between engine_ihya_v4.py's cascade and the
vendored AP-BWE model (apbwe_vendor/), replacing the dead SIDON slot.

AP-BWE (yxlu-0102/AP-BWE, MIT license): an amplitude+phase prediction
bandwidth-extension model. All-CNN (ConvNeXt blocks), no vocoder/diffusion
stage β€” it predicts wideband magnitude+phase spectra directly from the
narrowband STFT, then ISTFTs back to waveform. No internal language/text
model, nothing resembling word-level generation; the failure mode here is
spectral over/under-smoothing, not word substitution β€” much closer to the
masking-model risk profile than a full resynthesis pipeline.

CONFIG: expects checkpoints/<bucket>/{config.json, g_*} to exist, where
<bucket> matches the bandwidth pair the checkpoint was trained for (e.g.
"8kto48k" β€” Nyquist ~4kHz input, the right bucket for telephone-tier
audio). Checkpoints are NOT in the AP-BWE git repo (only a README pointing
at Google Drive) β€” download the matching folder yourself and place it at
apbwe_vendor/checkpoints/<bucket>/ before deploying.

This module never raises out to its caller β€” every failure path returns
False so the cascade (SIDON skip β†’ here β†’ VoiceFixer β†’ DSP fallback)
degrades gracefully instead of crashing a job.
"""
import os
import glob
import json

try:
    import torch
    import torchaudio
    import torchaudio.functional as aF
    import soundfile as sf
    from apbwe_vendor.env import AttrDict
    from apbwe_vendor.datasets.dataset import amp_pha_stft, amp_pha_istft
    from apbwe_vendor.models.model import APNet_BWE_Model
    APBWE_DEPS_OK = True
    _DEPS_ERROR = None
except Exception as _e:
    APBWE_DEPS_OK = False
    _DEPS_ERROR = f"{type(_e).__name__}: {_e}"

_CHECKPOINT_BUCKET = os.environ.get("APBWE_CHECKPOINT_BUCKET", "8kto48k")
_BASE = os.path.dirname(os.path.abspath(__file__))
_CHECKPOINT_DIR = os.path.join(_BASE, "apbwe_vendor", "checkpoints", _CHECKPOINT_BUCKET)

_model = None
_h = None
_device = None
_load_error = None


def _load():
    """Lazy singleton load β€” first call pays the cost, every job after
    reuses the in-memory model. Records _load_error once so repeated calls
    don't re-attempt a load that's already known to fail."""
    global _model, _h, _device, _load_error
    if _model is not None or _load_error is not None:
        return

    if not APBWE_DEPS_OK:
        _load_error = f"imports failed β€” {_DEPS_ERROR}"
        return

    config_file = os.path.join(_CHECKPOINT_DIR, "config.json")
    if not os.path.isfile(config_file):
        _load_error = f"no config.json at {_CHECKPOINT_DIR} β€” checkpoint not deployed"
        return

    ckpt_candidates = sorted(glob.glob(os.path.join(_CHECKPOINT_DIR, "g_*")))
    if not ckpt_candidates:
        _load_error = f"no checkpoint (g_*) in {_CHECKPOINT_DIR}"
        return

    try:
        with open(config_file) as f:
            _h = AttrDict(json.loads(f.read()))
        _device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
        _model = APNet_BWE_Model(_h).to(_device)
        state = torch.load(ckpt_candidates[-1], map_location=_device)
        _model.load_state_dict(state["generator"])
        _model.eval()
    except Exception as e:
        _model = None
        _load_error = f"checkpoint/model load failed: {e}"


def restore(wav_in: str, wav_out: str) -> bool:
    """Bandwidth-extend wav_in -> wav_out using AP-BWE. Returns True only
    if wav_out was actually written with content; False on any failure
    (including 'not deployed yet') so the caller's cascade falls through
    to the next stage rather than treating a missing checkpoint as a crash."""
    _load()
    if _model is None:
        if _load_error:
            print(f"[apbwe_wrapper] unavailable: {_load_error}")
        return False

    try:
        audio, orig_sr = torchaudio.load(wav_in)
        audio = audio.to(_device)
        if audio.shape[0] > 1:
            audio = audio.mean(dim=0, keepdim=True)  # AP-BWE is mono-only

        audio_hr = aF.resample(audio, orig_freq=orig_sr, new_freq=_h.hr_sampling_rate)
        audio_lr = aF.resample(audio, orig_freq=orig_sr, new_freq=_h.lr_sampling_rate)
        audio_lr = aF.resample(audio_lr, orig_freq=_h.lr_sampling_rate, new_freq=_h.hr_sampling_rate)
        audio_lr = audio_lr[:, : audio_hr.size(1)]

        with torch.no_grad():
            amp_nb, pha_nb, _com_nb = amp_pha_stft(audio_lr, _h.n_fft, _h.hop_size, _h.win_size)
            amp_wb_g, pha_wb_g, _com_wb_g = _model(amp_nb, pha_nb)
            audio_hr_g = amp_pha_istft(amp_wb_g, pha_wb_g, _h.n_fft, _h.hop_size, _h.win_size)

        sf.write(wav_out, audio_hr_g.squeeze().cpu().numpy(), _h.hr_sampling_rate, "PCM_16")
        return os.path.exists(wav_out) and os.path.getsize(wav_out) > 0
    except Exception as e:
        print(f"[apbwe_wrapper] restore failed: {e}")
        return False