keywordtensor-edge 1.0.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.
- keywordtensor_edge-1.0.0/LICENSE +21 -0
- keywordtensor_edge-1.0.0/PKG-INFO +15 -0
- keywordtensor_edge-1.0.0/README.md +141 -0
- keywordtensor_edge-1.0.0/keywordtensor/__init__.py +1 -0
- keywordtensor_edge-1.0.0/keywordtensor/core.py +260 -0
- keywordtensor_edge-1.0.0/keywordtensor/pretrained/__init__.py +0 -0
- keywordtensor_edge-1.0.0/keywordtensor/pretrained/prawda_falsz.onnx +0 -0
- keywordtensor_edge-1.0.0/keywordtensor/pretrained/prawda_falsz_config.json +11 -0
- keywordtensor_edge-1.0.0/keywordtensor_edge.egg-info/PKG-INFO +15 -0
- keywordtensor_edge-1.0.0/keywordtensor_edge.egg-info/SOURCES.txt +13 -0
- keywordtensor_edge-1.0.0/keywordtensor_edge.egg-info/dependency_links.txt +1 -0
- keywordtensor_edge-1.0.0/keywordtensor_edge.egg-info/requires.txt +8 -0
- keywordtensor_edge-1.0.0/keywordtensor_edge.egg-info/top_level.txt +1 -0
- keywordtensor_edge-1.0.0/pyproject.toml +25 -0
- keywordtensor_edge-1.0.0/setup.cfg +4 -0
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Fabian Kondela
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: keywordtensor-edge
|
|
3
|
+
Version: 1.0.0
|
|
4
|
+
Summary: A lightweight inference library for training custom keyword spotting models and running real-time voice command detection.
|
|
5
|
+
License: MIT
|
|
6
|
+
License-File: LICENSE
|
|
7
|
+
Requires-Dist: numpy
|
|
8
|
+
Requires-Dist: sounddevice
|
|
9
|
+
Requires-Dist: onnxruntime
|
|
10
|
+
Requires-Dist: torch
|
|
11
|
+
Requires-Dist: torchaudio
|
|
12
|
+
Requires-Dist: torch-audiomentations
|
|
13
|
+
Requires-Dist: torchcodec
|
|
14
|
+
Requires-Dist: onnx
|
|
15
|
+
Dynamic: license-file
|
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
<div align="center">
|
|
2
|
+
<img src="assets/logo.png" width="300" alt="KeywordTensor Logo">
|
|
3
|
+
<br>
|
|
4
|
+
<p><strong>A Python library for training custom keyword spotting models and running real-time voice command detection.</strong></p>
|
|
5
|
+
|
|
6
|
+
[](https://pypi.org/project/keywordtensor/)
|
|
7
|
+
[](https://www.python.org)
|
|
8
|
+
[](LICENSE)
|
|
9
|
+
</div>
|
|
10
|
+
|
|
11
|
+
---
|
|
12
|
+
|
|
13
|
+
## ⚡ About KeywordTensor
|
|
14
|
+
KeywordTensor is built for developers who want to integrate voice commands into their Python projects without requiring deep knowledge of audio processing.
|
|
15
|
+
|
|
16
|
+
- **Bring your own `.wav` files**: Just put your audio files into folders (e.g., `dataset/hello/`, `dataset/stop/`).
|
|
17
|
+
- **Trigger custom Python actions**: Easily map recognized words directly to your own Python functions. No Speech-to-Text required—KeywordTensor detects predefined commands and directly triggers Python callbacks.
|
|
18
|
+
- **Automated Export & Config**: Training automatically generates your optimized model and its configuration file. This allows you to launch live inference with a single command later. No manual saving required!
|
|
19
|
+
- **Built-in Audio Augmentation**: We automatically mutate your `.wav` files during training (PitchShift, Gain & Polarity Inversion, Colored Noise) to improve robustness in noisy environments.
|
|
20
|
+
- **SpecAugment Pipeline**: Raw audio is converted to Mel-spectrograms with Time and Frequency Masking applied. The model learns to recognize commands even if the microphone crackles or the audio drops out.
|
|
21
|
+
- **Continuous Listening**: A rolling buffer averages predictions over time to prevent sudden false positive clicks.
|
|
22
|
+
- **Full Control**: We hide the complexity by default, but give you full access to all deep learning training and listening parameters (training configuration, validation settings, and inference thresholds).
|
|
23
|
+
|
|
24
|
+
---
|
|
25
|
+
|
|
26
|
+
## 📦 Pre-trained Models
|
|
27
|
+
Don't have time to record your own dataset? You can use our ready-to-go models.
|
|
28
|
+
|
|
29
|
+
- **`prawda_falsz`**
|
|
30
|
+
[](#)
|
|
31
|
+
|
|
32
|
+
A highly robust model trained specifically to handle high-pitched children's voices and extremely noisy environments. This model was successfully deployed in a live public demonstration during the *"Noc Naukowców"* (Researchers' Night) event.
|
|
33
|
+
- **More models coming soon!**
|
|
34
|
+
|
|
35
|
+
---
|
|
36
|
+
|
|
37
|
+
## 💻 Quick Start & API
|
|
38
|
+
|
|
39
|
+
### 1. Installation (Choose your variant)
|
|
40
|
+
The library is available in two variants on PyPI depending on your needs:
|
|
41
|
+
|
|
42
|
+
- **`pip install keywordtensor`**
|
|
43
|
+
Installs the full training environment. Use this on your PC or Server to train your models.
|
|
44
|
+
|
|
45
|
+
- **`pip install keywordtensor-edge`**
|
|
46
|
+
A lightweight runtime variant. It strips out heavy training dependencies (like `fastai`), providing only what is needed for real-time inference (`listen()`). Perfect for Raspberry Pi or IoT devices.
|
|
47
|
+
|
|
48
|
+
---
|
|
49
|
+
|
|
50
|
+
### 2. Training your model
|
|
51
|
+
The `.train()` method takes your audio files and trains a neural network using PyTorch and FastAI under the hood.
|
|
52
|
+
|
|
53
|
+
```python
|
|
54
|
+
import keywordtensor
|
|
55
|
+
|
|
56
|
+
model = keywordtensor.Engine()
|
|
57
|
+
|
|
58
|
+
# The engine automatically applies audio & spectrogram augmentations during training
|
|
59
|
+
model.train(
|
|
60
|
+
dataset_path="path/to/audio/dataset",
|
|
61
|
+
model_name="my_custom_model",
|
|
62
|
+
epochs=30,
|
|
63
|
+
batch_size=32
|
|
64
|
+
)
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
**Training parameters:**
|
|
68
|
+
You have total control over the pipeline. Available parameters in `.train()`:
|
|
69
|
+
- `dataset_path` *(required)*: Path to your dataset folder. Any number of folders (classes) is supported.
|
|
70
|
+
- `model_name` *(default: 'myownmodel')*: Name of the final exported model.
|
|
71
|
+
- `epochs` *(default: 30)*: Number of training cycles over your dataset.
|
|
72
|
+
- `batch_size` *(default: 32)*: Number of audio samples processed simultaneously.
|
|
73
|
+
- `learning_rate` *(Automatic)*: The engine dynamically searches for the optimal learning rate for your specific dataset and automatically applies the One-Cycle Policy.
|
|
74
|
+
- `wd` *(default: 0.01)*: Weight decay (L2 penalty) to prevent overfitting.
|
|
75
|
+
- `eps` *(default: 0.01)*: Label smoothing epsilon to improve generalization.
|
|
76
|
+
- `valid_pct` *(default: 0.1)*: Percentage of data reserved for validation.
|
|
77
|
+
- `duration` *(default: 3.0)*: The exact duration of your audio clips in seconds. If an audio clip is shorter, it will be automatically padded with zeros (silence). If it is longer, it will be accurately truncated to match this length.
|
|
78
|
+
- `sr` *(default: 16000)*: Sample rate of your audio files.
|
|
79
|
+
|
|
80
|
+
---
|
|
81
|
+
|
|
82
|
+
### 3. Live Inference & Custom Actions
|
|
83
|
+
Once trained (or using a pre-trained model like `prawda_falsz`), you can run real-time inference using your microphone.
|
|
84
|
+
|
|
85
|
+
```python
|
|
86
|
+
import keywordtensor as kt
|
|
87
|
+
|
|
88
|
+
model = kt.Engine()
|
|
89
|
+
|
|
90
|
+
# Define your custom actions
|
|
91
|
+
def on_hello():
|
|
92
|
+
print("Action triggered: 'Hello' detected!")
|
|
93
|
+
|
|
94
|
+
def on_stop():
|
|
95
|
+
print("Action triggered: Stopping the robot!")
|
|
96
|
+
|
|
97
|
+
# Map keywords to your Python functions with per-keyword cooldowns
|
|
98
|
+
model.listen(
|
|
99
|
+
model_name="my_custom_model",
|
|
100
|
+
actions={
|
|
101
|
+
"hello": {"funkcja": on_hello, "cooldown": 2.0},
|
|
102
|
+
"stop": {"funkcja": on_stop, "cooldown": 3.0}
|
|
103
|
+
},
|
|
104
|
+
min_confidence=0.6,
|
|
105
|
+
n_averages=3
|
|
106
|
+
)
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
**Listen parameters:**
|
|
110
|
+
The `.listen()` method itself accepts the following runtime arguments:
|
|
111
|
+
- `model_name` *(required)*: The name of the model to load. You can provide the path to your own trained model, or use the built-in `"prawda_falsz"` model which is highly robust to noise and pitched voices.
|
|
112
|
+
- `actions` *(default: None)*: Optional dictionary mapping detected keywords to Python callbacks. You can pass just a function (defaults to 0.0s cooldown), or a dictionary for precise control: `{"funkcja": your_function, "cooldown": 2.0}`. Cooldowns are tracked individually per keyword!
|
|
113
|
+
- `min_confidence` *(default: 0.6)*: The probability threshold (0.0 to 1.0) required to trigger the action.
|
|
114
|
+
- `n_averages` *(default: 3)*: Temporal smoothing. Averages the last *N* predictions to prevent false positive clicks.
|
|
115
|
+
|
|
116
|
+
**Config file parameters:**
|
|
117
|
+
The rest of the underlying parameters are loaded automatically from the `<model_name>_config.json` file!
|
|
118
|
+
When you run `.train()`, this file is automatically generated for you. It looks like this:
|
|
119
|
+
|
|
120
|
+
```json
|
|
121
|
+
{
|
|
122
|
+
"labels": ["hello", "stop"],
|
|
123
|
+
"mean": -40.15,
|
|
124
|
+
"std": 17.35,
|
|
125
|
+
"duration": 3.0,
|
|
126
|
+
"sr": 16000
|
|
127
|
+
}
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
This file dictates the rules for the inference engine:
|
|
131
|
+
- `labels`: The list of keywords the model was trained on.
|
|
132
|
+
- `duration`: The size of the rolling audio buffer in seconds.
|
|
133
|
+
- `sr`: The microphone sample rate.
|
|
134
|
+
- `mean` / `std`: Normalization statistics for the Mel-spectrogram.
|
|
135
|
+
|
|
136
|
+
> 💡 **Total Flexibility:**
|
|
137
|
+
> Want to adjust the microphone sample rate or buffer duration without retraining? Just open the JSON file and edit it!
|
|
138
|
+
>
|
|
139
|
+
> **Bringing your own model?** No problem! If you trained an ONNX model entirely outside of KeywordTensor, simply drop it into your folder, create a matching `your_model_config.json` file next to it with the parameters above, and the `.listen()` method will load and run your external model.
|
|
140
|
+
|
|
141
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
from .core import Engine
|
|
@@ -0,0 +1,260 @@
|
|
|
1
|
+
import json
|
|
2
|
+
import torch
|
|
3
|
+
import torchaudio
|
|
4
|
+
import torchaudio.transforms as T
|
|
5
|
+
import onnxruntime as ort
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
import os
|
|
8
|
+
import time
|
|
9
|
+
from collections import deque
|
|
10
|
+
import numpy as np
|
|
11
|
+
import sounddevice as sd
|
|
12
|
+
#klasy niezbedne do transformacji danych audio i spektrogramów
|
|
13
|
+
|
|
14
|
+
try:
|
|
15
|
+
from fastai.vision.all import *
|
|
16
|
+
from fastcore.foundation import L
|
|
17
|
+
import itertools
|
|
18
|
+
if not hasattr(L, 'starmap'):
|
|
19
|
+
L.starmap = lambda self, f: L(itertools.starmap(f, self))
|
|
20
|
+
from torch_audiomentations import Compose, Shift, Gain, PolarityInversion, AddColoredNoise, PitchShift, HighPassFilter, LowPassFilter
|
|
21
|
+
HAS_FASTAI = True
|
|
22
|
+
except ImportError:
|
|
23
|
+
HAS_FASTAI = False
|
|
24
|
+
class Transform:
|
|
25
|
+
pass
|
|
26
|
+
class TensorImage:
|
|
27
|
+
def __new__(cls, x, *args, **kwargs):
|
|
28
|
+
return x
|
|
29
|
+
def show_image(*args, **kwargs):
|
|
30
|
+
pass
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
#zamiana pliku na falę dźwiękową
|
|
34
|
+
class LoadAudio(Transform):
|
|
35
|
+
def __init__(self, duration=3.0):
|
|
36
|
+
self.duration = duration
|
|
37
|
+
|
|
38
|
+
def encodes(self, file: Path):
|
|
39
|
+
waveform, sr = torchaudio.load(file)
|
|
40
|
+
target_length = int(self.duration * sr)
|
|
41
|
+
if waveform.shape[1] > target_length:
|
|
42
|
+
waveform = waveform[:, :target_length]
|
|
43
|
+
elif waveform.shape[1] < target_length:
|
|
44
|
+
pad_amount = target_length - waveform.shape[1]
|
|
45
|
+
waveform = torch.nn.functional.pad(waveform, (0, pad_amount))
|
|
46
|
+
return waveform
|
|
47
|
+
|
|
48
|
+
#definicja wyświetlenia spektrogramu
|
|
49
|
+
class AudioSpectrogram(TensorImage):
|
|
50
|
+
def show(self, ctx=None, **kwargs):
|
|
51
|
+
return show_image(self, ctx=ctx, cmap='magma', **kwargs)
|
|
52
|
+
|
|
53
|
+
#definicja zamiany fali dźwiękowej na spektrogram
|
|
54
|
+
class WaveformToSpectrogram(Transform):
|
|
55
|
+
def __init__(self, sr=16000):
|
|
56
|
+
self.melspec = T.MelSpectrogram(sample_rate=sr, n_mels=128, n_fft=1024, hop_length=128)
|
|
57
|
+
self.todb = T.AmplitudeToDB(top_db=80.0)
|
|
58
|
+
|
|
59
|
+
def encodes(self, waveform):
|
|
60
|
+
spec = self.todb(self.melspec(waveform))
|
|
61
|
+
return AudioSpectrogram(spec)
|
|
62
|
+
|
|
63
|
+
#definicja augmentacji audio
|
|
64
|
+
class AudioAugment(Transform):
|
|
65
|
+
split_idx = 0
|
|
66
|
+
|
|
67
|
+
def __init__(self, sr=16000):
|
|
68
|
+
self.sr = sr
|
|
69
|
+
|
|
70
|
+
self.audio_augcompose = Compose([
|
|
71
|
+
Shift(min_shift=-0.1, max_shift=0.1, sample_rate=self.sr, p=0.4, rollover=False, output_type="dict"),
|
|
72
|
+
Gain(min_gain_in_db=-15.0, max_gain_in_db=15.0, p=0.6, output_type="dict"),
|
|
73
|
+
PolarityInversion(p=0.5, output_type="dict"),
|
|
74
|
+
AddColoredNoise(min_snr_in_db=20.0, max_snr_in_db=35.0, p=0.6, output_type="dict"),
|
|
75
|
+
PitchShift(min_transpose_semitones=-8, max_transpose_semitones=8, sample_rate=self.sr, p=0.6, output_type="dict"),
|
|
76
|
+
HighPassFilter(min_cutoff_freq=50, max_cutoff_freq=300, sample_rate=self.sr, p=0.3, output_type="dict"),
|
|
77
|
+
LowPassFilter(min_cutoff_freq=5000, max_cutoff_freq=7000, sample_rate=self.sr, p=0.3, output_type="dict"),
|
|
78
|
+
], output_type="dict")
|
|
79
|
+
|
|
80
|
+
def encodes(self, waveform):
|
|
81
|
+
audio_input = waveform.unsqueeze(0)
|
|
82
|
+
augment_dict = self.audio_augcompose(audio_input, sample_rate=self.sr)
|
|
83
|
+
audio_output = augment_dict.samples
|
|
84
|
+
return audio_output.squeeze(0)
|
|
85
|
+
|
|
86
|
+
#defdinicja augmentacji spektrogramu
|
|
87
|
+
class SpecAugment(Transform):
|
|
88
|
+
split_idx = 0
|
|
89
|
+
def __init__(self):
|
|
90
|
+
self.tmask = T.TimeMasking(time_mask_param=4)
|
|
91
|
+
self.fmask = T.FrequencyMasking(freq_mask_param=4)
|
|
92
|
+
|
|
93
|
+
def encodes(self, spec: AudioSpectrogram):
|
|
94
|
+
return self.fmask(self.tmask(spec))
|
|
95
|
+
|
|
96
|
+
#definicja normalizacji spektrogramu
|
|
97
|
+
class NormalizeSpec(Transform):
|
|
98
|
+
def __init__(self, mean=0.0, std=1.0):
|
|
99
|
+
self.mean = mean
|
|
100
|
+
self.std = std
|
|
101
|
+
|
|
102
|
+
def setups(self, items):
|
|
103
|
+
sum_x, sum_x2, n = 0.0, 0.0, 0
|
|
104
|
+
for x in items:
|
|
105
|
+
sum_x += x.sum().item()
|
|
106
|
+
sum_x2 += (x**2).sum().item()
|
|
107
|
+
n += x.numel()
|
|
108
|
+
self.mean = sum_x / n
|
|
109
|
+
self.std = (sum_x2 / n - self.mean**2)**0.5
|
|
110
|
+
|
|
111
|
+
def encodes(self, spec: AudioSpectrogram):
|
|
112
|
+
normalized = (spec - self.mean) / self.std
|
|
113
|
+
return AudioSpectrogram(normalized)
|
|
114
|
+
|
|
115
|
+
#def decodes(self, spec: AudioSpectrogram):
|
|
116
|
+
#denormalized = (spec * self.std) + self.mean
|
|
117
|
+
#return AudioSpectrogram(denormalized)
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
#klasa gdy ktos chce trenowac swoj wlasny model, a nie korzystac z wbudowanych wytrenowanych przykladow
|
|
121
|
+
class Engine:
|
|
122
|
+
def __init__(self):
|
|
123
|
+
self.model_name = None
|
|
124
|
+
|
|
125
|
+
def train(self, dataset_path, epochs=30, batch_size=32, wd=0.01, eps=0.01, valid_pct=0.1, model_name='myownmodel', duration=3.0, sr=16000):
|
|
126
|
+
if not HAS_FASTAI:
|
|
127
|
+
raise RuntimeError("Training requires fastai. Install the full package: pip install keywordtensor")
|
|
128
|
+
|
|
129
|
+
self.model_name = model_name
|
|
130
|
+
|
|
131
|
+
get_audio_files = partial(get_files, extensions=['.wav'])
|
|
132
|
+
files = get_audio_files(Path(dataset_path))
|
|
133
|
+
splits = RandomSplitter(valid_pct=valid_pct, seed=42)(files)
|
|
134
|
+
|
|
135
|
+
norm_spec = NormalizeSpec()
|
|
136
|
+
|
|
137
|
+
tfms = [
|
|
138
|
+
[LoadAudio(duration=duration), AudioAugment(sr=sr), WaveformToSpectrogram(sr=sr), norm_spec, SpecAugment()],
|
|
139
|
+
[parent_label, Categorize()]
|
|
140
|
+
]
|
|
141
|
+
dsets = Datasets(files, tfms, splits=splits)
|
|
142
|
+
dls = dsets.dataloaders(bs=batch_size)
|
|
143
|
+
|
|
144
|
+
model = xresnet18(c_in=1, n_out=len(dls.vocab), pretrained=False)
|
|
145
|
+
learn = Learner(dls, model, wd=wd, metrics=accuracy, loss_func=LabelSmoothingCrossEntropy(eps=eps))
|
|
146
|
+
|
|
147
|
+
res = learn.lr_find()
|
|
148
|
+
base_lr = res.valley
|
|
149
|
+
learn.fit_one_cycle(epochs, lr_max=slice(base_lr/10, base_lr))
|
|
150
|
+
|
|
151
|
+
config = {
|
|
152
|
+
"labels": list(dls.vocab),
|
|
153
|
+
"mean": float(norm_spec.mean),
|
|
154
|
+
"std": float(norm_spec.std),
|
|
155
|
+
"duration": float(duration),
|
|
156
|
+
"sr": int(sr)
|
|
157
|
+
}
|
|
158
|
+
with open(f"{model_name}_config.json", "w", encoding="utf-8") as f:
|
|
159
|
+
json.dump(config, f, ensure_ascii=False, indent=4)
|
|
160
|
+
|
|
161
|
+
x, y = learn.dls.one_batch()
|
|
162
|
+
dummy_input = x[0].unsqueeze(0).cpu()
|
|
163
|
+
|
|
164
|
+
model = learn.model.cpu()
|
|
165
|
+
model.eval()
|
|
166
|
+
|
|
167
|
+
torch.onnx.export(
|
|
168
|
+
model,
|
|
169
|
+
dummy_input,
|
|
170
|
+
f"{model_name}.onnx",
|
|
171
|
+
input_names=['input'],
|
|
172
|
+
output_names=['output'],
|
|
173
|
+
opset_version=12,
|
|
174
|
+
dynamo=False
|
|
175
|
+
)
|
|
176
|
+
|
|
177
|
+
|
|
178
|
+
def listen(self, model_name, actions=None, min_confidence=0.6, n_averages=3):
|
|
179
|
+
if actions is None:
|
|
180
|
+
actions = {}
|
|
181
|
+
|
|
182
|
+
user_model_path = Path(f"{model_name}.onnx")
|
|
183
|
+
user_config_path = Path(f"{model_name}_config.json")
|
|
184
|
+
|
|
185
|
+
library_dir = os.path.dirname(os.path.abspath(__file__))
|
|
186
|
+
builtin_base_path = os.path.join(library_dir, "pretrained", model_name)
|
|
187
|
+
builtin_model_path = Path(f"{builtin_base_path}.onnx")
|
|
188
|
+
builtin_config_path = Path(f"{builtin_base_path}_config.json")
|
|
189
|
+
|
|
190
|
+
if user_model_path.exists() and user_config_path.exists():
|
|
191
|
+
resolved_path = model_name
|
|
192
|
+
elif builtin_model_path.exists() and builtin_config_path.exists():
|
|
193
|
+
resolved_path = builtin_base_path
|
|
194
|
+
|
|
195
|
+
with open(f"{resolved_path}_config.json", "r", encoding="utf-8") as f:
|
|
196
|
+
cfg = json.load(f)
|
|
197
|
+
|
|
198
|
+
labels = cfg["labels"]
|
|
199
|
+
sr = cfg["sr"]
|
|
200
|
+
duration = cfg["duration"]
|
|
201
|
+
|
|
202
|
+
wav_to_spec = WaveformToSpectrogram(sr=sr)
|
|
203
|
+
normalize_spec = NormalizeSpec(mean=cfg["mean"], std=cfg["std"])
|
|
204
|
+
|
|
205
|
+
sess = ort.InferenceSession(f"{resolved_path}.onnx")
|
|
206
|
+
inp_name = sess.get_inputs()[0].name
|
|
207
|
+
|
|
208
|
+
buf_len = int(sr * duration)
|
|
209
|
+
audio_buffer = deque([0.0] * buf_len, maxlen=buf_len)
|
|
210
|
+
prediction_history = deque(maxlen=n_averages)
|
|
211
|
+
|
|
212
|
+
|
|
213
|
+
def _audio_callback(indata, frames, time_info, status):
|
|
214
|
+
audio_buffer.extend(indata[:, 0])
|
|
215
|
+
|
|
216
|
+
last_trigger_times = {}
|
|
217
|
+
|
|
218
|
+
with sd.InputStream(samplerate=sr, channels=1, blocksize=int(sr * 0.1), callback=_audio_callback):
|
|
219
|
+
time.sleep(duration)
|
|
220
|
+
while True:
|
|
221
|
+
|
|
222
|
+
wav_tensor = torch.tensor(list(audio_buffer), dtype=torch.float32)
|
|
223
|
+
|
|
224
|
+
spectrogram = wav_to_spec.encodes(wav_tensor)
|
|
225
|
+
spectrogram = normalize_spec.encodes(spectrogram)
|
|
226
|
+
|
|
227
|
+
onnx_data = spectrogram.unsqueeze(0).unsqueeze(0).numpy()
|
|
228
|
+
|
|
229
|
+
logits = sess.run(None, {inp_name: onnx_data})[0][0]
|
|
230
|
+
|
|
231
|
+
exp_res = np.exp(logits - np.max(logits))
|
|
232
|
+
probs = exp_res / exp_res.sum()
|
|
233
|
+
|
|
234
|
+
prediction_history.append(probs)
|
|
235
|
+
|
|
236
|
+
if len(prediction_history) == n_averages:
|
|
237
|
+
mean_probs = np.mean(prediction_history, axis=0)
|
|
238
|
+
idx = np.argmax(mean_probs)
|
|
239
|
+
confidence = mean_probs[idx]
|
|
240
|
+
predicted_class = labels[idx]
|
|
241
|
+
|
|
242
|
+
if confidence > min_confidence:
|
|
243
|
+
if predicted_class in actions:
|
|
244
|
+
action_val = actions[predicted_class]
|
|
245
|
+
|
|
246
|
+
if callable(action_val):
|
|
247
|
+
func = action_val
|
|
248
|
+
cooldown = 0.0
|
|
249
|
+
else:
|
|
250
|
+
func = action_val.get("funkcja")
|
|
251
|
+
cooldown = action_val.get("cooldown", 0.0)
|
|
252
|
+
|
|
253
|
+
last_time = last_trigger_times.get(predicted_class, 0.0)
|
|
254
|
+
|
|
255
|
+
if func and (time.time() - last_time >= cooldown):
|
|
256
|
+
func()
|
|
257
|
+
prediction_history.clear()
|
|
258
|
+
last_trigger_times[predicted_class] = time.time()
|
|
259
|
+
|
|
260
|
+
time.sleep(0.05)
|
|
File without changes
|
|
Binary file
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: keywordtensor-edge
|
|
3
|
+
Version: 1.0.0
|
|
4
|
+
Summary: A lightweight inference library for training custom keyword spotting models and running real-time voice command detection.
|
|
5
|
+
License: MIT
|
|
6
|
+
License-File: LICENSE
|
|
7
|
+
Requires-Dist: numpy
|
|
8
|
+
Requires-Dist: sounddevice
|
|
9
|
+
Requires-Dist: onnxruntime
|
|
10
|
+
Requires-Dist: torch
|
|
11
|
+
Requires-Dist: torchaudio
|
|
12
|
+
Requires-Dist: torch-audiomentations
|
|
13
|
+
Requires-Dist: torchcodec
|
|
14
|
+
Requires-Dist: onnx
|
|
15
|
+
Dynamic: license-file
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
LICENSE
|
|
2
|
+
README.md
|
|
3
|
+
pyproject.toml
|
|
4
|
+
keywordtensor/__init__.py
|
|
5
|
+
keywordtensor/core.py
|
|
6
|
+
keywordtensor/pretrained/__init__.py
|
|
7
|
+
keywordtensor/pretrained/prawda_falsz.onnx
|
|
8
|
+
keywordtensor/pretrained/prawda_falsz_config.json
|
|
9
|
+
keywordtensor_edge.egg-info/PKG-INFO
|
|
10
|
+
keywordtensor_edge.egg-info/SOURCES.txt
|
|
11
|
+
keywordtensor_edge.egg-info/dependency_links.txt
|
|
12
|
+
keywordtensor_edge.egg-info/requires.txt
|
|
13
|
+
keywordtensor_edge.egg-info/top_level.txt
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
keywordtensor
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=61.0"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "keywordtensor-edge"
|
|
7
|
+
version = "1.0.0"
|
|
8
|
+
description = "A lightweight inference library for training custom keyword spotting models and running real-time voice command detection."
|
|
9
|
+
license = {text = "MIT"}
|
|
10
|
+
dependencies = [
|
|
11
|
+
"numpy",
|
|
12
|
+
"sounddevice",
|
|
13
|
+
"onnxruntime",
|
|
14
|
+
"torch",
|
|
15
|
+
"torchaudio",
|
|
16
|
+
"torch-audiomentations",
|
|
17
|
+
"torchcodec",
|
|
18
|
+
"onnx"
|
|
19
|
+
]
|
|
20
|
+
|
|
21
|
+
[tool.setuptools]
|
|
22
|
+
packages = ["keywordtensor", "keywordtensor.pretrained"]
|
|
23
|
+
|
|
24
|
+
[tool.setuptools.package-data]
|
|
25
|
+
"keywordtensor.pretrained" = ["*.onnx", "*.json"]
|