Skip to main content

micro_wakeword/
config.rs

1use std::fs;
2use std::path::{Path, PathBuf};
3
4use serde::Deserialize;
5
6use crate::{Error, Result};
7
8/// Descriptive fields from a microWakeWord model file.
9#[derive(Clone, Debug, Default, PartialEq, Eq)]
10pub struct ModelMetadata {
11    pub author: Option<String>,
12    pub website: Option<String>,
13    pub trained_languages: Vec<String>,
14    pub format_version: u32,
15}
16
17/// Validated model and detection configuration.
18#[derive(Clone, Debug)]
19pub struct Config {
20    pub model_path: PathBuf,
21    pub wake_word: String,
22    pub probability_cutoff: f32,
23    pub sliding_window_size: usize,
24    pub feature_step_size_ms: u32,
25    pub metadata: ModelMetadata,
26}
27
28#[derive(Deserialize)]
29struct FileConfig {
30    #[serde(rename = "type")]
31    kind: String,
32    wake_word: String,
33    model: PathBuf,
34    version: u32,
35    author: Option<String>,
36    website: Option<String>,
37    #[serde(default)]
38    trained_languages: Vec<String>,
39    micro: MicroConfig,
40}
41
42#[derive(Deserialize)]
43struct MicroConfig {
44    probability_cutoff: f32,
45    sliding_window_size: usize,
46    feature_step_size: u32,
47}
48
49impl Config {
50    /// Parse a standard microWakeWord JSON file and resolve its model path
51    /// relative to the JSON file.
52    pub fn from_file(path: impl AsRef<Path>) -> Result<Self> {
53        let path = path.as_ref();
54        let text = fs::read_to_string(path).map_err(|source| Error::Io {
55            path: path.to_owned(),
56            source,
57        })?;
58        let raw: FileConfig = serde_json::from_str(&text).map_err(|source| Error::Json {
59            path: path.to_owned(),
60            source,
61        })?;
62        let parent = path.parent().unwrap_or_else(|| Path::new("."));
63        let model_path = if raw.model.is_absolute() {
64            raw.model
65        } else {
66            parent.join(raw.model)
67        };
68        let config = Self {
69            model_path,
70            wake_word: raw.wake_word,
71            probability_cutoff: raw.micro.probability_cutoff,
72            sliding_window_size: raw.micro.sliding_window_size,
73            feature_step_size_ms: raw.micro.feature_step_size,
74            metadata: ModelMetadata {
75                author: raw.author,
76                website: raw.website,
77                trained_languages: raw.trained_languages,
78                format_version: raw.version,
79            },
80        };
81        config.validate_with_kind(&raw.kind)?;
82        Ok(config)
83    }
84
85    pub(crate) fn validate(&self) -> Result<()> {
86        self.validate_with_kind("micro")
87    }
88
89    fn validate_with_kind(&self, kind: &str) -> Result<()> {
90        if kind != "micro" {
91            return Err(Error::InvalidConfig(format!(
92                "type must be `micro`, got `{kind}`"
93            )));
94        }
95        if self.metadata.format_version != 2 {
96            return Err(Error::InvalidConfig(format!(
97                "only format version 2 is supported, got {}",
98                self.metadata.format_version
99            )));
100        }
101        if self.wake_word.trim().is_empty() {
102            return Err(Error::InvalidConfig("wake_word cannot be empty".into()));
103        }
104        if !(0.0..=1.0).contains(&self.probability_cutoff) || !self.probability_cutoff.is_finite() {
105            return Err(Error::InvalidConfig(
106                "probability_cutoff must be between 0 and 1".into(),
107            ));
108        }
109        if self.sliding_window_size == 0 {
110            return Err(Error::InvalidConfig(
111                "sliding_window_size must be greater than zero".into(),
112            ));
113        }
114        if self.feature_step_size_ms != 10 {
115            return Err(Error::InvalidConfig(format!(
116                "only a 10 ms feature_step_size is supported, got {}",
117                self.feature_step_size_ms
118            )));
119        }
120        Ok(())
121    }
122}