use std::sync::Arc;
use mediatime::TimeRange;
#[cfg(feature = "serde")]
use serde::{Deserialize, Serialize};
use smol_str::SmolStr;
use crate::types::{ChunkId, Lang};
#[derive(Clone, Debug)]
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
pub struct AsrParams {
#[cfg_attr(
feature = "serde",
serde(default, skip_serializing_if = "Option::is_none")
)]
language_hint: Option<Lang>,
#[cfg_attr(feature = "serde", serde(default))]
strategy: SamplingStrategy,
#[cfg_attr(feature = "serde", serde(default = "default_initial_temperature"))]
initial_temperature: f32,
#[cfg_attr(feature = "serde", serde(default = "default_temperature_increment"))]
temperature_increment: f32,
#[cfg_attr(
feature = "serde",
serde(
default = "default_max_attempts",
deserialize_with = "deserialize_nonzero_max_attempts"
)
)]
max_attempts: u8,
#[cfg_attr(feature = "serde", serde(default = "default_log_prob_threshold"))]
log_prob_threshold: f32,
#[cfg_attr(
feature = "serde",
serde(default = "default_compression_ratio_threshold")
)]
compression_ratio_threshold: f32,
#[cfg_attr(feature = "serde", serde(default = "default_no_speech_threshold"))]
no_speech_threshold: f32,
#[cfg_attr(feature = "serde", serde(default = "default_no_context"))]
no_context: bool,
#[cfg_attr(feature = "serde", serde(default = "default_suppress_blank"))]
suppress_blank: bool,
#[cfg_attr(feature = "serde", serde(default))]
suppress_non_speech_tokens: bool,
#[cfg_attr(
feature = "serde",
serde(default, skip_serializing_if = "Option::is_none")
)]
initial_prompt: Option<SmolStr>,
#[cfg_attr(
feature = "serde",
serde(
default = "default_n_threads",
deserialize_with = "deserialize_positive_n_threads"
)
)]
n_threads: i32,
}
#[cfg(feature = "serde")]
const fn default_initial_temperature() -> f32 {
0.0
}
#[cfg(feature = "serde")]
const fn default_temperature_increment() -> f32 {
0.2
}
#[cfg(feature = "serde")]
const fn default_max_attempts() -> u8 {
6
}
#[cfg(feature = "serde")]
fn deserialize_nonzero_max_attempts<'de, D>(deserializer: D) -> Result<u8, D::Error>
where
D: serde::Deserializer<'de>,
{
use serde::de::Error as _;
let v = u8::deserialize(deserializer)?;
if v == 0 {
return Err(D::Error::custom(
"max_attempts must be > 0; use 1 for a single attempt with no retries",
));
}
Ok(v)
}
#[cfg(feature = "serde")]
const fn default_log_prob_threshold() -> f32 {
-1.0
}
#[cfg(feature = "serde")]
const fn default_compression_ratio_threshold() -> f32 {
2.4
}
#[cfg(feature = "serde")]
const fn default_no_speech_threshold() -> f32 {
0.6
}
#[cfg(feature = "serde")]
const fn default_no_context() -> bool {
true
}
#[cfg(feature = "serde")]
const fn default_suppress_blank() -> bool {
true
}
#[cfg(feature = "serde")]
const fn default_n_threads() -> i32 {
1
}
#[cfg(feature = "serde")]
fn deserialize_positive_n_threads<'de, D>(deserializer: D) -> Result<i32, D::Error>
where
D: serde::Deserializer<'de>,
{
use serde::de::Error as _;
let v = i32::deserialize(deserializer)?;
if v < 1 {
return Err(D::Error::custom(format!(
"n_threads must be >= 1 (got {v}); whisper.cpp would underflow / abort otherwise"
)));
}
Ok(v)
}
impl AsrParams {
pub const fn new() -> Self {
Self {
language_hint: None,
strategy: SamplingStrategy::BeamSearch {
beam_size: 5,
patience: -1.0,
},
initial_temperature: 0.0,
temperature_increment: 0.2,
max_attempts: 6,
log_prob_threshold: -1.0,
compression_ratio_threshold: 2.4,
no_speech_threshold: 0.6,
no_context: true,
suppress_blank: true,
suppress_non_speech_tokens: false,
initial_prompt: None,
n_threads: 1,
}
}
pub const fn language_hint(&self) -> Option<&Lang> {
self.language_hint.as_ref()
}
pub const fn strategy(&self) -> SamplingStrategy {
self.strategy
}
pub const fn initial_temperature(&self) -> f32 {
self.initial_temperature
}
pub const fn temperature_increment(&self) -> f32 {
self.temperature_increment
}
pub const fn max_attempts(&self) -> u8 {
self.max_attempts
}
pub const fn log_prob_threshold(&self) -> f32 {
self.log_prob_threshold
}
pub const fn compression_ratio_threshold(&self) -> f32 {
self.compression_ratio_threshold
}
pub const fn no_speech_threshold(&self) -> f32 {
self.no_speech_threshold
}
pub const fn no_context(&self) -> bool {
self.no_context
}
pub const fn suppress_blank(&self) -> bool {
self.suppress_blank
}
pub const fn suppress_non_speech_tokens(&self) -> bool {
self.suppress_non_speech_tokens
}
pub const fn initial_prompt(&self) -> Option<&SmolStr> {
self.initial_prompt.as_ref()
}
pub const fn n_threads(&self) -> i32 {
self.n_threads
}
pub fn set_language_hint(&mut self, value: Option<Lang>) {
self.language_hint = value;
}
pub const fn set_strategy(&mut self, value: SamplingStrategy) {
self.strategy = value;
}
pub const fn set_initial_temperature(&mut self, value: f32) {
self.initial_temperature = value;
}
pub const fn set_temperature_increment(&mut self, value: f32) {
self.temperature_increment = value;
}
pub const fn set_max_attempts(&mut self, value: u8) {
assert!(
value > 0,
"max_attempts must be > 0 (got 0); use 1 for a single attempt with no retries"
);
self.max_attempts = value;
}
pub const fn set_log_prob_threshold(&mut self, value: f32) {
self.log_prob_threshold = value;
}
pub const fn set_compression_ratio_threshold(&mut self, value: f32) {
self.compression_ratio_threshold = value;
}
pub const fn set_no_speech_threshold(&mut self, value: f32) {
self.no_speech_threshold = value;
}
pub const fn set_no_context(&mut self, value: bool) {
self.no_context = value;
}
pub const fn set_suppress_blank(&mut self, value: bool) {
self.suppress_blank = value;
}
pub const fn set_suppress_non_speech_tokens(&mut self, value: bool) {
self.suppress_non_speech_tokens = value;
}
pub fn set_initial_prompt(&mut self, value: Option<SmolStr>) {
self.initial_prompt = value;
}
pub const fn set_n_threads(&mut self, value: i32) {
assert!(
value >= 1,
"n_threads must be >= 1; whisper.cpp would underflow / abort otherwise"
);
self.n_threads = value;
}
pub fn with_language_hint(mut self, value: Option<Lang>) -> Self {
self.language_hint = value;
self
}
pub const fn with_strategy(mut self, value: SamplingStrategy) -> Self {
self.strategy = value;
self
}
pub const fn with_initial_temperature(mut self, value: f32) -> Self {
self.initial_temperature = value;
self
}
pub const fn with_temperature_increment(mut self, value: f32) -> Self {
self.temperature_increment = value;
self
}
pub const fn with_max_attempts(mut self, value: u8) -> Self {
assert!(
value > 0,
"max_attempts must be > 0 (got 0); use 1 for a single attempt with no retries"
);
self.max_attempts = value;
self
}
pub const fn with_log_prob_threshold(mut self, value: f32) -> Self {
self.log_prob_threshold = value;
self
}
pub const fn with_compression_ratio_threshold(mut self, value: f32) -> Self {
self.compression_ratio_threshold = value;
self
}
pub const fn with_no_speech_threshold(mut self, value: f32) -> Self {
self.no_speech_threshold = value;
self
}
pub const fn with_no_context(mut self, value: bool) -> Self {
self.no_context = value;
self
}
pub const fn with_suppress_blank(mut self, value: bool) -> Self {
self.suppress_blank = value;
self
}
pub const fn with_suppress_non_speech_tokens(mut self, value: bool) -> Self {
self.suppress_non_speech_tokens = value;
self
}
pub fn with_initial_prompt(mut self, value: Option<SmolStr>) -> Self {
self.initial_prompt = value;
self
}
pub const fn with_n_threads(mut self, value: i32) -> Self {
assert!(
value >= 1,
"n_threads must be >= 1; whisper.cpp would underflow / abort otherwise"
);
self.n_threads = value;
self
}
}
impl Default for AsrParams {
fn default() -> Self {
Self::new()
}
}
#[derive(Copy, Clone, Debug)]
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
#[cfg_attr(feature = "serde", serde(rename_all = "snake_case"))]
pub enum SamplingStrategy {
Greedy {
best_of: i32,
},
BeamSearch {
beam_size: i32,
patience: f32,
},
}
impl Default for SamplingStrategy {
fn default() -> Self {
Self::BeamSearch {
beam_size: 5,
patience: -1.0,
}
}
}
#[derive(Clone, Debug)]
pub struct AsrResult {
text: SmolStr,
language: Lang,
avg_logprob: f32,
no_speech_prob: f32,
temperature: f32,
runs: Vec<crate::align::Run>,
}
impl AsrResult {
pub fn new(
text: SmolStr,
language: Lang,
avg_logprob: f32,
no_speech_prob: f32,
temperature: f32,
) -> Self {
Self {
text,
language,
avg_logprob,
no_speech_prob,
temperature,
runs: Vec::new(),
}
}
pub fn text(&self) -> &SmolStr {
&self.text
}
pub fn language(&self) -> &Lang {
&self.language
}
pub const fn avg_logprob(&self) -> f32 {
self.avg_logprob
}
pub const fn no_speech_prob(&self) -> f32 {
self.no_speech_prob
}
pub const fn temperature(&self) -> f32 {
self.temperature
}
pub fn runs(&self) -> &[crate::align::Run] {
&self.runs
}
#[must_use]
pub fn with_runs(mut self, runs: Vec<crate::align::Run>) -> Self {
self.runs = runs;
self
}
pub fn set_runs(&mut self, runs: Vec<crate::align::Run>) {
self.runs = runs;
}
}
#[derive(Clone, Debug)]
#[cfg(feature = "alignment")]
pub struct AlignmentResult {
words: Vec<crate::types::Word>,
}
#[cfg(feature = "alignment")]
impl AlignmentResult {
pub fn new(words: Vec<crate::types::Word>) -> Self {
Self { words }
}
pub fn words(&self) -> &[crate::types::Word] {
&self.words
}
pub fn into_words(self) -> Vec<crate::types::Word> {
self.words
}
}
#[derive(Clone, Debug)]
#[cfg(not(feature = "alignment"))]
pub struct AlignmentResult {
words: Vec<crate::types::Word>,
}
#[cfg(not(feature = "alignment"))]
impl AlignmentResult {
pub fn new(words: Vec<crate::types::Word>) -> Self {
Self { words }
}
pub fn words(&self) -> &[crate::types::Word] {
&self.words
}
pub fn into_words(self) -> Vec<crate::types::Word> {
self.words
}
}
#[derive(Debug)]
pub enum Command {
Asr {
chunk_id: ChunkId,
samples: Arc<[f32]>,
sample_rate: u32,
params: AsrParams,
},
Alignment {
chunk_id: ChunkId,
samples: Arc<[f32]>,
sub_segments: Vec<TimeRange>,
text: SmolStr,
language: Lang,
runs: Vec<crate::align::Run>,
},
}
#[derive(Clone, Debug, Default)]
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
pub struct AsrParamsOverride {
#[cfg_attr(
feature = "serde",
serde(
default,
skip_serializing_if = "Option::is_none",
deserialize_with = "deserialize_double_option_lang"
)
)]
language_hint: Option<Option<Lang>>,
#[cfg_attr(
feature = "serde",
serde(default, skip_serializing_if = "Option::is_none")
)]
strategy: Option<SamplingStrategy>,
#[cfg_attr(
feature = "serde",
serde(default, skip_serializing_if = "Option::is_none")
)]
initial_temperature: Option<f32>,
#[cfg_attr(
feature = "serde",
serde(
default,
skip_serializing_if = "Option::is_none",
deserialize_with = "deserialize_double_option_smolstr"
)
)]
initial_prompt: Option<Option<SmolStr>>,
}
#[cfg(feature = "serde")]
fn deserialize_double_option_lang<'de, D>(d: D) -> Result<Option<Option<Lang>>, D::Error>
where
D: serde::Deserializer<'de>,
{
Ok(Some(Option::<Lang>::deserialize(d)?))
}
#[cfg(feature = "serde")]
fn deserialize_double_option_smolstr<'de, D>(d: D) -> Result<Option<Option<SmolStr>>, D::Error>
where
D: serde::Deserializer<'de>,
{
Ok(Some(Option::<SmolStr>::deserialize(d)?))
}
impl AsrParamsOverride {
pub const fn new() -> Self {
Self {
language_hint: None,
strategy: None,
initial_temperature: None,
initial_prompt: None,
}
}
pub fn apply_to(&self, base: &AsrParams) -> AsrParams {
let mut out = base.clone();
if let Some(opt_lang) = &self.language_hint {
out.set_language_hint(opt_lang.clone());
}
if let Some(strategy) = self.strategy {
out.set_strategy(strategy);
}
if let Some(t) = self.initial_temperature {
out.set_initial_temperature(t);
}
if let Some(prompt) = &self.initial_prompt {
out.set_initial_prompt(prompt.clone());
}
out
}
pub const fn language_hint(&self) -> Option<&Option<Lang>> {
self.language_hint.as_ref()
}
pub const fn strategy(&self) -> Option<SamplingStrategy> {
self.strategy
}
pub const fn initial_temperature(&self) -> Option<f32> {
self.initial_temperature
}
pub const fn initial_prompt(&self) -> Option<&Option<SmolStr>> {
self.initial_prompt.as_ref()
}
pub fn set_language_hint(&mut self, value: Option<Option<Lang>>) {
self.language_hint = value;
}
pub const fn set_strategy(&mut self, value: Option<SamplingStrategy>) {
self.strategy = value;
}
pub const fn set_initial_temperature(&mut self, value: Option<f32>) {
self.initial_temperature = value;
}
pub fn set_initial_prompt(&mut self, value: Option<Option<SmolStr>>) {
self.initial_prompt = value;
}
pub fn with_language_hint(mut self, value: Option<Option<Lang>>) -> Self {
self.language_hint = value;
self
}
pub const fn with_strategy(mut self, value: Option<SamplingStrategy>) -> Self {
self.strategy = value;
self
}
pub const fn with_initial_temperature(mut self, value: Option<f32>) -> Self {
self.initial_temperature = value;
self
}
pub fn with_initial_prompt(mut self, value: Option<Option<SmolStr>>) -> Self {
self.initial_prompt = value;
self
}
}
#[allow(dead_code)] pub(crate) type ChunkAudio = Arc<[f32]>;
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn asr_params_defaults_match_spec() {
let p = AsrParams::default();
match p.strategy {
SamplingStrategy::BeamSearch {
beam_size,
patience,
} => {
assert_eq!(beam_size, 5);
assert!((patience - -1.0).abs() < 1e-9);
}
_ => panic!("default should be BeamSearch"),
}
assert!((p.initial_temperature - 0.0).abs() < 1e-9);
assert!((p.temperature_increment - 0.2).abs() < 1e-9);
assert_eq!(p.max_attempts, 6);
assert!(p.no_context);
}
#[cfg(feature = "serde")]
#[test]
fn asr_params_serde_round_trip() {
let mut p = AsrParams::default();
p.set_initial_temperature(0.7);
p.set_max_attempts(3);
let json = serde_json::to_string(&p).expect("serialize");
let back: AsrParams = serde_json::from_str(&json).expect("deserialize");
assert!((back.initial_temperature() - 0.7).abs() < 1e-9);
assert_eq!(back.max_attempts(), 3);
}
#[test]
#[should_panic(expected = "max_attempts must be > 0")]
fn set_max_attempts_zero_panics() {
let mut p = AsrParams::default();
p.set_max_attempts(0);
}
#[test]
#[should_panic(expected = "max_attempts must be > 0")]
fn with_max_attempts_zero_panics() {
let _ = AsrParams::default().with_max_attempts(0);
}
#[test]
#[should_panic(expected = "n_threads must be >= 1")]
fn set_n_threads_zero_panics() {
let mut p = AsrParams::default();
p.set_n_threads(0);
}
#[test]
#[should_panic(expected = "n_threads must be >= 1")]
fn set_n_threads_negative_panics() {
let mut p = AsrParams::default();
p.set_n_threads(-3);
}
#[test]
#[should_panic(expected = "n_threads must be >= 1")]
fn with_n_threads_zero_panics() {
let _ = AsrParams::default().with_n_threads(0);
}
#[cfg(feature = "serde")]
#[test]
fn deserialize_rejects_zero_n_threads() {
let json = r#"{"n_threads": 0}"#;
let res: Result<AsrParams, _> = serde_json::from_str(json);
assert!(res.is_err(), "n_threads=0 must be rejected");
let err = res.err().unwrap().to_string();
assert!(err.contains("n_threads must be >= 1"), "got {err:?}");
}
#[cfg(feature = "serde")]
#[test]
fn deserialize_rejects_negative_n_threads() {
let json = r#"{"n_threads": -2}"#;
let res: Result<AsrParams, _> = serde_json::from_str(json);
assert!(res.is_err(), "n_threads=-2 must be rejected");
}
#[cfg(feature = "serde")]
#[test]
fn deserialize_rejects_zero_max_attempts() {
let json = r#"{"max_attempts": 0}"#;
let res: Result<AsrParams, _> = serde_json::from_str(json);
assert!(res.is_err(), "max_attempts=0 must be rejected");
let err = res.err().unwrap().to_string();
assert!(
err.contains("max_attempts must be > 0"),
"expected diagnostic, got {err:?}"
);
}
#[cfg(feature = "serde")]
#[test]
fn asr_params_serde_empty_yields_defaults() {
let p: AsrParams = serde_json::from_str("{}").expect("deserialize empty");
assert_eq!(
p.initial_temperature(),
AsrParams::default().initial_temperature()
);
assert_eq!(p.max_attempts(), AsrParams::default().max_attempts());
assert_eq!(p.no_context(), AsrParams::default().no_context());
assert!(p.language_hint().is_none());
assert!(p.initial_prompt().is_none());
}
#[cfg(feature = "serde")]
#[test]
fn asr_params_override_serde_absent_means_no_override() {
let ovr: AsrParamsOverride = serde_json::from_str("{}").expect("deserialize empty");
assert!(
ovr.language_hint().is_none(),
"absent field must mean None (no override)"
);
assert!(
ovr.initial_prompt().is_none(),
"absent field must mean None (no override)"
);
}
#[cfg(feature = "serde")]
#[test]
fn asr_params_override_serde_null_means_clear() {
let ovr: AsrParamsOverride =
serde_json::from_str(r#"{"language_hint": null}"#).expect("deserialize null");
match ovr.language_hint() {
Some(None) => {}
other => panic!("JSON null on language_hint must produce Some(None) (clear); got {other:?}"),
}
let ovr: AsrParamsOverride =
serde_json::from_str(r#"{"initial_prompt": null}"#).expect("deserialize null");
match ovr.initial_prompt() {
Some(None) => {}
other => panic!("JSON null on initial_prompt must produce Some(None) (clear); got {other:?}"),
}
}
#[cfg(feature = "serde")]
#[test]
fn asr_params_override_serde_value_means_set() {
let ovr: AsrParamsOverride =
serde_json::from_str(r#"{"language_hint": "EN"}"#).expect("deserialize value");
match ovr.language_hint() {
Some(Some(Lang::En)) => {}
other => panic!("expected Some(Some(Lang::En)); got {other:?}"),
}
let ovr: AsrParamsOverride =
serde_json::from_str(r#"{"initial_prompt": "hint"}"#).expect("deserialize value");
match ovr.initial_prompt() {
Some(Some(s)) if s.as_str() == "hint" => {}
other => panic!("expected Some(Some(\"hint\")); got {other:?}"),
}
}
#[cfg(feature = "serde")]
#[test]
fn asr_params_override_serde_round_trips_three_states() {
let mut ovr_absent = AsrParamsOverride::new();
ovr_absent.set_initial_temperature(Some(0.7)); let json = serde_json::to_string(&ovr_absent).unwrap();
assert!(
!json.contains("language_hint") && !json.contains("initial_prompt"),
"absent fields must skip-serialize; got {json}"
);
let back: AsrParamsOverride = serde_json::from_str(&json).unwrap();
assert!(back.language_hint().is_none());
assert!(back.initial_prompt().is_none());
let ovr_clear = AsrParamsOverride::new()
.with_language_hint(Some(None))
.with_initial_prompt(Some(None));
let json = serde_json::to_string(&ovr_clear).unwrap();
assert!(json.contains("\"language_hint\":null"), "got {json}");
assert!(json.contains("\"initial_prompt\":null"), "got {json}");
let back: AsrParamsOverride = serde_json::from_str(&json).unwrap();
assert!(matches!(back.language_hint(), Some(None)));
assert!(matches!(back.initial_prompt(), Some(None)));
let ovr_set = AsrParamsOverride::new()
.with_language_hint(Some(Some(Lang::En)))
.with_initial_prompt(Some(Some(SmolStr::new("hint"))));
let json = serde_json::to_string(&ovr_set).unwrap();
let back: AsrParamsOverride = serde_json::from_str(&json).unwrap();
assert!(matches!(back.language_hint(), Some(Some(Lang::En))));
assert!(
matches!(back.initial_prompt(), Some(Some(s)) if s.as_str() == "hint"),
"got {:?}",
back.initial_prompt()
);
}
#[cfg(feature = "serde")]
#[test]
fn sampling_strategy_serde_uses_snake_case() {
let strat = SamplingStrategy::Greedy { best_of: 1 };
let json = serde_json::to_string(&strat).expect("serialize");
assert!(
json.contains("greedy"),
"external rep must be snake_case; got {json}"
);
let back: SamplingStrategy = serde_json::from_str(&json).expect("deserialize");
match back {
SamplingStrategy::Greedy { best_of } => assert_eq!(best_of, 1),
_ => panic!("expected Greedy"),
}
}
}