Skip to content

Commit e7ad5c2

Browse files
authored
Merge pull request #147 from iautolab/feat/replace-deepfilternet-with-dpdfnet
feat: replace DeepFilterNet with DPDFNet for noise suppression
2 parents 45be59c + 0e05280 commit e7ad5c2

11 files changed

Lines changed: 109 additions & 130 deletions

File tree

.github/workflows/ci.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ jobs:
1414
strategy:
1515
fail-fast: false
1616
matrix:
17-
python-version: [ "3.10", "3.11", "3.12" ]
17+
python-version: [ "3.11", "3.12" ]
1818
os: [ windows-latest, ubuntu-latest ]
1919

2020
defaults:

.github/workflows/dispatch_CI.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ jobs:
1414
strategy:
1515
fail-fast: false
1616
matrix:
17-
python-version: [ "3.9" ]
17+
python-version: [ "3.11" ]
1818
os: [ macos-latest ]
1919

2020
defaults:

README.md

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -59,7 +59,7 @@ into `.lrc` subtitles with LLMs such as
5959
```
6060

6161
5. **(Optional)** If you need noise suppression (`noise_suppress=True`), install the full extras
62-
which includes torch and DeepFilterNet:
62+
which includes DPDFNet:
6363

6464
```shell
6565
pip install 'openlrc[full]'
@@ -76,7 +76,7 @@ into `.lrc` subtitles with LLMs such as
7676
OpenLRC keeps several package-root APIs lightweight to import.
7777

7878
The following imports are guaranteed not to eagerly load heavyweight runtime dependencies such as
79-
`torch`, `spacy`, `faster-whisper`, `tiktoken`, or `lingua`:
79+
`dpdfnet`, `spacy`, `faster-whisper`, `tiktoken`, or `lingua`:
8080

