Skip to content

Commit

Permalink
Version 2.0.0 (#3)
Browse files Browse the repository at this point in the history
* Use Ruspotter v2
* Use cpal for audio recording
  • Loading branch information
GiviMAD authored Mar 14, 2023
1 parent 3ba89ae commit 691968f
Show file tree
Hide file tree
Showing 13 changed files with 1,343 additions and 806 deletions.
952 changes: 704 additions & 248 deletions Cargo.lock

Large diffs are not rendered by default.

22 changes: 7 additions & 15 deletions Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,24 +1,16 @@
[package]
name = "rustpotter-cli"
version = "1.0.1"
version = "2.0.0"
edition = "2021"
license = "Apache-2.0"
description = "CLI for Rustpotter, an open source wake word spotter forged in rust."
description = "CLI for Rustpotter, an open source wakeword spotter forged in rust."
authors = ["Miguel Álvarez Díez <[email protected]>"]
repository = "https://github.com/GiviMAD/rustpotter"

exclude = ["tools/**",".github",".gitignore"]
[dependencies]
rustpotter = { version = "1.0.1", features = ["files", "build", "log", "vad"] }
log = "0.4.6"
pv_recorder = "1.0.2"
rustpotter = { version = "2.0.0" }
ctrlc = "3.2.2"
clap = { version = "3.1.13", features = ["derive"] }
clap = { version = "4.1.6", features = ["derive"] }
hound = "3.4.0"
include_dir = "0.7.2"
tempfile = "3.3.0"
simple_logger = "2.1.0"

[features]
default = []
# include recorder library into the binary for distribution outside cargo
dist = []
cpal = "0.15.0"
time = "0.3.20"
25 changes: 13 additions & 12 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# Rustpotter CLI

## CLI for Rustpotter, an open source wake word spotter forged in rust
## CLI for Rustpotter, an open source wakeword spotter forged in rust

<div align="center">
<img src="./logo.png?raw=true" width="400px"</img>
Expand All @@ -13,32 +13,32 @@ This is a client for using the [rustpotter](https://github.com/GiviMAD/rustpotte
## Use Example

```sh
# quick look up to the help
# print help
rustpotter-cli -h
# list available input devices
# print command help
rustpotter-cli spot -h
# list available input devices and configs
rustpotter-cli devices
# record samples, you should press "ctrl + c" to stop after saying your wakeword
rustpotter-cli record hey_home.wav
rustpotter-cli record hey_home1.wav
rustpotter-cli record hey_home2.wav
rustpotter-cli record hey_home3.wav
# check that your samples are correctly trimmed and without noise using any player
...
# build a model, this op is idempotent (same input samples with same options = same model)
rustpotter-cli build-model \
--model-path hey_home.rpw \
--model-name "hey home" \
hey_home.wav hey_home1.wav hey_home2.wav hey_home3.wav
# test the model accuracy over the samples
rustpotter-cli test-model hey_home.rpw hey_home.wav
rustpotter-cli test-model hey_home.rpw hey_home1.wav
rustpotter-cli test-model hey_home.rpw hey_home2.wav
rustpotter-cli test-model hey_home.rpw hey_home3.wav
hey_home.wav hey_home1.wav hey_home2.wav
# test the model accuracy over the samples in verbose mode to print partial detections
rustpotter-cli test-model -v hey_home.rpw hey_home.wav
rustpotter-cli test-model -v hey_home.rpw hey_home1.wav
rustpotter-cli test-model -v hey_home.rpw hey_home2.wav
# test the spot functionality in real time, customizing
# the default detection threshold
rustpotter-cli spot -t 0.563 hey_home.rpw
# rebuild a model adding a custom threshold for the word,
# this one has prevalence over the default one
# this one has prevalence over the spot configuration
rustpotter-cli build-model \
--averaged-threshold 0.54 \
--threshold 0.54 \
Expand All @@ -48,5 +48,6 @@ hey_home.wav hey_home1.wav hey_home2.wav hey_home3.wav
# you can spot using multiple models
rustpotter-cli spot hey_home.rpw good_morning.rpw ...
# you will get an output like this in your terminal on each spot event
Detected 'good morning' with score 0.6146946!
Wakeword detection: 10:58:53
RustpotterDetection { name: "hey home", avg_score: 0.37827095, score: 0.5000453, scores: {"hey_home2.wav": 0.43628272, "hey_home.wav": 0.5000453, "hey_home1.wav": 0.4230849}, counter: 7 }
```
144 changes: 0 additions & 144 deletions build.rs

This file was deleted.

36 changes: 15 additions & 21 deletions src/cli/build_model.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,10 @@ use std::{fs::File, io::BufReader};

use clap::Args;
use hound::WavReader;
use rustpotter::WakewordDetectorBuilder;

use crate::utils::enable_rustpotter_log;
use rustpotter::Wakeword;

#[derive(Args, Debug)]
/// Build model file from samples
/// Creates a wakeword using RIFF wav audio files.
#[clap()]
pub struct BuildModelCommand {
#[clap(short = 'n', long)]
Expand All @@ -16,7 +14,7 @@ pub struct BuildModelCommand {
#[clap(short = 'p', long)]
/// Generated model path
model_path: String,
#[clap(min_values = 1, required = true)]
#[clap(num_args = 1.., required = true)]
/// List of sample record paths
sample_path: Vec<String>,
#[clap(short = 't', long)]
Expand All @@ -25,37 +23,33 @@ pub struct BuildModelCommand {
#[clap(short = 'a', long)]
/// Averaged threshold to configure in the generated model, overwrites the detector averaged threshold
averaged_threshold: Option<f32>,
#[clap(long)]
/// Enables rustpotter debug log
debug: bool,
}
pub fn build(command: BuildModelCommand) -> Result<(), String> {
println!("Start building {}!", command.model_path);
println!("From samples:");
for path in &command.sample_path {
let reader = BufReader::new(File::open(path).map_err(|err| err.to_string())?);
let wav_spec = WavReader::new(reader).map_err(|err| err.to_string())?
let wav_spec = WavReader::new(reader)
.map_err(|err| err.to_string())?
.spec();
println!("{}: {:?}", path, wav_spec);
}
if command.debug {
enable_rustpotter_log();
}
let mut word_detector = WakewordDetectorBuilder::new().build();
word_detector.add_wakeword_with_wav_files(
&command.model_name,
false,
command.averaged_threshold,
let wakeword = Wakeword::new_from_sample_files(
command.model_name.clone(),
command.threshold,
command.averaged_threshold,
command.sample_path,
).map_err(|e| e.to_string())?;
match word_detector.generate_wakeword_model_file(command.model_name.clone(), command.model_path)
{
)?;
match wakeword.save_to_file(&command.model_path) {
Ok(_) => {
println!("{} created!", command.model_name);
}
Err(error) => {
clap::Error::raw(clap::ErrorKind::InvalidValue, error.to_string() + "\n").exit();
clap::Error::raw(
clap::error::ErrorKind::InvalidValue,
error.to_string() + "\n",
)
.exit();
}
};
Ok(())
Expand Down
64 changes: 40 additions & 24 deletions src/cli/devices.rs
Original file line number Diff line number Diff line change
@@ -1,33 +1,49 @@
extern crate cpal;
use clap::Args;
use pv_recorder::RecorderBuilder;
#[cfg(feature = "dist")]
use crate::pv_recorder_utils::_get_pv_recorder_lib;
#[derive(Args, Debug)]
use cpal::traits::{DeviceTrait, HostTrait};
/// Record audio sample
#[derive(Args, Debug)]
#[clap()]
pub struct DevicesCommand {}
pub fn devices(_: DevicesCommand) -> Result<(), String> {
#[cfg(feature = "dist")]
let mut recorder_builder = RecorderBuilder::new();
#[cfg(not(feature = "dist"))]
let recorder_builder = RecorderBuilder::new();
#[cfg(feature = "dist")]
let lib_temp_path = _get_pv_recorder_lib();
#[cfg(feature = "dist")]
recorder_builder.library_path(lib_temp_path.to_path_buf().as_path());
let recorder = recorder_builder.init()
.expect("Failed to initialize recorder");
println!("Available record audio devices:");
let audio_devices = recorder.get_audio_devices();
match audio_devices {
Ok(audio_devices) => {
for (idx, device) in audio_devices.iter().enumerate() {
println!("{}: {:?}", idx, device);
println!("Supported hosts:\n {:?}", cpal::ALL_HOSTS);
let default_host = cpal::default_host();
let host_id = default_host.id();
println!("Using hosts:\n {:?}", default_host.id());
println!("{}", host_id.name());
let host = cpal::host_from_id(host_id).map_err(|err| err.to_string())?;
let default_in = host.default_input_device().map(|e| e.name().unwrap());
if let Some(def_in) = default_in {
println!(" Default input device:\n {}", def_in);
} else {
println!(" No default input device");
}
let devices = host.input_devices().map_err(|err| err.to_string())?;
println!(" Devices: ");
for (device_index, device) in devices.enumerate() {
println!(
" {} - \"{}\"",
device_index,
device.name().map_err(|err| err.to_string())?
);

// Input configs
if let Ok(conf) = device.default_input_config() {
println!(" Default input stream config:\n {:?}", conf);
}
let input_configs = match device.supported_input_configs() {
Ok(f) => f.collect(),
Err(e) => {
println!(" Error getting supported input configs: {:?}", e);
Vec::new()
}
};
if !input_configs.is_empty() {
println!(" All supported input stream configs:");
for (config_index, config) in input_configs.into_iter().enumerate() {
println!(" {} - {:?}", config_index, config);
}
}
Err(err) => panic!("Failed to get audio devices: {}", err),
};
#[cfg(all(feature = "dist", not(target_os = "windows")))]
lib_temp_path.close().expect("Unable to remove temp file");
}
Ok(())
}
Loading

0 comments on commit 691968f

Please sign in to comment.