use crate::{
dtype::Dtype,
error::{Error, ParsePayload, Result},
model_validation::require_positive,
};
pub const MODEL_TYPE: &str = "silero_vad";
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct BranchConfig {
sample_rate: u32,
filter_length: i32,
hop_length: i32,
pad: i32,
cutoff: i32,
context_size: i32,
chunk_size: i32,
}
impl BranchConfig {
pub const fn default_16k() -> Self {
Self {
sample_rate: 16_000,
filter_length: 256,
hop_length: 128,
pad: 64,
cutoff: 129,
context_size: 64,
chunk_size: 512,
}
}
pub const fn default_8k() -> Self {
Self {
sample_rate: 8_000,
filter_length: 128,
hop_length: 64,
pad: 32,
cutoff: 65,
context_size: 32,
chunk_size: 256,
}
}
#[allow(clippy::too_many_arguments)]
pub const fn new(
sample_rate: u32,
filter_length: i32,
hop_length: i32,
pad: i32,
cutoff: i32,
context_size: i32,
chunk_size: i32,
) -> Self {
Self {
sample_rate,
filter_length,
hop_length,
pad,
cutoff,
context_size,
chunk_size,
}
}
#[inline(always)]
pub const fn sample_rate(&self) -> u32 {
self.sample_rate
}
#[inline(always)]
pub const fn filter_length(&self) -> i32 {
self.filter_length
}
#[inline(always)]
pub const fn hop_length(&self) -> i32 {
self.hop_length
}
#[inline(always)]
pub const fn pad(&self) -> i32 {
self.pad
}
#[inline(always)]
pub const fn cutoff(&self) -> i32 {
self.cutoff
}
#[inline(always)]
pub const fn context_size(&self) -> i32 {
self.context_size
}
#[inline(always)]
pub const fn chunk_size(&self) -> i32 {
self.chunk_size
}
fn overlay_json(mut self, obj: &serde_json::Map<String, serde_json::Value>) -> Result<Self> {
if let Some(v) = obj.get("sample_rate") {
self.sample_rate = parse_u32("BranchConfig.sample_rate", v)?;
}
if let Some(v) = obj.get("filter_length") {
self.filter_length = parse_i32("BranchConfig.filter_length", v)?;
}
if let Some(v) = obj.get("hop_length") {
self.hop_length = parse_i32("BranchConfig.hop_length", v)?;
}
if let Some(v) = obj.get("pad") {
self.pad = parse_i32("BranchConfig.pad", v)?;
}
if let Some(v) = obj.get("cutoff") {
self.cutoff = parse_i32("BranchConfig.cutoff", v)?;
}
if let Some(v) = obj.get("context_size") {
self.context_size = parse_i32("BranchConfig.context_size", v)?;
}
if let Some(v) = obj.get("chunk_size") {
self.chunk_size = parse_i32("BranchConfig.chunk_size", v)?;
}
Ok(self)
}
fn validate(&self) -> Result<()> {
require_positive("BranchConfig.filter_length", self.filter_length)?;
require_positive("BranchConfig.hop_length", self.hop_length)?;
if self.pad < 0 {
return Err(Error::OutOfRange(crate::error::OutOfRangePayload::new(
"BranchConfig.pad",
"must be >= 0",
smol_str::format_smolstr!("{}", self.pad),
)));
}
require_positive("BranchConfig.cutoff", self.cutoff)?;
require_positive("BranchConfig.context_size", self.context_size)?;
require_positive("BranchConfig.chunk_size", self.chunk_size)?;
Ok(())
}
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct ModelConfig {
dtype: Dtype,
threshold: f64,
min_speech_duration_ms: i32,
min_silence_duration_ms: i32,
speech_pad_ms: i32,
branch_16k: BranchConfig,
branch_8k: BranchConfig,
}
impl Default for ModelConfig {
fn default() -> Self {
Self {
dtype: Dtype::F32,
threshold: 0.5,
min_speech_duration_ms: 250,
min_silence_duration_ms: 100,
speech_pad_ms: 30,
branch_16k: BranchConfig::default_16k(),
branch_8k: BranchConfig::default_8k(),
}
}
}
impl ModelConfig {
#[inline(always)]
pub const fn dtype(&self) -> Dtype {
self.dtype
}
#[inline(always)]
pub const fn threshold(&self) -> f64 {
self.threshold
}
#[inline(always)]
pub const fn min_speech_duration_ms(&self) -> i32 {
self.min_speech_duration_ms
}
#[inline(always)]
pub const fn min_silence_duration_ms(&self) -> i32 {
self.min_silence_duration_ms
}
#[inline(always)]
pub const fn speech_pad_ms(&self) -> i32 {
self.speech_pad_ms
}
#[inline(always)]
pub const fn branch_16k(&self) -> &BranchConfig {
&self.branch_16k
}
#[inline(always)]
pub const fn branch_8k(&self) -> &BranchConfig {
&self.branch_8k
}
pub fn from_json(config_json: &str) -> Result<Self> {
use serde_json::Value;
let value: Value = serde_json::from_str(config_json)
.map_err(|e| Error::Parse(ParsePayload::new("silero_vad config", "JSON", e)))?;
let Value::Object(map) = value else {
return Err(Error::OutOfRange(crate::error::OutOfRangePayload::new(
"silero_vad config",
"must be a JSON object",
"non-object",
)));
};
let mut cfg = Self::default();
if let Some(v) = map.get("dtype") {
cfg.dtype = match v.as_str() {
Some("float16") => Dtype::F16,
_ => Dtype::F32,
};
}
if let Some(v) = map.get("threshold") {
cfg.threshold = parse_f64("ModelConfig.threshold", v)?;
}
if let Some(v) = map.get("min_speech_duration_ms") {
cfg.min_speech_duration_ms = parse_i32("ModelConfig.min_speech_duration_ms", v)?;
}
if let Some(v) = map.get("min_silence_duration_ms") {
cfg.min_silence_duration_ms = parse_i32("ModelConfig.min_silence_duration_ms", v)?;
}
if let Some(v) = map.get("speech_pad_ms") {
cfg.speech_pad_ms = parse_i32("ModelConfig.speech_pad_ms", v)?;
}
match map.get("branch_16k") {
None | Some(Value::Null) => {}
Some(Value::Object(obj)) => cfg.branch_16k = cfg.branch_16k.overlay_json(obj)?,
Some(_) => return Err(branch_type_error("branch_16k")),
}
match map.get("branch_8k") {
None | Some(Value::Null) => {}
Some(Value::Object(obj)) => cfg.branch_8k = BranchConfig::default_16k().overlay_json(obj)?,
Some(_) => return Err(branch_type_error("branch_8k")),
}
cfg.validate()?;
Ok(cfg)
}
fn validate(&self) -> Result<()> {
self.branch_16k.validate()?;
self.branch_8k.validate()?;
for (field, v) in [
(
"ModelConfig.min_speech_duration_ms",
self.min_speech_duration_ms,
),
(
"ModelConfig.min_silence_duration_ms",
self.min_silence_duration_ms,
),
("ModelConfig.speech_pad_ms", self.speech_pad_ms),
] {
if v < 0 {
return Err(Error::OutOfRange(crate::error::OutOfRangePayload::new(
field,
"must be >= 0",
smol_str::format_smolstr!("{v}"),
)));
}
}
Ok(())
}
}
fn branch_type_error(field: &'static str) -> Error {
Error::OutOfRange(crate::error::OutOfRangePayload::new(
field,
"must be a JSON object or null",
"non-object",
))
}
fn parse_i32(field: &'static str, v: &serde_json::Value) -> Result<i32> {
v.as_i64()
.and_then(|n| i32::try_from(n).ok())
.ok_or_else(|| {
Error::OutOfRange(crate::error::OutOfRangePayload::new(
field,
"must be an i32 integer",
smol_str::format_smolstr!("{v}"),
))
})
}
fn parse_u32(field: &'static str, v: &serde_json::Value) -> Result<u32> {
v.as_u64()
.and_then(|n| u32::try_from(n).ok())
.ok_or_else(|| {
Error::OutOfRange(crate::error::OutOfRangePayload::new(
field,
"must be a u32 integer",
smol_str::format_smolstr!("{v}"),
))
})
}
fn parse_f64(field: &'static str, v: &serde_json::Value) -> Result<f64> {
v.as_f64().ok_or_else(|| {
Error::OutOfRange(crate::error::OutOfRangePayload::new(
field,
"must be a number",
smol_str::format_smolstr!("{v}"),
))
})
}