8181
```python
8282
import openlrc
@@ -91,15 +91,16 @@ without immediately starting transcription or language-processing work.
9191
Heavy dependencies are loaded only when the corresponding features are first used. For example:
9292

9393
- `faster-whisper` is loaded when transcription is first needed.
94-
- `torch` and `df.enhance` are loaded when noise suppression is used.
94+
- `dpdfnet` is loaded when noise suppression is used.
9595
- `spacy` is loaded when sentence segmentation or related NLP helpers are used.
9696
- `tiktoken` is loaded when token counting is used.
9797
- `lingua` is loaded when language detection helpers are used.
9898

9999
> [!NOTE]
100-
> The base `pip install openlrc` does **not** include torch or DeepFilterNet.
101-
> These are only installed with `pip install 'openlrc[full]'` and are only needed
102-
> for noise suppression (`noise_suppress=True`).
100+
> The base `pip install openlrc` does **not** include dpdfnet.
101+
> It is only installed with `pip install 'openlrc[full]'` and is only needed
102+
> for noise suppression (`noise_suppress=True`). On first use, DPDFNet downloads
103+
> its model (default `dpdfnet2_48khz_hr`, ~10MB) to `~/.cache/dpdfnet/models`.
103104

104105
## Usage 🐍
105106

openlrc/defaults.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,9 @@
4040
"speech_pad_ms": 400,
4141
}
4242

43-
default_preprocess_options = {"atten_lim_db": 15}
43+
DEFAULT_DPDFNET_MODEL = "dpdfnet2_48khz_hr"
44+
45+
default_preprocess_options = {"atten_lim_db": 15, "dpdfnet_model": DEFAULT_DPDFNET_MODEL}
4446

4547
# File name suffixes used throughout the pipeline.
4648
# The processing chain produces files like:

openlrc/media_utils.py

Lines changed: 2 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
"""Media-related utility functions that depend on heavy external libraries.
55
66
Functions in this module import packages such as ``ffmpeg``, ``filetype``,
7-
``audioread``, ``torch``, and ``spacy`` inside their bodies. Keeping them
7+
``audioread``, and ``spacy`` inside their bodies. Keeping them
88
separate from :mod:`openlrc.utils` ensures that the lightweight translation
99
path never triggers those imports — critical for Nuitka ``--nofollow-import-to``
1010
builds where the heavy packages are intentionally excluded.
@@ -14,7 +14,7 @@
1414

1515
import subprocess
1616
from pathlib import Path
17-
from typing import TYPE_CHECKING, Any
17+
from typing import TYPE_CHECKING
1818

1919
if TYPE_CHECKING:
2020
from spacy.language import Language as SpacyLanguage
@@ -84,16 +84,6 @@ def get_audio_duration(path: str | Path) -> float:
8484
return audio.duration
8585

8686

87-
def release_memory(model: Any) -> None:
88-
try:
89-
import torch
90-
except ImportError:
91-
return
92-
93-
if isinstance(model, torch.nn.Module):
94-
torch.cuda.empty_cache()
95-
96-
9787
def get_spacy_lib(lang):
9888
special_case = {"core_web": ["zh", "en"], "ent_wiki": ["xx"]}
9989

openlrc/models.py

Lines changed: 2 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,11 @@
11
# Copyright (C) 2025. Hao Zheng
22
# All rights reserved.
33

4-
import sys
54
from dataclasses import dataclass
6-
from enum import Enum
5+
from enum import StrEnum
76

8-
if sys.version_info >= (3, 11):
9-
from enum import StrEnum as _StrEnum
10-
else:
11-
class _StrEnum(str, Enum):
12-
"""Backport of StrEnum for Python 3.10."""
13-
def __str__(self) -> str:
14-
return self.value
157

16-
17-
class ModelProvider(_StrEnum):
8+
class ModelProvider(StrEnum):
189
ANTHROPIC = "anthropic"
1910
OPENAI = "openai"
2011
GOOGLE = "google"

openlrc/preprocess.py

Lines changed: 29 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -7,9 +7,14 @@
77
from ffmpeg_normalize import FFmpegNormalize
88
from tqdm import tqdm
99

10-
from openlrc.defaults import LOUDNORM_SUFFIX, NOISE_SUPPRESSED_SUFFIX, PREPROCESSED_DIR, default_preprocess_options
10+
from openlrc.defaults import (
11+
DEFAULT_DPDFNET_MODEL,
12+
LOUDNORM_SUFFIX,
13+
NOISE_SUPPRESSED_SUFFIX,
14+
PREPROCESSED_DIR,
15+
default_preprocess_options,
16+
)
1117
from openlrc.logger import logger
12-
from openlrc.media_utils import release_memory
1318
from openlrc.utils import get_preprocessed_path
1419

1520

@@ -63,17 +68,15 @@ def noise_suppression(self, audio_paths: list[Path], atten_lim_db: int = 15):
6368
return []
6469

6570
try:
66-
import torch
67-
from df.enhance import enhance, init_df, load_audio, save_audio
71+
import dpdfnet
72+
import librosa
73+
import soundfile
6874
except ImportError:
69-
raise ImportError(
70-
"Noise suppression requires torch and deepfilternet. Install them with: pip install 'openlrc[full]'"
71-
)
75+
raise ImportError("Noise suppression requires dpdfnet. Install it with: pip install 'openlrc[full]'")
7276

7377
if "atten_lim_db" in self.options:
7478
atten_lim_db = self.options["atten_lim_db"]
75-
76-
model, df_state, _ = init_df()
79+
model = self.options.get("dpdfnet_model", DEFAULT_DPDFNET_MODEL)
7780
chunk_size = 180 # 3 min
7881

7982
ns_audio_paths = []
@@ -82,31 +85,33 @@ def noise_suppression(self, audio_paths: list[Path], atten_lim_db: int = 15):
8285
ns_path = output_path / f"{audio_name}{NOISE_SUPPRESSED_SUFFIX}.wav"
8386

8487
if not ns_path.exists():
85-
audio, info = load_audio(str(audio_path), sr=df_state.sr())
88+
waveform, sr = librosa.load(str(audio_path), sr=None, mono=False)
89+
sr = int(sr)
8690

8791
# Split audio into 3 min chunks
92+
chunk_samples = chunk_size * sr
8893
audio_chunks = [
89-
audio[:, i : i + chunk_size * info.sample_rate]
90-
for i in range(0, audio.shape[1], chunk_size * info.sample_rate)
94+
waveform[..., i : i + chunk_samples] for i in range(0, waveform.shape[-1], chunk_samples)
9195
]
9296

93-
enhanced_chunks = []
94-
for ac in tqdm(audio_chunks, desc=f"Noise suppressing for {audio_name}"):
95-
enhanced_chunks.append(enhance(model, df_state, ac, atten_lim_db=atten_lim_db))
96-
97-
enhanced = torch.cat(enhanced_chunks, dim=1)
97+
try:
98+
with soundfile.SoundFile(str(ns_path), mode="w", samplerate=sr, channels=1, subtype="PCM_16") as f:
99+
for ac in tqdm(audio_chunks, desc=f"Noise suppressing for {audio_name}"):
100+
enhanced = dpdfnet.enhance(ac, sr, model=model, attn_limit_db=atten_lim_db)
98101

99-
if enhanced.shape != audio.shape:
100-
raise ValueError(
101-
f"Enhanced audio shape does not match original audio shape: {enhanced.shape} != {audio.shape}"
102-
)
102+
if enhanced.shape[-1] != ac.shape[-1]:
103+
raise ValueError(
104+
f"Enhanced audio shape does not match original audio shape: "
105+
f"{enhanced.shape} != {ac.shape}"
106+
)
103107

104-
save_audio(str(ns_path), enhanced, sr=df_state.sr())
108+
f.write(enhanced)
109+
except Exception:
110+
ns_path.unlink(missing_ok=True)
111+
raise
105112

106113
ns_audio_paths.append(ns_path)
107114

108-
release_memory(model)
109-
110115
return ns_audio_paths
111116

112117
def loudness_normalization(self, audio_paths: list[Path]):

pyproject.toml

Lines changed: 7 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ name = "openlrc"
77
version = "1.7.0a1"
88
description = "Transcribe (whisper) and translate (gpt) voice into LRC file."
99
authors = [{ name = "Hao Zheng", email = "zhenghaosustc@gmail.com" }]
10-
requires-python = ">=3.10, <3.13"
10+
requires-python = ">=3.11, <3.13"
1111
readme = "README.md"
1212
license = "MIT"
1313
keywords = ["openai-gpt3", "whisper", "voice transcribe", "lrc"]
@@ -38,19 +38,17 @@ dependencies = [
3838
"ffmpeg-normalize>=1.27.5,<2",
3939
"google-genai>=1.70.0",
4040
"json_repair==0.25.2",
41-
"onnxruntime>=1.20.0,<1.24; python_version < '3.11'",
42-
"onnxruntime>=1.20.0,<2; python_version >= '3.11'",
41+
"onnxruntime>=1.20.0,<2",
4342
"pip>=25.1",
4443
]
4544

4645
[project.optional-dependencies]
47-
# Noise suppression via DeepFilterNet (requires torch).
46+
# Noise suppression via DPDFNet (pure Python + ONNX Runtime, no torch/torchaudio).
4847
# Install with: pip install openlrc[full]
4948
# Only needed when using noise_suppress=True in LRCer.run() or Preprocessor.run().
5049
full = [
51-
"torch>=2.6.0",
52-
"torchaudio>=2.0.0",
53-
"deepfilternet>=0.5.6,<0.6",
50+
"dpdfnet>=0.6.0",
51+
"numba>=0.58.0", # first version supporting both Python 3.11 and 3.12
5452
]
5553
litellm = [
5654
"litellm>=1.60,<1.85",
@@ -75,21 +73,8 @@ name = "PyPI"
7573
url = "https://pypi.org/simple/"
7674
default = true
7775

78-
[[tool.uv.index]]
79-
name = "pytorch-cu124"
80-
url = "https://download.pytorch.org/whl/cu124"
81-
explicit = true
82-
83-
[tool.uv.sources]
84-
torch = [
85-
{ index = "pytorch-cu124", marker = "sys_platform == 'linux' or sys_platform == 'win32'" },
86-
]
87-
torchaudio = [
88-
{ index = "pytorch-cu124", marker = "sys_platform == 'linux' or sys_platform == 'win32'" },
89-
]
90-
9176
[tool.ruff]
92-
target-version = "py310"
77+
target-version = "py311"
9378
line-length = 120
9479

9580
[tool.ruff.format]
@@ -106,7 +91,7 @@ known-first-party = ["openlrc"]
10691
split-on-trailing-comma = false
10792

10893
[tool.pyright]
109-
pythonVersion = "3.10"
94+
pythonVersion = "3.11"
11095
exclude = ["openlrc/gui_streamlit"]
11196

11297
[tool.hatch.build.targets.sdist]

tests/test_lazy_imports.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@
1010

1111

1212
class TestLazyImports(unittest.TestCase):
13-
FORBIDDEN_ROOTS = {"faster_whisper", "spacy", "tiktoken", "torch", "lingua"}
13+
FORBIDDEN_ROOTS = {"faster_whisper", "spacy", "tiktoken", "dpdfnet", "lingua"}
1414
PROBE_MARKER = "__OPENLRC_LAZY_IMPORTS__="
1515

1616
def _loaded_modules_after(self, statement: str):
@@ -59,7 +59,7 @@ def test_translate_path_does_not_load_media_utils_or_heavy_deps(self):
5959
# The translate path legitimately loads lingua (via validators.py) and
6060
# tiktoken (via utils.get_text_token_number). Only check for the truly
6161
# heavy packages that would bloat a Nuitka binary.
62-
heavy_roots = {"faster_whisper", "spacy", "torch"}
62+
heavy_roots = {"faster_whisper", "spacy", "dpdfnet"}
6363
loaded = self._loaded_modules_after(
6464
"from openlrc.agents import create_chatbot; "
6565
"from openlrc.context import TranslateInfo; "

0 commit comments

Comments
 (0)