1use std::fs;
2use std::path::{Path, PathBuf};
3
4use serde::Deserialize;
5
6use crate::{Error, Result};
7
8#[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#[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 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}