crepe-predictor 0.1.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
|
+
Name: crepe-predictor
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: A dependency-light reimplementation of CREPE inference with ONNX
|
|
5
|
+
Keywords: machine learning,speech
|
|
6
|
+
Author: Maxime Poli
|
|
7
|
+
Requires-Python: >=3.13
|
|
8
|
+
Description-Content-Type: text/markdown
|
|
9
|
+
Classifier: Development Status :: 4 - Beta
|
|
10
|
+
Classifier: Intended Audience :: Science/Research
|
|
11
|
+
Classifier: Operating System :: OS Independent
|
|
12
|
+
Classifier: Programming Language :: Python :: 3 :: Only
|
|
13
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.14
|
|
15
|
+
Classifier: Topic :: Scientific/Engineering
|
|
16
|
+
Classifier: Topic :: Scientific/Engineering :: Artificial Intelligence
|
|
17
|
+
Requires-Dist: numpy>=2.4.6
|
|
18
|
+
Requires-Dist: onnxruntime>=1.27.0
|
|
19
|
+
Requires-Dist: scipy>=1.18.0
|
|
20
|
+
Project-URL: repository, https://github.com/mxmpl/crepe-predictor
|
|
21
|
+
Import-Name: crepe_predictor
|
|
22
|
+
|
|
23
|
+
# crepe-predictor
|
|
24
|
+
|
|
25
|
+
A dependency-light reimplementation of [CREPE](https://github.com/marl/crepe) pitch estimation, exported to ONNX for inference without PyTorch.
|
|
26
|
+
|
|
27
|
+
- `crepe_predictor.py` — the runtime package: framing, Viterbi-decoded pitch prediction from an ONNX session, and Kaldi-compatible (NCCF, pitch) postprocessing, wrapped in a `CrepePredictor` class.
|
|
28
|
+
- `export_torchcrepe_to_onnx.py` — script to (re-)generate the ONNX checkpoints.
|
|
29
|
+
|
|
30
|
+
## Installation
|
|
31
|
+
|
|
32
|
+
```sh
|
|
33
|
+
pip install crepe-predictor
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
Inference only depends on `numpy`, `onnxruntime`, and `scipy`.
|
|
37
|
+
Exporting checkpoints additionally requires `torch` and `onnxscript`, listed as script dependencies at the top of `export_torchcrepe_to_onnx.py`.
|
|
38
|
+
|
|
39
|
+
## API
|
|
40
|
+
|
|
41
|
+
Everything is exposed through `crepe_predictor.CrepePredictor`.
|
|
42
|
+
|
|
43
|
+
### `CrepePredictor(capacity="full", *, checkpoint=None, onnx_providers=None)`
|
|
44
|
+
|
|
45
|
+
Resolves a checkpoint and opens an ONNX Runtime session for it.
|
|
46
|
+
|
|
47
|
+
- `capacity`: `"tiny"`, `"small"`, `"medium"`, `"large"`, or `"full"` — model size, trading accuracy for speed.
|
|
48
|
+
- `checkpoint`: path to a local `.onnx` file. If omitted, the checkpoint matching `capacity` is downloaded and cached under `$CREPE_CACHE_DIR`, `$XDG_CACHE_HOME`, or `~/.cache/crepe_predictor/checkpoints`.
|
|
49
|
+
- `onnx_providers`: ONNX Runtime execution providers, e.g. `["CUDAExecutionProvider", "CPUExecutionProvider"]`. Defaults to `["CPUExecutionProvider"]`.
|
|
50
|
+
|
|
51
|
+
### `predict(audio, *, viterbi=True, center=True, frame_shift=0.01, frame_length=0.025) -> np.ndarray`
|
|
52
|
+
|
|
53
|
+
Estimates pitch from 16 kHz mono `audio`, returning an `(n_frames, 2)` array of `(POV, pitch)`: probability of voicing in `[0, 1]`, and pitch in Hz.
|
|
54
|
+
|
|
55
|
+
- `viterbi`: decode pitch bins along a Viterbi path enforcing pitch continuity, instead of a per-frame argmax.
|
|
56
|
+
- `center`: pad `audio` so each frame is centered on its timestamp.
|
|
57
|
+
- `frame_shift`, `frame_length`: frame spacing and length in seconds, used to resample the output to the frame count they imply.
|
|
58
|
+
|
|
59
|
+
### `predict_kaldi(audio, *, viterbi=True, center=True, frame_shift=0.01, frame_length=0.025) -> np.ndarray`
|
|
60
|
+
|
|
61
|
+
Same arguments as `predict`, but returns `(n_frames, 2)` of `(NCCF, pitch)`, compatible with Kaldi's `process-pitch`: unvoiced frames are detected with a voicing HMM, pitch is interpolated over them, and POV is converted to an NCCF value. Raises `ValueError` if no frame is voiced.
|
|
62
|
+
|
|
63
|
+
## Usage
|
|
64
|
+
|
|
65
|
+
Estimate pitch from a synthetic tone:
|
|
66
|
+
|
|
67
|
+
```python
|
|
68
|
+
import numpy as np
|
|
69
|
+
from crepe_predictor import CrepePredictor
|
|
70
|
+
|
|
71
|
+
predictor = CrepePredictor("full") # "tiny", "small", "medium", "large", or "full"
|
|
72
|
+
|
|
73
|
+
t = np.arange(16000) / 16000 # 1 second at 16 kHz
|
|
74
|
+
audio = np.sin(2 * np.pi * 220 * t).astype(np.float32) # a 220 Hz tone
|
|
75
|
+
|
|
76
|
+
pov, pitch = predictor.predict(audio).T
|
|
77
|
+
print(pitch[pov > 0.5]) # pitch in Hz for confidently voiced frames
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
Process a recording and produce Kaldi-compatible pitch features, running on GPU when available:
|
|
81
|
+
|
|
82
|
+
```python
|
|
83
|
+
from scipy.io import wavfile
|
|
84
|
+
from crepe_predictor import CrepePredictor
|
|
85
|
+
|
|
86
|
+
predictor = CrepePredictor(
|
|
87
|
+
"full",
|
|
88
|
+
onnx_providers=["CUDAExecutionProvider", "CPUExecutionProvider"],
|
|
89
|
+
)
|
|
90
|
+
|
|
91
|
+
sample_rate, audio = wavfile.read("speech.wav")
|
|
92
|
+
assert sample_rate == 16000
|
|
93
|
+
audio = audio.astype("float32") / 32768.0 # int16 PCM -> float32 in [-1, 1]
|
|
94
|
+
|
|
95
|
+
nccf, pitch = predictor.predict_kaldi(audio).T
|
|
96
|
+
```
|
|
97
|
+
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
# crepe-predictor
|
|
2
|
+
|
|
3
|
+
A dependency-light reimplementation of [CREPE](https://github.com/marl/crepe) pitch estimation, exported to ONNX for inference without PyTorch.
|
|
4
|
+
|
|
5
|
+
- `crepe_predictor.py` — the runtime package: framing, Viterbi-decoded pitch prediction from an ONNX session, and Kaldi-compatible (NCCF, pitch) postprocessing, wrapped in a `CrepePredictor` class.
|
|
6
|
+
- `export_torchcrepe_to_onnx.py` — script to (re-)generate the ONNX checkpoints.
|
|
7
|
+
|
|
8
|
+
## Installation
|
|
9
|
+
|
|
10
|
+
```sh
|
|
11
|
+
pip install crepe-predictor
|
|
12
|
+
```
|
|
13
|
+
|
|
14
|
+
Inference only depends on `numpy`, `onnxruntime`, and `scipy`.
|
|
15
|
+
Exporting checkpoints additionally requires `torch` and `onnxscript`, listed as script dependencies at the top of `export_torchcrepe_to_onnx.py`.
|
|
16
|
+
|
|
17
|
+
## API
|
|
18
|
+
|
|
19
|
+
Everything is exposed through `crepe_predictor.CrepePredictor`.
|
|
20
|
+
|
|
21
|
+
### `CrepePredictor(capacity="full", *, checkpoint=None, onnx_providers=None)`
|
|
22
|
+
|
|
23
|
+
Resolves a checkpoint and opens an ONNX Runtime session for it.
|
|
24
|
+
|
|
25
|
+
- `capacity`: `"tiny"`, `"small"`, `"medium"`, `"large"`, or `"full"` — model size, trading accuracy for speed.
|
|
26
|
+
- `checkpoint`: path to a local `.onnx` file. If omitted, the checkpoint matching `capacity` is downloaded and cached under `$CREPE_CACHE_DIR`, `$XDG_CACHE_HOME`, or `~/.cache/crepe_predictor/checkpoints`.
|
|
27
|
+
- `onnx_providers`: ONNX Runtime execution providers, e.g. `["CUDAExecutionProvider", "CPUExecutionProvider"]`. Defaults to `["CPUExecutionProvider"]`.
|
|
28
|
+
|
|
29
|
+
### `predict(audio, *, viterbi=True, center=True, frame_shift=0.01, frame_length=0.025) -> np.ndarray`
|
|
30
|
+
|
|
31
|
+
Estimates pitch from 16 kHz mono `audio`, returning an `(n_frames, 2)` array of `(POV, pitch)`: probability of voicing in `[0, 1]`, and pitch in Hz.
|
|
32
|
+
|
|
33
|
+
- `viterbi`: decode pitch bins along a Viterbi path enforcing pitch continuity, instead of a per-frame argmax.
|
|
34
|
+
- `center`: pad `audio` so each frame is centered on its timestamp.
|
|
35
|
+
- `frame_shift`, `frame_length`: frame spacing and length in seconds, used to resample the output to the frame count they imply.
|
|
36
|
+
|
|
37
|
+
### `predict_kaldi(audio, *, viterbi=True, center=True, frame_shift=0.01, frame_length=0.025) -> np.ndarray`
|
|
38
|
+
|
|
39
|
+
Same arguments as `predict`, but returns `(n_frames, 2)` of `(NCCF, pitch)`, compatible with Kaldi's `process-pitch`: unvoiced frames are detected with a voicing HMM, pitch is interpolated over them, and POV is converted to an NCCF value. Raises `ValueError` if no frame is voiced.
|
|
40
|
+
|
|
41
|
+
## Usage
|
|
42
|
+
|
|
43
|
+
Estimate pitch from a synthetic tone:
|
|
44
|
+
|
|
45
|
+
```python
|
|
46
|
+
import numpy as np
|
|
47
|
+
from crepe_predictor import CrepePredictor
|
|
48
|
+
|
|
49
|
+
predictor = CrepePredictor("full") # "tiny", "small", "medium", "large", or "full"
|
|
50
|
+
|
|
51
|
+
t = np.arange(16000) / 16000 # 1 second at 16 kHz
|
|
52
|
+
audio = np.sin(2 * np.pi * 220 * t).astype(np.float32) # a 220 Hz tone
|
|
53
|
+
|
|
54
|
+
pov, pitch = predictor.predict(audio).T
|
|
55
|
+
print(pitch[pov > 0.5]) # pitch in Hz for confidently voiced frames
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
Process a recording and produce Kaldi-compatible pitch features, running on GPU when available:
|
|
59
|
+
|
|
60
|
+
```python
|
|
61
|
+
from scipy.io import wavfile
|
|
62
|
+
from crepe_predictor import CrepePredictor
|
|
63
|
+
|
|
64
|
+
predictor = CrepePredictor(
|
|
65
|
+
"full",
|
|
66
|
+
onnx_providers=["CUDAExecutionProvider", "CPUExecutionProvider"],
|
|
67
|
+
)
|
|
68
|
+
|
|
69
|
+
sample_rate, audio = wavfile.read("speech.wav")
|
|
70
|
+
assert sample_rate == 16000
|
|
71
|
+
audio = audio.astype("float32") / 32768.0 # int16 PCM -> float32 in [-1, 1]
|
|
72
|
+
|
|
73
|
+
nccf, pitch = predictor.predict_kaldi(audio).T
|
|
74
|
+
```
|
|
@@ -0,0 +1,252 @@
|
|
|
1
|
+
import hashlib
|
|
2
|
+
import os
|
|
3
|
+
import tempfile
|
|
4
|
+
import urllib.request
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
from typing import Literal
|
|
7
|
+
|
|
8
|
+
import numpy as np
|
|
9
|
+
import onnxruntime as ort
|
|
10
|
+
import scipy.interpolate
|
|
11
|
+
import scipy.optimize
|
|
12
|
+
import scipy.signal
|
|
13
|
+
|
|
14
|
+
__all__ = ["Capacity", "CrepePredictor"]
|
|
15
|
+
|
|
16
|
+
Capacity = Literal["tiny", "small", "medium", "large", "full"]
|
|
17
|
+
|
|
18
|
+
# bin-number-to-cents mapping used by the CREPE classifier (360 pitch bins)
|
|
19
|
+
_CENTS_MAPPING = np.linspace(0, 7180, 360) + 1997.3794084376191
|
|
20
|
+
_SAMPLE_RATE = 16000
|
|
21
|
+
_TIMEOUT = 30
|
|
22
|
+
_REMOTE_URLS = {
|
|
23
|
+
"tiny": "https://media.githubusercontent.com/media/mxmpl/crepe-predictor/main/checkpoints/tiny.onnx",
|
|
24
|
+
"small": "https://media.githubusercontent.com/media/mxmpl/crepe-predictor/main/checkpoints/small.onnx",
|
|
25
|
+
"medium": "https://media.githubusercontent.com/media/mxmpl/crepe-predictor/main/checkpoints/medium.onnx",
|
|
26
|
+
"large": "https://media.githubusercontent.com/media/mxmpl/crepe-predictor/main/checkpoints/large.onnx",
|
|
27
|
+
"full": "https://media.githubusercontent.com/media/mxmpl/crepe-predictor/main/checkpoints/full.onnx",
|
|
28
|
+
}
|
|
29
|
+
_CHECKSUMS = {
|
|
30
|
+
"tiny": "345b8ed787dc94236f237f234c6fb3f3f389291315b44909643c011b7a16f8c7",
|
|
31
|
+
"small": "762e975d7717d5c47265344d3588dfac5bdee7e9a66d666a40f888a20eddbfa8",
|
|
32
|
+
"medium": "00e25a2fbf0b141a2c739609fd1587cb8923daf78162d7dd3404413cc7fbf985",
|
|
33
|
+
"large": "bff31dfecdaca02141cf3c1c3bc984e2ea2bef7de98d9cd6a36bbbe851fb12c4",
|
|
34
|
+
"full": "9046c78f1cf40ebdbad1a2b3d9dc154dab36ef51ab55c6cd4d43776b9f5948ce",
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def _frame(audio: np.ndarray, hop_length: int, center: bool) -> np.ndarray:
|
|
39
|
+
"""Split ``audio`` into normalized 1024-sample frames expected by CREPE."""
|
|
40
|
+
audio = np.asarray(audio, dtype=np.float32)
|
|
41
|
+
if center:
|
|
42
|
+
# pad so frames are centered on their timestamps (first frame is zero-centered)
|
|
43
|
+
audio = np.pad(audio, 512)
|
|
44
|
+
if len(audio) < 1024:
|
|
45
|
+
raise ValueError(f"audio is too short to form a single 1024-sample frame: got {len(audio)} samples")
|
|
46
|
+
frames = np.lib.stride_tricks.sliding_window_view(audio, 1024)[::hop_length].copy()
|
|
47
|
+
frames -= frames.mean(axis=1, keepdims=True)
|
|
48
|
+
frames /= np.clip(frames.std(axis=1, keepdims=True), 1e-8, None) # avoid /0 on constant (silent) frames
|
|
49
|
+
return frames
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def _local_average_cents(salience: np.ndarray, centers: np.ndarray) -> np.ndarray:
|
|
53
|
+
"""Weighted average of the cents mapping over a +/-4 bin window around each center."""
|
|
54
|
+
salience = np.pad(salience, ((0, 0), (4, 4)))
|
|
55
|
+
mapping = np.pad(_CENTS_MAPPING, (4, 4))
|
|
56
|
+
index = centers[:, None] + np.arange(9) # window [center-4, center+4] in padded coords
|
|
57
|
+
window = np.take_along_axis(salience, index, axis=1)
|
|
58
|
+
return (window * mapping[index]).sum(axis=1) / window.sum(axis=1)
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def _viterbi(log_start: np.ndarray, log_trans: np.ndarray, log_emit: np.ndarray) -> np.ndarray:
|
|
62
|
+
"""Generic Viterbi decode. ``log_emit`` has shape (n_frames, n_states)."""
|
|
63
|
+
n_frames = log_emit.shape[0]
|
|
64
|
+
score = log_start + log_emit[0]
|
|
65
|
+
backpointers = np.empty_like(log_emit, dtype=int)
|
|
66
|
+
for t in range(1, n_frames):
|
|
67
|
+
candidates = score[:, None] + log_trans
|
|
68
|
+
backpointers[t] = candidates.argmax(axis=0)
|
|
69
|
+
score = candidates.max(axis=0) + log_emit[t]
|
|
70
|
+
path = np.empty(n_frames, dtype=int)
|
|
71
|
+
path[-1] = score.argmax()
|
|
72
|
+
for t in range(n_frames - 1, 0, -1):
|
|
73
|
+
path[t - 1] = backpointers[t, path[t]]
|
|
74
|
+
return path
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
def _viterbi_centers(salience: np.ndarray) -> np.ndarray:
|
|
78
|
+
"""Viterbi path over the 360 pitch bins with a transition prior enforcing continuity."""
|
|
79
|
+
n = 360
|
|
80
|
+
transition = np.maximum(12 - np.abs(np.subtract.outer(np.arange(n), np.arange(n))), 0.0)
|
|
81
|
+
transition /= transition.sum(axis=1, keepdims=True)
|
|
82
|
+
emission = np.eye(n) * 0.1 + 0.9 / n # fixed self-probability, uniform otherwise
|
|
83
|
+
observations = salience.argmax(axis=1)
|
|
84
|
+
with np.errstate(divide="ignore"):
|
|
85
|
+
return _viterbi(np.full(n, -np.log(n)), np.log(transition), np.log(emission)[:, observations].T)
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
def _predict(
|
|
89
|
+
session: ort.InferenceSession,
|
|
90
|
+
audio: np.ndarray,
|
|
91
|
+
viterbi: bool = True,
|
|
92
|
+
center: bool = True,
|
|
93
|
+
frame_shift: float = 0.01,
|
|
94
|
+
frame_length: float = 0.025,
|
|
95
|
+
) -> np.ndarray:
|
|
96
|
+
"""Extract the (POV, pitch) per frame from a 16 kHz mono ``audio`` signal.
|
|
97
|
+
|
|
98
|
+
``session`` runs the CREPE model exported to ONNX (see ``model.export_onnx``). The first
|
|
99
|
+
output column is the probability of voicing, the second the estimated pitch in Hz.
|
|
100
|
+
"""
|
|
101
|
+
hop_length = round(_SAMPLE_RATE * frame_shift)
|
|
102
|
+
frames = _frame(audio, hop_length, center)
|
|
103
|
+
salience = np.asarray(session.run(None, {session.get_inputs()[0].name: frames})[0]) # activation matrix, (T, 360)
|
|
104
|
+
confidence = salience.max(axis=1) # heuristic voicing probability
|
|
105
|
+
centers = _viterbi_centers(salience) if viterbi else salience.argmax(axis=1)
|
|
106
|
+
cents = _local_average_cents(salience, centers)
|
|
107
|
+
frequency = 10 * 2 ** (cents / 1200)
|
|
108
|
+
frequency[np.isnan(frequency)] = 0
|
|
109
|
+
|
|
110
|
+
# resample (POV, pitch) to the frame count implied by frame_shift/frame_length
|
|
111
|
+
nsamples = 1 + int((len(audio) - frame_length * _SAMPLE_RATE) / hop_length)
|
|
112
|
+
if nsamples < 1:
|
|
113
|
+
min_samples = int(frame_length * _SAMPLE_RATE)
|
|
114
|
+
raise ValueError(
|
|
115
|
+
f"audio is too short to produce any output frames: got {len(audio)} samples, but "
|
|
116
|
+
f"frame_length={frame_length}s needs at least {min_samples} samples at {_SAMPLE_RATE} Hz"
|
|
117
|
+
)
|
|
118
|
+
data = scipy.signal.resample(np.stack([confidence, frequency], axis=1), nsamples)
|
|
119
|
+
data[data[:, 0] < 1e-2, 0] = 0
|
|
120
|
+
data[data[:, 0] > 1, 0] = 1
|
|
121
|
+
return data
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
def _predict_voicing(confidence: np.ndarray) -> np.ndarray:
|
|
125
|
+
"""Viterbi path over voiced (1) vs unvoiced (0) frames from the voicing confidence."""
|
|
126
|
+
means, variance = np.array([0.0, 1.0]), 0.25 # unvoiced and voiced states
|
|
127
|
+
log_emit = -((confidence[:, None] - means) ** 2) / (2 * variance) # gaussian, up to a constant
|
|
128
|
+
log_start = np.log([0.5, 0.5])
|
|
129
|
+
log_trans = np.log([[0.99, 0.01], [0.01, 0.99]]) # prior on continuous voicing state
|
|
130
|
+
return _viterbi(log_start, log_trans, log_emit)
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
def _nccf_to_pov(nccf: float) -> float:
|
|
134
|
+
"""Normalized cross-correlation to probability of voicing (Povey, ICASSP 2014)."""
|
|
135
|
+
y = -5.2 + 5.4 * np.exp(7.5 * (nccf - 1)) + 4.8 * nccf - 2 * np.exp(-10 * nccf) + 4.2 * np.exp(20 * (nccf - 1))
|
|
136
|
+
return 1 / (1 + np.exp(-y))
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
def _postprocess(pitch: np.ndarray) -> np.ndarray:
|
|
140
|
+
"""Turn the raw (POV, pitch) from :func:`_predict` into (NCCF, pitch) for Kaldi.
|
|
141
|
+
|
|
142
|
+
Unvoiced frames are detected with a voicing HMM and their pitch interpolated, then
|
|
143
|
+
the POV is converted back to an NCCF usable by Kaldi's ``process-pitch``.
|
|
144
|
+
"""
|
|
145
|
+
to_remove = _predict_voicing(pitch[:, 0]) == 0 # interpolate pitch values over the unvoiced frames
|
|
146
|
+
if np.all(to_remove):
|
|
147
|
+
raise ValueError("No voiced frames")
|
|
148
|
+
data = pitch[:, 1].copy()
|
|
149
|
+
keep = np.where(~to_remove)[0]
|
|
150
|
+
first, last = keep[0], keep[-1]
|
|
151
|
+
interp = scipy.interpolate.interp1d(keep, data[keep], fill_value="extrapolate")
|
|
152
|
+
data[to_remove] = interp(np.where(to_remove)[0])
|
|
153
|
+
data[:first] = data[first]
|
|
154
|
+
data[last:] = data[last]
|
|
155
|
+
if not np.all(data > 0):
|
|
156
|
+
raise ValueError("Not all pitch values are positive after interpolation")
|
|
157
|
+
|
|
158
|
+
# invert the POV -> NCCF mapping (saturates to 0/1 outside the range spanned by nccf in [0, 1])
|
|
159
|
+
lo, hi = _nccf_to_pov(0.0), _nccf_to_pov(1.0)
|
|
160
|
+
nccf = np.array(
|
|
161
|
+
[
|
|
162
|
+
0.0
|
|
163
|
+
if pov <= lo
|
|
164
|
+
else 1.0
|
|
165
|
+
if pov >= hi
|
|
166
|
+
else scipy.optimize.bisect(lambda x, pov=pov: _nccf_to_pov(x) - pov, 0, 1)
|
|
167
|
+
for pov in pitch[:, 0]
|
|
168
|
+
]
|
|
169
|
+
)
|
|
170
|
+
return np.stack([nccf, data], axis=1)
|
|
171
|
+
|
|
172
|
+
|
|
173
|
+
def _cache_dir() -> Path:
|
|
174
|
+
base = os.environ.get("CREPE_CACHE_DIR") or os.environ.get("XDG_CACHE_HOME") or str(Path.home() / ".cache")
|
|
175
|
+
return Path(base) / "crepe_predictor" / "checkpoints"
|
|
176
|
+
|
|
177
|
+
|
|
178
|
+
def _download(capacity: Capacity, destination: Path) -> None:
|
|
179
|
+
destination.parent.mkdir(parents=True, exist_ok=True)
|
|
180
|
+
with tempfile.TemporaryDirectory(dir=destination.parent) as tmp_dir:
|
|
181
|
+
tmp = Path(tmp_dir) / destination.name
|
|
182
|
+
digest = hashlib.sha256()
|
|
183
|
+
with urllib.request.urlopen(_REMOTE_URLS[capacity], timeout=_TIMEOUT) as response, tmp.open("wb") as f:
|
|
184
|
+
for chunk in iter(lambda: response.read(1 << 20), b""):
|
|
185
|
+
f.write(chunk)
|
|
186
|
+
digest.update(chunk)
|
|
187
|
+
if digest.hexdigest() != _CHECKSUMS[capacity]:
|
|
188
|
+
raise ValueError(
|
|
189
|
+
f"checksum mismatch for the {capacity!r} checkpoint: "
|
|
190
|
+
f"expected {_CHECKSUMS[capacity]}, got {digest.hexdigest()}"
|
|
191
|
+
)
|
|
192
|
+
tmp.replace(destination)
|
|
193
|
+
|
|
194
|
+
|
|
195
|
+
def _resolve_checkpoint(capacity: Capacity, checkpoint: str | Path | None) -> Path:
|
|
196
|
+
"""Resolve a checkpoint path, downloading and caching the remote one if needed."""
|
|
197
|
+
if checkpoint is not None:
|
|
198
|
+
path = Path(checkpoint)
|
|
199
|
+
if not path.is_file():
|
|
200
|
+
raise FileNotFoundError(f"No checkpoint at {path}")
|
|
201
|
+
return path
|
|
202
|
+
path = _cache_dir() / f"{capacity}.onnx"
|
|
203
|
+
if not path.is_file():
|
|
204
|
+
_download(capacity, path)
|
|
205
|
+
return path
|
|
206
|
+
|
|
207
|
+
|
|
208
|
+
class CrepePredictor:
|
|
209
|
+
"""Load a CREPE ONNX checkpoint and run pitch inference."""
|
|
210
|
+
|
|
211
|
+
def __init__(
|
|
212
|
+
self,
|
|
213
|
+
capacity: Capacity = "full",
|
|
214
|
+
*,
|
|
215
|
+
checkpoint: str | Path | None = None,
|
|
216
|
+
onnx_providers: list[str] | None = None,
|
|
217
|
+
) -> None:
|
|
218
|
+
self.capacity = capacity
|
|
219
|
+
path = _resolve_checkpoint(capacity, checkpoint)
|
|
220
|
+
self.session = ort.InferenceSession(str(path), providers=onnx_providers or ["CPUExecutionProvider"])
|
|
221
|
+
|
|
222
|
+
def predict(
|
|
223
|
+
self,
|
|
224
|
+
audio: np.ndarray,
|
|
225
|
+
*,
|
|
226
|
+
viterbi: bool = True,
|
|
227
|
+
center: bool = True,
|
|
228
|
+
frame_shift: float = 0.01,
|
|
229
|
+
frame_length: float = 0.025,
|
|
230
|
+
) -> np.ndarray:
|
|
231
|
+
"""Estimate (POV, pitch) per frame, as an ``(n_frames, 2)`` array in Hz."""
|
|
232
|
+
return _predict(self.session, audio, viterbi, center, frame_shift, frame_length)
|
|
233
|
+
|
|
234
|
+
def predict_kaldi(
|
|
235
|
+
self,
|
|
236
|
+
audio: np.ndarray,
|
|
237
|
+
*,
|
|
238
|
+
viterbi: bool = True,
|
|
239
|
+
center: bool = True,
|
|
240
|
+
frame_shift: float = 0.01,
|
|
241
|
+
frame_length: float = 0.025,
|
|
242
|
+
) -> np.ndarray:
|
|
243
|
+
"""Like :meth:`predict`, but returns (NCCF, pitch) per frame for use with Kaldi's ``process-pitch``."""
|
|
244
|
+
return _postprocess(
|
|
245
|
+
self.predict(
|
|
246
|
+
audio,
|
|
247
|
+
viterbi=viterbi,
|
|
248
|
+
center=center,
|
|
249
|
+
frame_shift=frame_shift,
|
|
250
|
+
frame_length=frame_length,
|
|
251
|
+
)
|
|
252
|
+
)
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
[project]
|
|
2
|
+
name = "crepe-predictor"
|
|
3
|
+
version = "0.1.0"
|
|
4
|
+
description = "A dependency-light reimplementation of CREPE inference with ONNX"
|
|
5
|
+
readme = "README.md"
|
|
6
|
+
requires-python = ">=3.13"
|
|
7
|
+
authors = [{ name = "Maxime Poli" }]
|
|
8
|
+
keywords = ["machine learning", "speech"]
|
|
9
|
+
classifiers = [
|
|
10
|
+
"Development Status :: 4 - Beta",
|
|
11
|
+
"Intended Audience :: Science/Research",
|
|
12
|
+
"Operating System :: OS Independent",
|
|
13
|
+
"Programming Language :: Python :: 3 :: Only",
|
|
14
|
+
"Programming Language :: Python :: 3.13",
|
|
15
|
+
"Programming Language :: Python :: 3.14",
|
|
16
|
+
"Topic :: Scientific/Engineering",
|
|
17
|
+
"Topic :: Scientific/Engineering :: Artificial Intelligence",
|
|
18
|
+
]
|
|
19
|
+
dependencies = [
|
|
20
|
+
"numpy>=2.4.6",
|
|
21
|
+
"onnxruntime>=1.27.0",
|
|
22
|
+
"scipy>=1.18.0",
|
|
23
|
+
]
|
|
24
|
+
|
|
25
|
+
[project.urls]
|
|
26
|
+
repository = "https://github.com/mxmpl/crepe-predictor"
|
|
27
|
+
|
|
28
|
+
[dependency-groups]
|
|
29
|
+
test = [
|
|
30
|
+
"hypothesis>=6.156.4",
|
|
31
|
+
"onnx>=1.22.0",
|
|
32
|
+
"onnxscript>=0.7.1",
|
|
33
|
+
"pytest>=9.1.1",
|
|
34
|
+
"pytest-cov>=7.1.0",
|
|
35
|
+
"torchcrepe>=0.0.24",
|
|
36
|
+
]
|
|
37
|
+
|
|
38
|
+
[build-system]
|
|
39
|
+
requires = ["flit_core>=4.0.2"]
|
|
40
|
+
build-backend = "flit_core.buildapi"
|
|
41
|
+
|
|
42
|
+
[tool.coverage.run]
|
|
43
|
+
branch = true
|
|
44
|
+
source = ["crepe_predictor.py"]
|
|
45
|
+
|
|
46
|
+
[tool.coverage.report]
|
|
47
|
+
fail_under = 99
|
|
48
|
+
|
|
49
|
+
[tool.pytest]
|
|
50
|
+
addopts = ["--cov=crepe_predictor"]
|
|
51
|
+
filterwarnings = ["ignore:.*LeafSpec.*:FutureWarning"]
|
|
52
|
+
|
|
53
|
+
[tool.ruff]
|
|
54
|
+
line-length = 119
|
|
55
|
+
|
|
56
|
+
[tool.typos.default.extend-words]
|
|
57
|
+
arange = "arange"
|