ssl-kaldi-feats 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,201 @@
1
+ Apache License
2
+ Version 2.0, January 2004
3
+ http://www.apache.org/licenses/
4
+
5
+ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
6
+
7
+ 1. Definitions.
8
+
9
+ "License" shall mean the terms and conditions for use, reproduction,
10
+ and distribution as defined by Sections 1 through 9 of this document.
11
+
12
+ "Licensor" shall mean the copyright owner or entity authorized by
13
+ the copyright owner that is granting the License.
14
+
15
+ "Legal Entity" shall mean the union of the acting entity and all
16
+ other entities that control, are controlled by, or are under common
17
+ control with that entity. For the purposes of this definition,
18
+ "control" means (i) the power, direct or indirect, to cause the
19
+ direction or management of such entity, whether by contract or
20
+ otherwise, or (ii) ownership of fifty percent (50%) or more of the
21
+ outstanding shares, or (iii) beneficial ownership of such entity.
22
+
23
+ "You" (or "Your") shall mean an individual or Legal Entity
24
+ exercising permissions granted by this License.
25
+
26
+ "Source" form shall mean the preferred form for making modifications,
27
+ including but not limited to software source code, documentation
28
+ source, and configuration files.
29
+
30
+ "Object" form shall mean any form resulting from mechanical
31
+ transformation or translation of a Source form, including but
32
+ not limited to compiled object code, generated documentation,
33
+ and conversions to other media types.
34
+
35
+ "Work" shall mean the work of authorship, whether in Source or
36
+ Object form, made available under the License, as indicated by a
37
+ copyright notice that is included in or attached to the work
38
+ (an example is provided in the Appendix below).
39
+
40
+ "Derivative Works" shall mean any work, whether in Source or Object
41
+ form, that is based on (or derived from) the Work and for which the
42
+ editorial revisions, annotations, elaborations, or other modifications
43
+ represent, as a whole, an original work of authorship. For the purposes
44
+ of this License, Derivative Works shall not include works that remain
45
+ separable from, or merely link (or bind by name) to the interfaces of,
46
+ the Work and Derivative Works thereof.
47
+
48
+ "Contribution" shall mean any work of authorship, including
49
+ the original version of the Work and any modifications or additions
50
+ to that Work or Derivative Works thereof, that is intentionally
51
+ submitted to Licensor for inclusion in the Work by the copyright owner
52
+ or by an individual or Legal Entity authorized to submit on behalf of
53
+ the copyright owner. For the purposes of this definition, "submitted"
54
+ means any form of electronic, verbal, or written communication sent
55
+ to the Licensor or its representatives, including but not limited to
56
+ communication on electronic mailing lists, source code control systems,
57
+ and issue tracking systems that are managed by, or on behalf of, the
58
+ Licensor for the purpose of discussing and improving the Work, but
59
+ excluding communication that is conspicuously marked or otherwise
60
+ designated in writing by the copyright owner as "Not a Contribution."
61
+
62
+ "Contributor" shall mean Licensor and any individual or Legal Entity
63
+ on behalf of whom a Contribution has been received by Licensor and
64
+ subsequently incorporated within the Work.
65
+
66
+ 2. Grant of Copyright License. Subject to the terms and conditions of
67
+ this License, each Contributor hereby grants to You a perpetual,
68
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
69
+ copyright license to reproduce, prepare Derivative Works of,
70
+ publicly display, publicly perform, sublicense, and distribute the
71
+ Work and such Derivative Works in Source or Object form.
72
+
73
+ 3. Grant of Patent License. Subject to the terms and conditions of
74
+ this License, each Contributor hereby grants to You a perpetual,
75
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
76
+ (except as stated in this section) patent license to make, have made,
77
+ use, offer to sell, sell, import, and otherwise transfer the Work,
78
+ where such license applies only to those patent claims licensable
79
+ by such Contributor that are necessarily infringed by their
80
+ Contribution(s) alone or by combination of their Contribution(s)
81
+ with the Work to which such Contribution(s) was submitted. If You
82
+ institute patent litigation against any entity (including a
83
+ cross-claim or counterclaim in a lawsuit) alleging that the Work
84
+ or a Contribution incorporated within the Work constitutes direct
85
+ or contributory patent infringement, then any patent licenses
86
+ granted to You under this License for that Work shall terminate
87
+ as of the date such litigation is filed.
88
+
89
+ 4. Redistribution. You may reproduce and distribute copies of the
90
+ Work or Derivative Works thereof in any medium, with or without
91
+ modifications, and in Source or Object form, provided that You
92
+ meet the following conditions:
93
+
94
+ (a) You must give any other recipients of the Work or
95
+ Derivative Works a copy of this License; and
96
+
97
+ (b) You must cause any modified files to carry prominent notices
98
+ stating that You changed the files; and
99
+
100
+ (c) You must retain, in the Source form of any Derivative Works
101
+ that You distribute, all copyright, patent, trademark, and
102
+ attribution notices from the Source form of the Work,
103
+ excluding those notices that do not pertain to any part of
104
+ the Derivative Works; and
105
+
106
+ (d) If the Work includes a "NOTICE" text file as part of its
107
+ distribution, then any Derivative Works that You distribute must
108
+ include a readable copy of the attribution notices contained
109
+ within such NOTICE file, excluding those notices that do not
110
+ pertain to any part of the Derivative Works, in at least one
111
+ of the following places: within a NOTICE text file distributed
112
+ as part of the Derivative Works; within the Source form or
113
+ documentation, if provided along with the Derivative Works; or,
114
+ within a display generated by the Derivative Works, if and
115
+ wherever such third-party notices normally appear. The contents
116
+ of the NOTICE file are for informational purposes only and
117
+ do not modify the License. You may add Your own attribution
118
+ notices within Derivative Works that You distribute, alongside
119
+ or as an addendum to the NOTICE text from the Work, provided
120
+ that such additional attribution notices cannot be construed
121
+ as modifying the License.
122
+
123
+ You may add Your own copyright statement to Your modifications and
124
+ may provide additional or different license terms and conditions
125
+ for use, reproduction, or distribution of Your modifications, or
126
+ for any such Derivative Works as a whole, provided Your use,
127
+ reproduction, and distribution of the Work otherwise complies with
128
+ the conditions stated in this License.
129
+
130
+ 5. Submission of Contributions. Unless You explicitly state otherwise,
131
+ any Contribution intentionally submitted for inclusion in the Work
132
+ by You to the Licensor shall be under the terms and conditions of
133
+ this License, without any additional terms or conditions.
134
+ Notwithstanding the above, nothing herein shall supersede or modify
135
+ the terms of any separate license agreement you may have executed
136
+ with Licensor regarding such Contributions.
137
+
138
+ 6. Trademarks. This License does not grant permission to use the trade
139
+ names, trademarks, service marks, or product names of the Licensor,
140
+ except as required for reasonable and customary use in describing the
141
+ origin of the Work and reproducing the content of the NOTICE file.
142
+
143
+ 7. Disclaimer of Warranty. Unless required by applicable law or
144
+ agreed to in writing, Licensor provides the Work (and each
145
+ Contributor provides its Contributions) on an "AS IS" BASIS,
146
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
147
+ implied, including, without limitation, any warranties or conditions
148
+ of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
149
+ PARTICULAR PURPOSE. You are solely responsible for determining the
150
+ appropriateness of using or redistributing the Work and assume any
151
+ risks associated with Your exercise of permissions under this License.
152
+
153
+ 8. Limitation of Liability. In no event and under no legal theory,
154
+ whether in tort (including negligence), contract, or otherwise,
155
+ unless required by applicable law (such as deliberate and grossly
156
+ negligent acts) or agreed to in writing, shall any Contributor be
157
+ liable to You for damages, including any direct, indirect, special,
158
+ incidental, or consequential damages of any character arising as a
159
+ result of this License or out of the use or inability to use the
160
+ Work (including but not limited to damages for loss of goodwill,
161
+ work stoppage, computer failure or malfunction, or any and all
162
+ other commercial damages or losses), even if such Contributor
163
+ has been advised of the possibility of such damages.
164
+
165
+ 9. Accepting Warranty or Additional Liability. While redistributing
166
+ the Work or Derivative Works thereof, You may choose to offer,
167
+ and charge a fee for, acceptance of support, warranty, indemnity,
168
+ or other liability obligations and/or rights consistent with this
169
+ License. However, in accepting such obligations, You may act only
170
+ on Your own behalf and on Your sole responsibility, not on behalf
171
+ of any other Contributor, and only if You agree to indemnify,
172
+ defend, and hold each Contributor harmless for any liability
173
+ incurred by, or claims asserted against, such Contributor by reason
174
+ of your accepting any such warranty or additional liability.
175
+
176
+ END OF TERMS AND CONDITIONS
177
+
178
+ APPENDIX: How to apply the Apache License to your work.
179
+
180
+ To apply the Apache License to your work, attach the following
181
+ boilerplate notice, with the fields enclosed by brackets "{}"
182
+ replaced with your own identifying information. (Don't include
183
+ the brackets!) The text should be enclosed in the appropriate
184
+ comment syntax for the file format. We also recommend that a
185
+ file or class name and description of purpose be included on the
186
+ same "printed page" as the copyright notice for easier
187
+ identification within third-party archives.
188
+
189
+ Copyright {yyyy} {name of copyright owner}
190
+
191
+ Licensed under the Apache License, Version 2.0 (the "License");
192
+ you may not use this file except in compliance with the License.
193
+ You may obtain a copy of the License at
194
+
195
+ http://www.apache.org/licenses/LICENSE-2.0
196
+
197
+ Unless required by applicable law or agreed to in writing, software
198
+ distributed under the License is distributed on an "AS IS" BASIS,
199
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
200
+ See the License for the specific language governing permissions and
201
+ limitations under the License.
@@ -0,0 +1,5 @@
1
+ # The compatibility fixture must ship: it is what lets a user verify that their
2
+ # transformers version still produces the same features. Without it,
3
+ # `pytest -m needs_model` silently skips.
4
+ include LICENSE
5
+ recursive-include tests *.py *.npz *.npy *.json
@@ -0,0 +1,137 @@
1
+ Metadata-Version: 2.1
2
+ Name: ssl-kaldi-feats
3
+ Version: 0.1.0
4
+ Summary: Kaldi ark/scp features from any HuggingFace speech encoder, without a Kaldi install
5
+ Author: Ibrahim Almajai
6
+ License: Apache-2.0
7
+ Project-URL: Homepage, https://github.com/ialmajai/ssl-kaldi
8
+ Project-URL: Source, https://github.com/ialmajai/ssl-kaldi/tree/main/packages/ssl-kaldi-feats
9
+ Keywords: speech,kaldi,self-supervised,asr,features,wav2vec2,hubert,whisper
10
+ Classifier: Intended Audience :: Science/Research
11
+ Classifier: License :: OSI Approved :: Apache Software License
12
+ Classifier: Programming Language :: Python :: 3
13
+ Classifier: Programming Language :: Python :: 3.8
14
+ Classifier: Programming Language :: Python :: 3.9
15
+ Classifier: Programming Language :: Python :: 3.10
16
+ Classifier: Programming Language :: Python :: 3.11
17
+ Classifier: Programming Language :: Python :: 3.12
18
+ Classifier: Topic :: Scientific/Engineering
19
+ Requires-Python: >=3.8
20
+ Description-Content-Type: text/markdown
21
+ License-File: LICENSE
22
+ Requires-Dist: numpy<2.0,>=1.22
23
+ Requires-Dist: torch>=2.0
24
+ Requires-Dist: transformers<5,>=4.46.0
25
+ Requires-Dist: kaldiio>=2.18.0
26
+ Provides-Extra: audio
27
+ Requires-Dist: soundfile>=0.12; extra == "audio"
28
+ Provides-Extra: dev
29
+ Requires-Dist: pytest>=7; extra == "dev"
30
+
31
+ # ssl-kaldi-feats
32
+
33
+ Kaldi `ark`/`scp` features from any HuggingFace speech encoder. **No Kaldi
34
+ installation required** - archives are written in pure Python, so the output is
35
+ consumable by Kaldi, ESPnet, or anything else that reads the format, on a
36
+ machine that has never compiled Kaldi.
37
+
38
+ ```bash
39
+ pip install ssl-kaldi-feats
40
+ ```
41
+
42
+ ## Use
43
+
44
+ ```bash
45
+ # single layer, from a Kaldi wav.scp (the "cmd |" form works too)
46
+ ssl-kaldi-feats --model facebook/hubert-base-ls960 --layer 9 \
47
+ --wav-scp data/train/wav.scp --output-dir feats/train
48
+
49
+ # or from a plain list of audio files
50
+ ssl-kaldi-feats --model microsoft/wavlm-large --layer 16 \
51
+ --file-list files.txt --output-dir feats/train
52
+ ```
53
+
54
+ Output is a directory Kaldi will accept as-is:
55
+
56
+ ```
57
+ feats/train/feats.scp feats.ark utt2num_frames utt2dur frame_shift
58
+ ```
59
+
60
+ As a library:
61
+
62
+ ```python
63
+ from ssl_kaldi_feats import SSLExtractor
64
+
65
+ ex = SSLExtractor("facebook/mms-300m", layers=14)
66
+ feats = ex.extract_one(waveform) # (frames, 1024) float32, 20 ms shift
67
+ ```
68
+
69
+ ## Sweeping layers costs one forward pass, not one per layer
70
+
71
+ ```bash
72
+ ssl-kaldi-feats --model facebook/mms-300m --layers 10,12,14,16,18 \
73
+ --wav-scp data/train/wav.scp --output-dir feats/sweep
74
+ # -> feats/sweep/layer10/ layer12/ layer14/ layer16/ layer18/
75
+ ```
76
+
77
+ Probing those five layers separately costs 10+12+14+16+18 = 70
78
+ layer-evaluations. Together it costs 18, plus one model load and one pass over
79
+ the audio instead of five. Measured at **3.9x faster** on a 3696-utterance set.
80
+
81
+ ## The layer_norm gotcha
82
+
83
+ Taking layer *k* by truncating the encoder and reading the last hidden state is
84
+ **not** the same as indexing `hidden_states[k]` on the full model.
85
+ `Wav2Vec2EncoderStableLayerNorm` (the large models: mms-300m, mms-1b,
86
+ wavlm-large, wav2vec2-large, xls-r) and `WhisperEncoder` apply
87
+ `encoder.layer_norm` *after* the layer loop, so truncation moves a norm trained
88
+ for the top layer onto layer *k*. Base-sized models (hubert-base, wavlm-base,
89
+ mHuBERT-147) norm before the loop and are unaffected.
90
+
91
+ Verified exactly on mms-300m: cosine against `layer_norm(L_k)` is 1.000000 at
92
+ layers 8, 14 and 20, while cosine against the raw layer is 0.67 to 0.87.
93
+
94
+ ```bash
95
+ --final-layer-norm keep # default: layer_norm(layer_k)
96
+ --final-layer-norm strip # hidden_states[k], comparable with the literature
97
+ ```
98
+
99
+ `keep` is the default because it measures better as a front end: on TIMIT with
100
+ mms-300m layer 14 it wins at every HMM-GMM stage by 0.67 to 2.21 PER. The norm
101
+ acts as a free per-frame normalisation, which a per-speaker CMVN cannot
102
+ substitute for. It also **changes which layer wins** (14 with the norm, 16
103
+ without), so a layer study run through a misplaced norm is not measuring the
104
+ layer alone. Use `strip` when you need numbers comparable with published layer
105
+ studies.
106
+
107
+ ## Notes
108
+
109
+ - **Frame rate is measured, not assumed.** A short probe pass at load time
110
+ derives seconds-per-frame and writes it to `frame_shift`. Every encoder
111
+ supported here is 20 ms, but hardcoding that would be silently wrong for
112
+ anything else, and a wrong `frame_shift` misaligns everything downstream
113
+ without erroring. Measured rates within 5% of 10, 20 or 40 ms snap to the
114
+ clean value (models typically emit one frame fewer than duration/shift, from
115
+ the convolutional receptive field); anything else is reported with a warning.
116
+ Override with `SSLExtractor(..., frame_shift=0.04)` if you disagree.
117
+ - **Whisper** is handled: the decoder is dropped, and because its feature
118
+ extractor pads every utterance to 30 s the frames beyond the real audio are
119
+ discarded. Utterances over 30 s raise rather than being silently truncated.
120
+ - **Multi-channel input is rejected**, not mangled. A stereo `(n, 2)` array
121
+ survives `np.squeeze` and the HF feature extractor then reads it as a *batch
122
+ of two*, returning wrong features with no error.
123
+ - **Failures abort by default.** A tolerated failure silently shrinks the
124
+ output, which later looks like a smaller test set rather than an error. Raise
125
+ `--max-failures` deliberately.
126
+ - **`soundfile`** is only needed for non-wav or non-PCM16 audio:
127
+ `pip install ssl-kaldi-feats[audio]`.
128
+
129
+ ## Where this comes from
130
+
131
+ Extracted from [ssl-kaldi](https://github.com/ialmajai/ssl-kaldi), a set of
132
+ Kaldi recipes that put frozen self-supervised features in front of a classic
133
+ HMM-GMM and LF-MMI back end. Measured results, including which choices turned
134
+ out not to matter, are in that repo's
135
+ [FINDINGS.md](https://github.com/ialmajai/ssl-kaldi/blob/main/FINDINGS.md).
136
+
137
+ Apache-2.0.
@@ -0,0 +1,107 @@
1
+ # ssl-kaldi-feats
2
+
3
+ Kaldi `ark`/`scp` features from any HuggingFace speech encoder. **No Kaldi
4
+ installation required** - archives are written in pure Python, so the output is
5
+ consumable by Kaldi, ESPnet, or anything else that reads the format, on a
6
+ machine that has never compiled Kaldi.
7
+
8
+ ```bash
9
+ pip install ssl-kaldi-feats
10
+ ```
11
+
12
+ ## Use
13
+
14
+ ```bash
15
+ # single layer, from a Kaldi wav.scp (the "cmd |" form works too)
16
+ ssl-kaldi-feats --model facebook/hubert-base-ls960 --layer 9 \
17
+ --wav-scp data/train/wav.scp --output-dir feats/train
18
+
19
+ # or from a plain list of audio files
20
+ ssl-kaldi-feats --model microsoft/wavlm-large --layer 16 \
21
+ --file-list files.txt --output-dir feats/train
22
+ ```
23
+
24
+ Output is a directory Kaldi will accept as-is:
25
+
26
+ ```
27
+ feats/train/feats.scp feats.ark utt2num_frames utt2dur frame_shift
28
+ ```
29
+
30
+ As a library:
31
+
32
+ ```python
33
+ from ssl_kaldi_feats import SSLExtractor
34
+
35
+ ex = SSLExtractor("facebook/mms-300m", layers=14)
36
+ feats = ex.extract_one(waveform) # (frames, 1024) float32, 20 ms shift
37
+ ```
38
+
39
+ ## Sweeping layers costs one forward pass, not one per layer
40
+
41
+ ```bash
42
+ ssl-kaldi-feats --model facebook/mms-300m --layers 10,12,14,16,18 \
43
+ --wav-scp data/train/wav.scp --output-dir feats/sweep
44
+ # -> feats/sweep/layer10/ layer12/ layer14/ layer16/ layer18/
45
+ ```
46
+
47
+ Probing those five layers separately costs 10+12+14+16+18 = 70
48
+ layer-evaluations. Together it costs 18, plus one model load and one pass over
49
+ the audio instead of five. Measured at **3.9x faster** on a 3696-utterance set.
50
+
51
+ ## The layer_norm gotcha
52
+
53
+ Taking layer *k* by truncating the encoder and reading the last hidden state is
54
+ **not** the same as indexing `hidden_states[k]` on the full model.
55
+ `Wav2Vec2EncoderStableLayerNorm` (the large models: mms-300m, mms-1b,
56
+ wavlm-large, wav2vec2-large, xls-r) and `WhisperEncoder` apply
57
+ `encoder.layer_norm` *after* the layer loop, so truncation moves a norm trained
58
+ for the top layer onto layer *k*. Base-sized models (hubert-base, wavlm-base,
59
+ mHuBERT-147) norm before the loop and are unaffected.
60
+
61
+ Verified exactly on mms-300m: cosine against `layer_norm(L_k)` is 1.000000 at
62
+ layers 8, 14 and 20, while cosine against the raw layer is 0.67 to 0.87.
63
+
64
+ ```bash
65
+ --final-layer-norm keep # default: layer_norm(layer_k)
66
+ --final-layer-norm strip # hidden_states[k], comparable with the literature
67
+ ```
68
+
69
+ `keep` is the default because it measures better as a front end: on TIMIT with
70
+ mms-300m layer 14 it wins at every HMM-GMM stage by 0.67 to 2.21 PER. The norm
71
+ acts as a free per-frame normalisation, which a per-speaker CMVN cannot
72
+ substitute for. It also **changes which layer wins** (14 with the norm, 16
73
+ without), so a layer study run through a misplaced norm is not measuring the
74
+ layer alone. Use `strip` when you need numbers comparable with published layer
75
+ studies.
76
+
77
+ ## Notes
78
+
79
+ - **Frame rate is measured, not assumed.** A short probe pass at load time
80
+ derives seconds-per-frame and writes it to `frame_shift`. Every encoder
81
+ supported here is 20 ms, but hardcoding that would be silently wrong for
82
+ anything else, and a wrong `frame_shift` misaligns everything downstream
83
+ without erroring. Measured rates within 5% of 10, 20 or 40 ms snap to the
84
+ clean value (models typically emit one frame fewer than duration/shift, from
85
+ the convolutional receptive field); anything else is reported with a warning.
86
+ Override with `SSLExtractor(..., frame_shift=0.04)` if you disagree.
87
+ - **Whisper** is handled: the decoder is dropped, and because its feature
88
+ extractor pads every utterance to 30 s the frames beyond the real audio are
89
+ discarded. Utterances over 30 s raise rather than being silently truncated.
90
+ - **Multi-channel input is rejected**, not mangled. A stereo `(n, 2)` array
91
+ survives `np.squeeze` and the HF feature extractor then reads it as a *batch
92
+ of two*, returning wrong features with no error.
93
+ - **Failures abort by default.** A tolerated failure silently shrinks the
94
+ output, which later looks like a smaller test set rather than an error. Raise
95
+ `--max-failures` deliberately.
96
+ - **`soundfile`** is only needed for non-wav or non-PCM16 audio:
97
+ `pip install ssl-kaldi-feats[audio]`.
98
+
99
+ ## Where this comes from
100
+
101
+ Extracted from [ssl-kaldi](https://github.com/ialmajai/ssl-kaldi), a set of
102
+ Kaldi recipes that put frozen self-supervised features in front of a classic
103
+ HMM-GMM and LF-MMI back end. Measured results, including which choices turned
104
+ out not to matter, are in that repo's
105
+ [FINDINGS.md](https://github.com/ialmajai/ssl-kaldi/blob/main/FINDINGS.md).
106
+
107
+ Apache-2.0.
@@ -0,0 +1,61 @@
1
+ [build-system]
2
+ requires = ["setuptools>=61"]
3
+ build-backend = "setuptools.build_meta"
4
+
5
+ [project]
6
+ name = "ssl-kaldi-feats"
7
+ version = "0.1.0"
8
+ description = "Kaldi ark/scp features from any HuggingFace speech encoder, without a Kaldi install"
9
+ readme = "README.md"
10
+ # 3.8 because that is the interpreter the ssl-kaldi recipes run on.
11
+ # The code uses no 3.9+ syntax. Bump to >=3.9 (and switch the CLI
12
+ # boolean flags to argparse.BooleanOptionalAction) when that env moves
13
+ # to the 3.11 the top-level README already recommends.
14
+ requires-python = ">=3.8"
15
+ license = {text = "Apache-2.0"}
16
+ authors = [{name = "Ibrahim Almajai"}]
17
+ keywords = ["speech", "kaldi", "self-supervised", "asr", "features", "wav2vec2", "hubert", "whisper"]
18
+ classifiers = [
19
+ "Intended Audience :: Science/Research",
20
+ "License :: OSI Approved :: Apache Software License",
21
+ "Programming Language :: Python :: 3",
22
+ "Programming Language :: Python :: 3.8",
23
+ "Programming Language :: Python :: 3.9",
24
+ "Programming Language :: Python :: 3.10",
25
+ "Programming Language :: Python :: 3.11",
26
+ "Programming Language :: Python :: 3.12",
27
+ "Topic :: Scientific/Engineering",
28
+ ]
29
+ dependencies = [
30
+ "numpy>=1.22,<2.0",
31
+ "torch>=2.0",
32
+ # Upper bound is deliberate. This package truncates model.encoder.layers,
33
+ # reads config.do_stable_layer_norm and swaps encoder.layer_norm for
34
+ # Identity: internals, not public API. A major release can change any of
35
+ # them, and the failure mode is silently different features rather than an
36
+ # exception. 5.x is untested, not known broken. To lift the bound, run
37
+ # pytest -m needs_model tests/
38
+ # against 5.x and relax it if the fixture still reproduces.
39
+ "transformers>=4.46.0,<5",
40
+ "kaldiio>=2.18.0",
41
+ ]
42
+
43
+ [project.optional-dependencies]
44
+ audio = ["soundfile>=0.12"]
45
+ dev = ["pytest>=7"]
46
+
47
+ [project.urls]
48
+ Homepage = "https://github.com/ialmajai/ssl-kaldi"
49
+ Source = "https://github.com/ialmajai/ssl-kaldi/tree/main/packages/ssl-kaldi-feats"
50
+
51
+ [project.scripts]
52
+ ssl-kaldi-feats = "ssl_kaldi_feats.cli:main"
53
+
54
+ [tool.setuptools.packages.find]
55
+ where = ["src"]
56
+
57
+ [tool.pytest.ini_options]
58
+ markers = [
59
+ "needs_model: downloads a model from HuggingFace; opt in with -m needs_model",
60
+ ]
61
+ addopts = "-m 'not needs_model'"
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -0,0 +1,18 @@
1
+ """Kaldi ark/scp features from any HuggingFace speech encoder."""
2
+ from .extractor import (
3
+ EXPECTED_SAMPLE_RATE,
4
+ WHISPER_SAMPLES_PER_FRAME,
5
+ WHISPER_WINDOW_SECONDS,
6
+ SSLExtractor,
7
+ preprocess_waveform,
8
+ )
9
+
10
+ __version__ = "0.1.0"
11
+ __all__ = [
12
+ "SSLExtractor",
13
+ "preprocess_waveform",
14
+ "EXPECTED_SAMPLE_RATE",
15
+ "WHISPER_WINDOW_SECONDS",
16
+ "WHISPER_SAMPLES_PER_FRAME",
17
+ "__version__",
18
+ ]
@@ -0,0 +1,128 @@
1
+ """Command line entry point."""
2
+ import argparse
3
+ import logging
4
+ import os
5
+ import sys
6
+
7
+ from kaldiio import WriteHelper
8
+
9
+ from .extractor import SSLExtractor
10
+ from .io import read_file_list, read_wav_scp, write_side_files
11
+
12
+ logger = logging.getLogger("ssl_kaldi_feats")
13
+ logger.setLevel(logging.INFO)
14
+
15
+
16
+ def build_parser():
17
+ p = argparse.ArgumentParser(
18
+ prog="ssl-kaldi-feats",
19
+ description="Kaldi ark/scp features from any HuggingFace speech encoder. "
20
+ "No Kaldi installation required.",
21
+ formatter_class=argparse.ArgumentDefaultsHelpFormatter,
22
+ )
23
+ p.add_argument("--model", required=True,
24
+ help="HuggingFace model id, e.g. facebook/hubert-base-ls960")
25
+ g = p.add_mutually_exclusive_group(required=True)
26
+ g.add_argument("--layer", type=int, help="single layer to extract, 1-indexed")
27
+ g.add_argument("--layers",
28
+ help="comma-separated layers from ONE forward pass, e.g. "
29
+ "'10,12,14'. Costs one pass to the deepest layer "
30
+ "instead of one pass each")
31
+ src = p.add_mutually_exclusive_group(required=True)
32
+ src.add_argument("--wav-scp", help="Kaldi wav.scp, including the 'cmd |' form")
33
+ src.add_argument("--file-list", help="text file of audio paths, one per line")
34
+ p.add_argument("--output-dir", required=True,
35
+ help="written as feats.scp/feats.ark plus utt2num_frames, "
36
+ "utt2dur and frame_shift. With --layers, one "
37
+ "subdirectory per layer")
38
+ p.add_argument("--final-layer-norm", choices=["keep", "strip"], default="keep",
39
+ help="'keep' returns layer_norm(layer_k); 'strip' returns "
40
+ "hidden_states[k], which is what published layer studies "
41
+ "mean. Only affects models that norm after the layer "
42
+ "loop (the large ones, and Whisper)")
43
+ p.add_argument("--device", default=None, help="torch device (default: cuda if available)")
44
+ # argparse.BooleanOptionalAction would be tidier but is 3.9+, and this
45
+ # package supports 3.8. Both halves carry help text so neither reads as an
46
+ # accident.
47
+ p.add_argument("--compress", dest="compress", action="store_true",
48
+ default=True,
49
+ help="write compressed archives, Kaldi kSpeechFeature, the "
50
+ "same codec as copy-feats --compress=true (the default)")
51
+ p.add_argument("--no-compress", dest="compress", action="store_false",
52
+ help="write uncompressed archives. Compression is lossy, so "
53
+ "use this when comparing archives bit-for-bit")
54
+ p.add_argument("--max-failures", type=int, default=0,
55
+ help="how many utterances may fail before aborting. A "
56
+ "tolerated failure silently shrinks the output")
57
+ p.add_argument("--quiet", action="store_true")
58
+ return p
59
+
60
+
61
+ def main(argv=None):
62
+ args = build_parser().parse_args(argv)
63
+ logging.basicConfig(
64
+ level=logging.WARNING if args.quiet else logging.INFO,
65
+ format="%(asctime)s %(levelname)s %(message)s",
66
+ )
67
+
68
+ layers = ([int(x) for x in args.layers.split(",")] if args.layers
69
+ else args.layer)
70
+ extractor = SSLExtractor(args.model, layers, args.final_layer_norm, args.device)
71
+ multi = len(extractor.layers) > 1
72
+
73
+ dirs = {}
74
+ for layer in extractor.layers:
75
+ d = os.path.join(args.output_dir, f"layer{layer}") if multi else args.output_dir
76
+ os.makedirs(d, exist_ok=True)
77
+ dirs[layer] = d
78
+
79
+ reader = (read_wav_scp(args.wav_scp) if args.wav_scp
80
+ else read_file_list(args.file_list))
81
+
82
+ comp = 2 if args.compress else None
83
+ writers = {}
84
+ for layer, d in dirs.items():
85
+ spec = f"ark,scp:{os.path.abspath(d)}/feats.ark,{os.path.abspath(d)}/feats.scp"
86
+ writers[layer] = WriteHelper(spec, compression_method=comp)
87
+
88
+ utt2num_frames, utt2dur, failed = {}, {}, []
89
+ processed = 0
90
+ try:
91
+ for utt_id, wav, sr in reader:
92
+ try:
93
+ feats = extractor.extract(wav, sr)
94
+ for layer, w in writers.items():
95
+ w(utt_id, feats[layer])
96
+ first = feats[extractor.layers[0]]
97
+ utt2num_frames[utt_id] = len(first)
98
+ utt2dur[utt_id] = len(wav) / float(sr)
99
+ processed += 1
100
+ if processed % 100 == 0:
101
+ logger.info("processed %d utterances", processed)
102
+ except Exception as e: # noqa: BLE001
103
+ logger.error("failed on %s: %s", utt_id, e)
104
+ failed.append(utt_id)
105
+ if len(failed) > args.max_failures:
106
+ raise SystemExit(
107
+ f"aborting after {len(failed)} failure(s), "
108
+ f"--max-failures is {args.max_failures}. "
109
+ f"Failed: {' '.join(failed)}"
110
+ )
111
+ finally:
112
+ for w in writers.values():
113
+ w.close()
114
+
115
+ if processed == 0:
116
+ raise SystemExit("no utterances were processed")
117
+
118
+ for layer, d in dirs.items():
119
+ write_side_files(d, utt2num_frames, utt2dur, extractor.frame_shift)
120
+
121
+ logger.info("wrote %d utterances to %s", processed, args.output_dir)
122
+ if failed:
123
+ logger.warning("tolerated %d failure(s): %s", len(failed), " ".join(failed))
124
+ return 0
125
+
126
+
127
+ if __name__ == "__main__":
128
+ sys.exit(main())