diff --git a/examples/detect_from_microphone.py b/examples/detect_from_microphone.py index f72b084..a62778b 100644 --- a/examples/detect_from_microphone.py +++ b/examples/detect_from_microphone.py @@ -13,10 +13,15 @@ # limitations under the License. # Imports +import sys +import os import pyaudio import numpy as np from openwakeword.model import Model +from openwakeword.resources.webui.server import openWakeWordWebUI import argparse +from http.server import ThreadingHTTPServer +import threading # Parse input arguments parser=argparse.ArgumentParser() @@ -25,7 +30,20 @@ parser.add_argument( help="How much audio (in samples) to predict on at once", type=int, default=1280, - required=True + required=False +) + +parser.add_argument( + "--vad_threshold", + help="The minimum threshold for voice activity detection required before an activations", + type=float, + default=0.3 +) + +parser.add_argument( + "--custom_verifier_model_online_learning", + help="Whether to enable online learning of a custom verifier model", + action='store_true', ) args=parser.parse_args() @@ -39,8 +57,10 @@ audio = pyaudio.PyAudio() mic_stream = audio.open(format=FORMAT, channels=CHANNELS, rate=RATE, input=True, frames_per_buffer=CHUNK) # Load pre-trained openwakeword models -owwModel = Model() - +owwModel = Model( + vad_threshold=args.vad_threshold, + custom_verifier_model_online_learning=args.custom_verifier_model_online_learning +) # Run capture loop continuosly, checking for wakewords if __name__ == "__main__": # Generate output string header @@ -50,28 +70,40 @@ if __name__ == "__main__": print("#"*100) print("\n"*13) + # Start HTTP server + os.chdir(owwModel.cache_dir) + server = ThreadingHTTPServer(("127.0.0.1", 9999), lambda *args, **kwargs: openWakeWordWebUI(owwModel, *args, **kwargs)) + server_thread = threading.Thread(target=server.serve_forever) + server_thread.daemon = True + server_thread.start() + while True: - # Get audio - audio = np.frombuffer(mic_stream.read(CHUNK), dtype=np.int16) + try: + # Get audio + audio = np.frombuffer(mic_stream.read(CHUNK), dtype=np.int16) - # Feed to openWakeWord model - prediction = owwModel.predict(audio) + # Feed to openWakeWord model + prediction = owwModel.predict(audio) - # Column titles - n_spaces = 16 - output_string_header = """ - Model Name | Score | Wakeword Status - -------------------------------------- - """ + # Column titles + n_spaces = 16 + output_string_header = """ + Model Name | Score | Wakeword Status + -------------------------------------- + """ - for mdl in owwModel.prediction_buffer.keys(): - # Add scores in formatted table - scores = list(owwModel.prediction_buffer[mdl]) - curr_score = format(scores[-1], '.20f').replace("-", "") + for mdl in owwModel.prediction_buffer.keys(): + # Add scores in formatted table + scores = list(owwModel.prediction_buffer[mdl]) + curr_score = format(scores[-1], '.20f').replace("-", "") - output_string_header += f"""{mdl}{" "*(n_spaces - len(mdl))} | {curr_score[0:5]} | {"--"+" "*20 if scores[-1] <= 0.5 else "Wakeword Detected!"} - """ + output_string_header += f"""{mdl}{" "*(n_spaces - len(mdl))} | {curr_score[0:5]} | {"--"+" "*20 if scores[-1] <= 0.5 else "Wakeword Detected!"} + """ - # Print results table - print("\033[F"*14) - print(output_string_header, " ", end='\r') + # # Print results table + # print("\033[F"*15) + # print(output_string_header, " ", end='\r') + + except KeyboardInterrupt: + server.shutdown() + sys.exit(0) diff --git a/openwakeword/custom_verifier_model.py b/openwakeword/custom_verifier_model.py index 4b3b922..dc5eb91 100644 --- a/openwakeword/custom_verifier_model.py +++ b/openwakeword/custom_verifier_model.py @@ -92,7 +92,7 @@ def flatten_features(x): def make_sklearn_pipeline(): # clf = SVC(gamma='auto', probability=True) - clf = LogisticRegression(random_state=0, max_iter=2000, C=0.01) + clf = LogisticRegression(random_state=0, max_iter=2000, C=0.01, class_weight='balanced') pipeline = make_pipeline(FunctionTransformer(flatten_features), StandardScaler(), clf) return pipeline diff --git a/openwakeword/model.py b/openwakeword/model.py index 57640b4..d3f632a 100755 --- a/openwakeword/model.py +++ b/openwakeword/model.py @@ -19,6 +19,8 @@ import openwakeword from openwakeword.utils import AudioFeatures import wave +import soundfile +import uuid import os from pathlib import Path import pickle @@ -125,12 +127,17 @@ class Model(): self.model_input_names[mdl_name] = self.models[mdl_name].get_inputs()[0].name # Create attributes to store realtime usage data for verifier models - self.custom_verifier_data[mdl_name] = { - "features": { - "positive": deque(maxlen=1000), - "negative": deque(maxlen=1000) + usage_data = { + "features": { + "positive": deque(maxlen=1000), + "negative": deque(maxlen=2000) + } } - } + if self.model_outputs[mdl_name] == 1: + self.custom_verifier_data[mdl_name] = usage_data + else: + for cls in self.class_mapping[mdl_name].values(): + self.custom_verifier_data[cls] = usage_data # Create filesystem cache locations, or load existing data in the cache if cache_directory == "": @@ -140,6 +147,7 @@ class Model(): self.cache_dir = cache_directory if not os.path.exists(self.cache_dir): os.mkdir(self.cache_dir) + os.mkdir(os.path.join(self.cache_dir, "activation_clips")) else: if os.path.exists(os.path.join(self.cache_dir, "cached_features.pkl")): # Load existing data in the cache @@ -284,7 +292,6 @@ class Model(): self.custom_verifier_data[cls]["features"]["negative"].append(frame_features.flatten()) # Update scores of positive predictions - positive_examples_added = False if predictions[cls] >= self.custom_verifier_threshold: parent_model = self.get_parent_model_from_label(cls) if self.custom_verifier_models.get(parent_model, False): @@ -295,35 +302,52 @@ class Model(): )[0][-1] predictions[cls] = verifier_prediction - # Update data cache for verifier models - if self.custom_verifier_model_online_learning and predictions[cls] >= self.custom_verifier_threshold: + # Update data cache for verifier models only on predictions with two or more sequential + # frames with scores above the threshold + if self.custom_verifier_model_online_learning and predictions[cls] >= self.custom_verifier_threshold and \ + self.prediction_buffer[cls][-1] >= self.custom_verifier_threshold: self.custom_verifier_data[cls]["features"]["positive"].append(frame_features.flatten()) - positive_examples_added = True - # Save feature data to cache + # Save feature data to cache after activation has finished + if (np.array(self.prediction_buffer[cls])[-3:] > self.custom_verifier_threshold).sum() == 0: pickle.dump(self.custom_verifier_data, open(os.path.join(self.cache_dir, "cached_features.pkl"), 'wb')) - # Train verifier model on latest data at most every 10 seconds or after every positive detection + # Save audio clip of activation (last 4 seconds) + raw_data = np.array(list(self.preprocessor.raw_data_buffer)[-16000*4:]).astype(np.int16) + soundfile.write(os.path.join(self.cache_dir, "activation_clips", f"{cls}_{uuid.uuid4().hex[0:6]}.ogg"), + raw_data, 16000) + + # Train verifier model on latest data at most every 60 seconds and only if there are enough positive clips if self.custom_verifier_model_online_learning and \ - (self.samples_processed >= 16000*10 or positive_examples_added): + self.samples_processed >= 16000*60 and \ + len(self.custom_verifier_data[cls]["features"]["positive"]) >= 3: self.samples_processed = 0 parent_model = self.get_parent_model_from_label(cls) + # y = np.array( + # [1]*np.array(self.custom_verifier_data[cls]["features"]["positive"]).shape[0] + + # [0]*np.array(self.custom_verifier_data[cls]["features"]["negative"]).shape[0] + # ) + y = np.array( [1]*np.array(self.custom_verifier_data[cls]["features"]["positive"]).shape[0] + - [0]*np.array(self.custom_verifier_data[cls]["features"]["negative"]).shape[0] + [0]*np.array(self.custom_verifier_data[cls]["features"]["positive"]).shape[0]*2 ) - # need a minimum number of examples to train (5) - if len(self.custom_verifier_data[cls]["features"]["positive"]) > 3 and \ - len(self.custom_verifier_data[cls]["features"]["negative"]) > 5: + # need a minimum number of negative examples to train + if len(self.custom_verifier_data[cls]["features"]["negative"]) > len(self.custom_verifier_data[cls]["features"]["positive"]): print("Updating custom verifier model") + random_ndcs = np.random.randint( + 0, len(self.custom_verifier_data[cls]["features"]["negative"]) - len(self.custom_verifier_data[cls]["features"]["positive"]) - 1, + len(self.custom_verifier_data[cls]["features"]["positive"])) + x = np.vstack(( np.array(self.custom_verifier_data[cls]["features"]["positive"]), - np.array(self.custom_verifier_data[cls]["features"]["negative"]), + np.array(self.custom_verifier_data[cls]["features"]["negative"])[random_ndcs, :], + np.array(self.custom_verifier_data[cls]["features"]["negative"])[-len(random_ndcs):, :] )) self.custom_verifier_models[parent_model].fit(x, y) - print("Accuracy: ", sum(y == self.custom_verifier_models[parent_model].predict(x))/len(x)) + # print("Accuracy: ", sum(y == self.custom_verifier_models[parent_model].predict(x))/len(x)) # Update prediction buffer, and zero predictions for first 5 frames during model initialization for cls in predictions.keys(): diff --git a/openwakeword/resources/webui/index.html b/openwakeword/resources/webui/index.html new file mode 100644 index 0000000..279607a --- /dev/null +++ b/openwakeword/resources/webui/index.html @@ -0,0 +1,178 @@ + + + + +
+| Name | +Audio | ++ |
|---|