Jarbas commited on
Commit
1201150
·
verified ·
1 Parent(s): 76170bf

Model card: standalone use without OpenVoiceOS

Browse files
Files changed (1) hide show
  1. README.md +55 -0
README.md CHANGED
@@ -79,6 +79,61 @@ pip install --pre ovos-ww-plugin-wakeforge
79
  A model file from this repository also loads by path: set `model` to the local `.onnx` file. The plugin reads
80
  the featurizer and the default threshold from the model's metadata.
81
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
82
  ## Choosing a threshold
83
 
84
  Set `"threshold"` in the hotword config to trade missed wake words against false activations. A lower value
 
79
  A model file from this repository also loads by path: set `model` to the local `.onnx` file. The plugin reads
80
  the featurizer and the default threshold from the model's metadata.
81
 
82
+ ## Use in your own code
83
+
84
+ The models need only `numpy`, `onnxruntime` and `huggingface_hub`; nothing from OpenVoiceOS. Each model is a small classifier that reads WakeHuBERT-tiny features, so you run two ONNX files: the featurizer, then the wake-word model.
85
+
86
+ ```python
87
+ import numpy as np
88
+ import onnxruntime as ort
89
+ from huggingface_hub import hf_hub_download
90
+
91
+ word = "jarvis"
92
+ head = ort.InferenceSession(hf_hub_download("OpenVoiceOS/wakehubert-wakewords", f"models/wakehubert_{word}.onnx"))
93
+ meta = head.get_modelmeta().custom_metadata_map
94
+ featurizer_file = "wakehubert_int8.onnx" if meta["pretrained_featurizer"] == "wakehubert-int8" else "wakehubert.onnx"
95
+ feat = ort.InferenceSession(hf_hub_download("TigreGotico/wakehubert-tiny", featurizer_file))
96
+ threshold = float(meta["default_threshold"])
97
+
98
+ WINDOW = 24000 # 1.5 s of 16 kHz audio: the window the models were trained on (75 feature frames)
99
+ BLOCK = 1280 # score every 80 ms
100
+ DEBOUNCE_BLOCKS = 25 # ignore 2 s after a detection
101
+
102
+
103
+ class WakeWordDetector:
104
+ def __init__(self):
105
+ self.buf = np.zeros(WINDOW, np.float32)
106
+ self.cooldown = 0
107
+
108
+ def push(self, chunk):
109
+ """chunk: 1280 float32 samples at 16 kHz in -1..1. Returns (score, detected)."""
110
+ self.buf = np.concatenate([self.buf, chunk.astype(np.float32)])[-WINDOW:]
111
+ features = feat.run(None, {"waveform": self.buf[None]})[0] # [1, 75, 128]
112
+ logit = head.run(None, {"features": features})[0][0]
113
+ score = float(1.0 / (1.0 + np.exp(-logit)))
114
+ self.cooldown = max(0, self.cooldown - 1)
115
+ detected = score >= threshold and self.cooldown == 0
116
+ if detected:
117
+ self.cooldown = DEBOUNCE_BLOCKS
118
+ return score, detected
119
+ ```
120
+
121
+ From a microphone, for example with `sounddevice`:
122
+
123
+ ```python
124
+ import sounddevice as sd
125
+
126
+ detector = WakeWordDetector()
127
+ with sd.InputStream(samplerate=16000, channels=1, dtype="float32", blocksize=BLOCK) as stream:
128
+ while True:
129
+ chunk, _ = stream.read(BLOCK)
130
+ score, detected = detector.push(chunk[:, 0])
131
+ if detected:
132
+ print(f"{word} detected (score {score:.2f})")
133
+ ```
134
+
135
+ Each block featurizes the last 1.5 s on its own, exactly as the models were trained and scored. The featurizer is causal, the per-block cost is about 1–2 ms on one CPU core for the int8 featurizer, and one featurizer run can feed any number of wake-word models: run `feat` once per block and pass the same `features` to each model. Replace `threshold` to change sensitivity (see below). Checked against the plugin on real recordings: the snippet fires on the same clips.
136
+
137
  ## Choosing a threshold
138
 
139
  Set `"threshold"` in the hotword config to trade missed wake words against false activations. A lower value