Skip to content

Commit ae5c4cc

Browse files
authored
Remove hard torchaudio dependency for inference; add kaldi-native-fbank fbank backend (#3526)
* Remove hard torchaudio dependency for inference; add kaldi-native-fbank fbank backend Make torchaudio an optional dependency for the inference path so FunASR can run on Ascend NPU (aarch64) servers, where no matching torchaudio wheel exists (Huawei's Ascend torch is version-decoupled from the PyTorch upstream release line). - Add funasr/utils/fbank.py: an fbank shim that delegates to torchaudio.compliance.kaldi when torchaudio is installed, otherwise falls back to kaldi-native-fbank (a torch-free C++ library with aarch64 wheels). Options and frame-shift API are mapped to torchaudio semantics so outputs are equivalent. - Switch funasr/frontends/wav_frontend.py, funasr/utils/speaker_utils.py and funasr/models/campplus/utils.py to the shim. - Make the torchaudio import optional in funasr/utils/load_utils.py (resampler falls back to librosa). No device-specific branching is introduced — a pure backend fallback that also benefits CPU-only and other accelerator platforms. Verified end-to-end on Ascend 910B (npu:0) with torchaudio absent: paraformer-large loads and infers correctly, RTF ~0.016 (~60x realtime). * Make torchaudio import optional in paraformer_v2, fun_asr_nano and dataset preprocessors Follow-up to the inference-path change: make the remaining top-level torchaudio imports optional so these modules import cleanly when torchaudio is not installed (e.g. on Ascend NPU servers). - funasr/models/paraformer_v2_community/model.py - funasr/models/fun_asr_nano/tools/utils.py - funasr/datasets/audio_datasets/preprocessor.py - funasr/datasets/llm_datasets/preprocessor.py Functions that still require torchaudio (forced_align, Resample) degrade to a clear error only when called, rather than a hard import failure. * Address review: declare knf extra, harden fbank fallback and torchaudio guards Respond to maintainer review feedback. - Declare kaldi-native-fbank as an optional 'knf' extra in setup.py; the fbank module now raises an actionable ImportError only when fbank() is actually called with neither torchaudio nor kaldi-native-fbank installed (previously it failed at import time with a bare ModuleNotFoundError). - Harden the fbank fallback contract: handle the channel argument explicitly and reject torchaudio-only options (subtract_mean, min_duration, VTLN warping, non-default blackman_coeff) with NotImplementedError instead of silently dropping them via **kwargs. - Add a shared guard funasr/utils/torchaudio_compat.py and use it for resampling, forced alignment and speed perturbation so a missing torchaudio fails loudly instead of returning a silently wrong result (empty alignment). - Add regression tests (tests/test_torchaudio_optional.py) that import the affected modules with torchaudio absent.
1 parent b291502 commit ae5c4cc

12 files changed

Lines changed: 440 additions & 20 deletions

File tree

‎funasr/datasets/audio_datasets/preprocessor.py‎

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,12 +7,16 @@
77
import torch.distributed as dist
88
from typing import Collection
99
import torch
10-
import torchaudio
10+
try:
11+
import torchaudio
12+
except ImportError:
13+
torchaudio = None
1114
from torch import nn
1215
import random
1316
import re
1417
from funasr.tokenizer.cleaner import TextCleaner
1518
from funasr.register import tables
19+
from funasr.utils.torchaudio_compat import require_torchaudio
1620

1721

1822
@tables.register("preprocessor_classes", "SpeechPreprocessSpeedPerturb")
@@ -39,6 +43,7 @@ def forward(self, waveform, fs, **kwargs):
3943
return waveform
4044
speed = random.choice(self.speed_perturb)
4145
if speed != 1.0:
46+
torchaudio = require_torchaudio("speed perturbation")
4247
if not isinstance(waveform, torch.Tensor):
4348
waveform = torch.tensor(waveform)
4449
waveform, _ = torchaudio.sox_effects.apply_effects_tensor(
@@ -79,4 +84,4 @@ def forward(self, text, **kwargs):
7984
"""
8085
text = self.text_cleaner(text)
8186

82-
return text
87+
return text

‎funasr/datasets/llm_datasets/preprocessor.py‎

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,10 @@
77
import torch.distributed as dist
88
from typing import Collection
99
import torch
10-
import torchaudio
10+
try:
11+
import torchaudio
12+
except ImportError:
13+
torchaudio = None
1114
from torch import nn
1215
import random
1316
import re

‎funasr/frontends/wav_frontend.py‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
import numpy as np
66
import torch
77
import torch.nn as nn
8-
import torchaudio.compliance.kaldi as kaldi
8+
from funasr.utils import fbank as kaldi
99
from torch.nn.utils.rnn import pad_sequence
1010

1111
import funasr.frontends.eend_ola_feature as eend_ola_feature

‎funasr/models/campplus/utils.py‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@
1616
from pathlib import Path
1717
from typing import Generator, Union
1818
from abc import ABCMeta, abstractmethod
19-
import torchaudio.compliance.kaldi as Kaldi
19+
from funasr.utils import fbank as Kaldi
2020

2121
from funasr.models.transformer.utils.nets_utils import pad_list
2222

‎funasr/models/fun_asr_nano/tools/utils.py‎

Lines changed: 17 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,15 @@
11
from itertools import groupby
22

3+
import librosa
34
import soundfile as sf
45
import torch
5-
import torchaudio
6-
import torchaudio.functional as F
6+
7+
from funasr.utils.torchaudio_compat import require_torchaudio, torchaudio_available
78

89

910
def load_audio(wav_path, rate: int = None, offset: float = 0, duration: float = None):
1011
"""Load audio.
11-
12+
1213
Args:
1314
wav_path: TODO.
1415
rate: TODO.
@@ -29,27 +30,34 @@ def load_audio(wav_path, rate: int = None, offset: float = 0, duration: float =
2930
audio_tensor = audio_tensor.unsqueeze(0)
3031
else:
3132
audio_tensor = audio_tensor.T
32-
resampler = torchaudio.transforms.Resample(orig_freq=f.samplerate, new_freq=rate)
33-
audio_tensor = resampler(audio_tensor)
33+
if torchaudio_available():
34+
torchaudio = require_torchaudio("resampling")
35+
resampler = torchaudio.transforms.Resample(orig_freq=f.samplerate, new_freq=rate)
36+
audio_tensor = resampler(audio_tensor)
37+
else:
38+
audio_tensor = torch.from_numpy(
39+
librosa.resample(audio_tensor.numpy(), orig_sr=f.samplerate, target_sr=rate)
40+
)
3441
if audio_tensor.shape[0] == 1:
3542
audio_tensor = audio_tensor.squeeze(0)
3643
return audio_tensor, rate if rate is not None else f.samplerate
3744

3845

3946
def forced_align(log_probs: torch.Tensor, targets: torch.Tensor, blank: int = 0):
4047
"""Forced align.
41-
48+
4249
Args:
4350
log_probs: TODO.
4451
targets: TODO.
4552
blank: TODO.
4653
"""
54+
torchaudio = require_torchaudio("forced_align")
4755
items = []
4856
try:
4957
# The current version only supports batch_size==1.
5058
log_probs, targets = log_probs.unsqueeze(0).cpu(), targets.unsqueeze(0).cpu()
5159
assert log_probs.shape[1] >= targets.shape[1]
52-
alignments, scores = F.forced_align(log_probs, targets, blank=blank)
60+
alignments, scores = torchaudio.functional.forced_align(log_probs, targets, blank=blank)
5361
alignments, scores = alignments[0], torch.exp(scores[0]).tolist()
5462
# use enumerate to keep track of the original indices, then group by token value
5563
for token, group in groupby(enumerate(alignments), key=lambda item: item[1]):
@@ -67,6 +75,6 @@ def forced_align(log_probs: torch.Tensor, targets: torch.Tensor, blank: int = 0)
6775
"score": round(score, 3),
6876
}
6977
)
70-
except:
78+
except Exception:
7179
pass
72-
return items
80+
return items

‎funasr/models/paraformer_v2_community/model.py‎

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,8 +24,12 @@
2424
from funasr.models.transformer.utils.nets_utils import make_pad_mask
2525
from funasr.utils.timestamp_tools import ts_prediction_lfr6_standard
2626
from funasr.utils.load_utils import load_audio_text_image_video, extract_fbank
27+
from funasr.utils.torchaudio_compat import require_torchaudio
2728
from torch.nn.utils.rnn import pad_sequence
28-
import torchaudio
29+
try:
30+
import torchaudio
31+
except ImportError:
32+
torchaudio = None
2933

3034
@tables.register("model_classes", "Paraformer_v2_community")
3135
class Paraformer(torch.nn.Module):
@@ -406,6 +410,7 @@ def force_align(self, ctc_probs: torch.Tensor, y: torch.Tensor, blank_id=0) -> l
406410
Returns:
407411
torch.Tensor: alignment result
408412
"""
413+
torchaudio = require_torchaudio("forced alignment")
409414
ctc_probs = ctc_probs[None].cpu()
410415
y = y[None].cpu()
411416
alignments, _ = torchaudio.functional.forced_align(ctc_probs, y, blank=blank_id)

‎funasr/utils/fbank.py‎

Lines changed: 166 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,166 @@
1+
# Copyright (c) community contributors.
2+
# A minimal fbank backend that lets FunASR run without torchaudio.
3+
#
4+
# torchaudio has no wheel matching the torch build on Ascend NPU (aarch64)
5+
# servers, where torch is a Huawei-forked nightly (e.g. 2.14.0a0) whose version
6+
# number is out of step with upstream PyTorch releases. This module forwards to
7+
# torchaudio.compliance.kaldi.fbank when torchaudio is present and otherwise
8+
# falls back to kaldi-native-fbank -- a standalone C++ Kaldi fbank
9+
# implementation with no torch dependency that ships manylinux aarch64 wheels.
10+
#
11+
# torchaudio.compliance.kaldi.fbank is only used on the CPU for feature
12+
# extraction. The fallback mirrors its signature so call sites only need to
13+
# change a single import line:
14+
#
15+
# from funasr.utils import fbank as kaldi
16+
#
17+
# Not a full drop-in: the fallback supports single-channel waveforms and the
18+
# FbankOptions exposed by kaldi-native-fbank. torchaudio-only options such as
19+
# ``subtract_mean``, ``min_duration``, VTLN warping or an explicit
20+
# ``blackman_coeff`` are rejected with a clear error rather than silently
21+
# ignored, and multi-channel waveforms without an explicit ``channel`` are
22+
# rejected (kaldi-native-fbank is single-channel).
23+
import torch
24+
import numpy as np
25+
26+
try:
27+
import torchaudio.compliance.kaldi as _torchaudio_kaldi
28+
29+
_HAS_TORCHAUDIO = True
30+
except Exception: # noqa: BLE001 -- torchaudio is optional here
31+
_HAS_TORCHAUDIO = False
32+
33+
_knf = None
34+
if not _HAS_TORCHAUDIO:
35+
try:
36+
import kaldi_native_fbank as _knf
37+
except Exception: # noqa: BLE001 -- reported lazily on first use
38+
_knf = None
39+
40+
41+
def _knf_missing_error():
42+
return ImportError(
43+
"torchaudio is not installed and neither is the kaldi-native-fbank "
44+
"fallback backend. FunASR needs one fbank backend for feature "
45+
"extraction. Install either torchaudio (matching your torch version) "
46+
"or kaldi-native-fbank, e.g. `pip install kaldi-native-fbank` (or "
47+
"`pip install funasr[knf]`)."
48+
)
49+
50+
51+
def _fbank_knf(
52+
waveform: torch.Tensor,
53+
blackman_coeff: float = 0.42,
54+
channel: int = -1,
55+
dither: float = 0.0,
56+
energy_floor: float = 1.0,
57+
frame_length: float = 25.0,
58+
frame_shift: float = 10.0,
59+
high_freq: float = 0.0,
60+
htk_compat: bool = False,
61+
low_freq: float = 20.0,
62+
min_duration: float = 0.0,
63+
num_mel_bins: int = 23,
64+
preemphasis_coefficient: float = 0.97,
65+
raw_energy: bool = True,
66+
remove_dc_offset: bool = True,
67+
round_to_power_of_two: bool = True,
68+
sample_frequency: float = 16000.0,
69+
snip_edges: bool = True,
70+
subtract_mean: bool = False,
71+
use_energy: bool = False,
72+
use_log_fbank: bool = True,
73+
use_power: bool = True,
74+
vtln_high: float = -500.0,
75+
vtln_low: float = 100.0,
76+
vtln_warp: float = 1.0,
77+
window_type: str = "povey",
78+
) -> torch.Tensor:
79+
"""kaldi-native-fbank implementation of ``torchaudio.compliance.kaldi.fbank``."""
80+
if _knf is None:
81+
raise _knf_missing_error()
82+
83+
# torchaudio options the kaldi-native-fbank fallback does not implement.
84+
# Reject them explicitly instead of silently dropping them.
85+
unsupported = {}
86+
if blackman_coeff != 0.42:
87+
unsupported["blackman_coeff"] = blackman_coeff
88+
if min_duration != 0.0:
89+
unsupported["min_duration"] = min_duration
90+
if subtract_mean:
91+
unsupported["subtract_mean"] = subtract_mean
92+
if vtln_warp != 1.0:
93+
unsupported["vtln_warp"] = vtln_warp
94+
if vtln_low != 100.0:
95+
unsupported["vtln_low"] = vtln_low
96+
if vtln_high != -500.0:
97+
unsupported["vtln_high"] = vtln_high
98+
if unsupported:
99+
raise NotImplementedError(
100+
"The kaldi-native-fbank fallback does not support these "
101+
f"torchaudio fbank options: {sorted(unsupported)}. Install "
102+
"torchaudio to use them, or drop these options."
103+
)
104+
105+
# Channel / waveform-shape handling, mirroring torchaudio semantics for the
106+
# single-channel case kaldi-native-fbank supports.
107+
if waveform.dim() == 2:
108+
if channel >= 0:
109+
waveform = waveform[channel]
110+
elif waveform.size(0) == 1:
111+
waveform = waveform[0]
112+
else:
113+
raise NotImplementedError(
114+
"The kaldi-native-fbank fallback extracts features from a "
115+
"single channel only. Pass `channel` to select one channel, or "
116+
"reduce the waveform to mono before calling fbank()."
117+
)
118+
elif waveform.dim() == 1:
119+
if channel > 0:
120+
raise ValueError(
121+
f"Invalid channel {channel} for a 1-D waveform; use channel=-1 or 0."
122+
)
123+
else:
124+
raise ValueError("waveform must be a 1-D or 2-D tensor")
125+
126+
opts = _knf.FbankOptions()
127+
opts.frame_opts.samp_freq = float(sample_frequency)
128+
opts.frame_opts.frame_length_ms = float(frame_length)
129+
opts.frame_opts.frame_shift_ms = float(frame_shift)
130+
opts.frame_opts.dither = float(dither)
131+
opts.frame_opts.snip_edges = bool(snip_edges)
132+
opts.frame_opts.window_type = str(window_type)
133+
opts.frame_opts.preemph_coeff = float(preemphasis_coefficient)
134+
opts.frame_opts.remove_dc_offset = bool(remove_dc_offset)
135+
opts.frame_opts.round_to_power_of_two = bool(round_to_power_of_two)
136+
opts.mel_opts.num_bins = int(num_mel_bins)
137+
opts.mel_opts.low_freq = float(low_freq)
138+
opts.mel_opts.high_freq = float(high_freq)
139+
opts.energy_floor = float(energy_floor)
140+
opts.raw_energy = bool(raw_energy)
141+
opts.use_energy = bool(use_energy)
142+
opts.use_log_fbank = bool(use_log_fbank)
143+
opts.use_power = bool(use_power)
144+
opts.htk_compat = bool(htk_compat)
145+
146+
fb = _knf.OnlineFbank(opts)
147+
wav = waveform.detach().cpu().numpy().reshape(-1).astype(np.float32)
148+
fb.accept_waveform(int(sample_frequency), wav)
149+
fb.input_finished()
150+
n = fb.num_frames_ready
151+
if n == 0:
152+
return torch.zeros((0, int(num_mel_bins)), dtype=torch.float32)
153+
frames = np.stack([fb.get_frame(i) for i in range(n)])
154+
return torch.from_numpy(frames)
155+
156+
157+
def fbank(waveform: torch.Tensor, **kwargs) -> torch.Tensor:
158+
"""Drop-in replacement for :func:`torchaudio.compliance.kaldi.fbank`.
159+
160+
Forwards to torchaudio when it is installed; otherwise falls back to
161+
kaldi-native-fbank, rejecting unsupported options explicitly (see module
162+
docstring).
163+
"""
164+
if _HAS_TORCHAUDIO:
165+
return _torchaudio_kaldi.fbank(waveform, **kwargs)
166+
return _fbank_knf(waveform, **kwargs)

‎funasr/utils/load_utils.py‎

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,10 @@
66
import numpy as np
77
import kaldiio
88
import librosa
9-
import torchaudio
9+
try:
10+
import torchaudio
11+
except ImportError:
12+
torchaudio = None
1013
import time
1114
import logging
1215
from torch.nn.utils.rnn import pad_sequence
@@ -174,8 +177,14 @@ def load_audio_text_image_video(
174177
# print(f"unsupport data type: {data_or_path_or_list}, return raw data")
175178

176179
if audio_fs != fs and data_type != "text":
177-
resampler = torchaudio.transforms.Resample(audio_fs, fs)
178-
data_or_path_or_list = resampler(data_or_path_or_list[None, :])[0, :]
180+
if torchaudio is not None:
181+
resampler = torchaudio.transforms.Resample(audio_fs, fs)
182+
data_or_path_or_list = resampler(data_or_path_or_list[None, :])[0, :]
183+
else:
184+
y = librosa.resample(
185+
data_or_path_or_list.cpu().numpy(), orig_sr=int(audio_fs), target_sr=int(fs)
186+
)
187+
data_or_path_or_list = torch.from_numpy(y)
179188
return data_or_path_or_list
180189

181190

‎funasr/utils/speaker_utils.py‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@
99
import numpy as np
1010
import torch
1111
import torch.nn.functional as F
12-
import torchaudio.compliance.kaldi as Kaldi
12+
from funasr.utils import fbank as Kaldi
1313
from torch import nn
1414

1515
from funasr.utils.modelscope_file import File

‎funasr/utils/torchaudio_compat.py‎

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
# Copyright (c) community contributors.
2+
# Shared guard for the optional ``torchaudio`` dependency.
3+
#
4+
# FunASR's inference path no longer hard-depends on torchaudio: fbank feature
5+
# extraction falls back to kaldi-native-fbank (see :mod:`funasr.utils.fbank`)
6+
# and audio decoding falls back to soundfile/librosa. A few operations still
7+
# require torchaudio; call :func:`require_torchaudio` at their entry point so a
8+
# missing torchaudio fails with an actionable error instead of a bare
9+
# ``AttributeError`` (or, worse, a silently wrong result).
10+
import importlib.util
11+
12+
_INSTALL_HINT = (
13+
"Install a torchaudio build matching your torch version, e.g. "
14+
"``pip install torchaudio``. Some platforms -- notably Ascend NPU "
15+
"(aarch64) servers -- ship no matching torchaudio wheel; there, prefer a "
16+
"FunASR path that does not need torchaudio."
17+
)
18+
19+
20+
def torchaudio_available() -> bool:
21+
"""Return ``True`` if torchaudio is importable."""
22+
return importlib.util.find_spec("torchaudio") is not None
23+
24+
25+
def require_torchaudio(feature: str = "This operation"):
26+
"""Return the torchaudio module, or raise an actionable ``ImportError``.
27+
28+
Args:
29+
feature: Human-readable name of the operation that needs torchaudio,
30+
used in the error message.
31+
"""
32+
try:
33+
import torchaudio
34+
except ImportError as exc:
35+
raise ImportError(
36+
f"{feature} requires torchaudio, which is not installed. {_INSTALL_HINT}"
37+
) from exc
38+
return torchaudio

0 commit comments

Comments
 (0)