use std::sync::{
Arc,
atomic::{AtomicBool, Ordering},
};
use whispercpp::{
Params as FullParams, SamplingStrategy as WhisperStrategy, State as WhisperState,
};
use smol_str::{SmolStr, format_smolstr};
use crate::{
core::{AsrParams, AsrResult, SamplingStrategy},
types::{AsrError, AsrFailure, ChunkId, Lang, WorkFailure, WorkerHangTimeout, WorkerKind},
};
const MAX_LANGUAGE_CODE_LEN: usize = 8;
fn validate_language_code(s: &str) -> Result<(), &'static str> {
if s.is_empty() {
return Err("language code is empty");
}
if s.len() > MAX_LANGUAGE_CODE_LEN {
return Err("language code longer than 8 bytes (whisper.cpp codes are 2–3 ASCII letters)");
}
if !s.bytes().all(|b| b.is_ascii_lowercase()) {
return Err("language code must be lowercase ASCII letters [a-z] only");
}
Ok(())
}
pub(in crate::runner) struct AsrWorkItem {
pub chunk_id: ChunkId,
pub samples: Arc<[f32]>,
pub params: AsrParams,
pub abort_flag: Arc<AtomicBool>,
}
pub(in crate::runner) fn validate_for_whisper_ffi(params: &AsrParams) -> Result<(), WorkFailure> {
if let Some(lang) = params.language_hint()
&& let Err(reason) = validate_language_code(lang.as_str())
{
return Err(WorkFailure::Asr(AsrError::Backend(AsrFailure::new(
format_smolstr!(
"language hint rejected: {reason}. Reject before FFI so callers see a stable \
diagnostic; whispercpp's `Params::set_language` would otherwise return \
`InputTooLong` / `UnknownLanguage` for the same input."
),
))));
}
if let Some(prompt) = params.initial_prompt()
&& prompt.as_str().contains('\0')
{
return Err(WorkFailure::Asr(AsrError::Backend(AsrFailure::new(
format_smolstr!(
"initial_prompt of len {} contains an interior NUL byte; whisper-rs's set_initial_prompt \
would panic. Reject before FFI.",
prompt.as_str().len()
),
))));
}
if params.n_threads() < 1 {
return Err(WorkFailure::Asr(AsrError::Backend(AsrFailure::new(
format_smolstr!(
"n_threads must be >= 1 (got {}); whisper.cpp's std::vector<std::thread>({} - 1) \
would underflow / abort. Reject before FFI.",
params.n_threads(),
params.n_threads(),
),
))));
}
match params.strategy() {
SamplingStrategy::Greedy { best_of } => {
if best_of < 1 {
return Err(WorkFailure::Asr(AsrError::Backend(AsrFailure::new(
format_smolstr!(
"SamplingStrategy::Greedy.best_of must be >= 1 (got {best_of}); \
whisper.cpp would either abort or fall back to greedy=1 silently. \
Reject before FFI."
),
))));
}
}
SamplingStrategy::BeamSearch {
beam_size,
patience,
} => {
if beam_size < 1 {
return Err(WorkFailure::Asr(AsrError::Backend(AsrFailure::new(
format_smolstr!(
"SamplingStrategy::BeamSearch.beam_size must be >= 1 (got {beam_size}); \
whisper.cpp's beam-search loop would underflow on a non-positive size. \
Reject before FFI."
),
))));
}
if !patience.is_finite() || (patience <= 0.0 && (patience - -1.0).abs() > f32::EPSILON) {
return Err(WorkFailure::Asr(AsrError::Backend(AsrFailure::new(
format_smolstr!(
"SamplingStrategy::BeamSearch.patience must be a finite positive value \
(or the documented `-1.0` whisper.cpp default sentinel); got {patience}. \
Reject before FFI."
),
))));
}
}
}
for (name, value) in [
("initial_temperature", params.initial_temperature()),
("temperature_increment", params.temperature_increment()),
("log_prob_threshold", params.log_prob_threshold()),
(
"compression_ratio_threshold",
params.compression_ratio_threshold(),
),
("no_speech_threshold", params.no_speech_threshold()),
] {
if !value.is_finite() {
return Err(WorkFailure::Asr(AsrError::Backend(AsrFailure::new(
format_smolstr!(
"AsrParams::{name} must be finite (got {value}); non-finite values are \
either undefined to whisper.cpp's sampler or make the post-decode \
gates degenerate. Reject before FFI."
),
))));
}
}
let init_t = params.initial_temperature();
if !(0.0..=1.0).contains(&init_t) {
return Err(WorkFailure::Asr(AsrError::Backend(AsrFailure::new(
format_smolstr!(
"AsrParams::initial_temperature must be in [0.0, 1.0] (got {init_t}); \
WhisperX sampling temperatures are probabilities."
),
))));
}
let step = params.temperature_increment();
if !(0.0..=1.0).contains(&step) {
return Err(WorkFailure::Asr(AsrError::Backend(AsrFailure::new(
format_smolstr!(
"AsrParams::temperature_increment must be in [0.0, 1.0] (got {step}); \
a step outside this range either skips meaningful retries or wraps \
past the [0, 1] sampler domain."
),
))));
}
let nsp = params.no_speech_threshold();
if !(0.0..=1.0).contains(&nsp) {
return Err(WorkFailure::Asr(AsrError::Backend(AsrFailure::new(
format_smolstr!(
"AsrParams::no_speech_threshold must be in [0.0, 1.0] (got {nsp}); \
it gates against per-segment no-speech probabilities."
),
))));
}
let cratio = params.compression_ratio_threshold();
if cratio <= 0.0 {
return Err(WorkFailure::Asr(AsrError::Backend(AsrFailure::new(
format_smolstr!(
"AsrParams::compression_ratio_threshold must be > 0 (got {cratio}); \
WhisperX's gate compares `len(text) / len(zlib(text))` to a positive \
ceiling — a non-positive ceiling rejects every transcript."
),
))));
}
Ok(())
}
fn build_template(params: &AsrParams) -> Result<FullParams, WorkFailure> {
let strategy = match params.strategy() {
SamplingStrategy::Greedy { best_of } => WhisperStrategy::Greedy { best_of },
SamplingStrategy::BeamSearch {
beam_size,
patience,
} => WhisperStrategy::BeamSearch {
beam_size,
patience,
},
};
let mut p = FullParams::new(strategy);
if let Some(lang) = params.language_hint() {
p.set_language(lang.as_str()).map_err(|e| {
WorkFailure::Asr(AsrError::Backend(AsrFailure::new(format_smolstr!(
"set_language({}) rejected by whisper.cpp: {e}",
lang.as_str()
))))
})?;
} else {
p.set_detect_language(true);
}
if let Some(prompt) = params.initial_prompt() {
if let Err(e) = p.set_initial_prompt(prompt.as_str()) {
eprintln!(
"asry asr initial_prompt rejected by whisper.cpp; \
continuing without prompt prompt_chars={} error={e:?}",
prompt.chars().count(),
);
}
}
Ok(p)
}
fn finalize_chunk(
mut full: FullParams,
params: &AsrParams,
abort_flag: Arc<AtomicBool>,
) -> FullParams {
full.set_n_threads(params.n_threads());
full.set_no_context(params.no_context());
full.set_suppress_blank(params.suppress_blank());
full.set_suppress_nst(params.suppress_non_speech_tokens());
full.silence_print_toggles();
full.set_no_speech_thold(params.no_speech_threshold());
full.set_temperature_inc(0.0);
full.set_abort_callback(move || abort_flag.load(Ordering::Relaxed));
full
}
pub(super) fn full_params_from(
params: &AsrParams,
attempt_temperature: f32,
abort_flag: Arc<AtomicBool>,
) -> Result<FullParams, WorkFailure> {
validate_for_whisper_ffi(params)?;
let template = build_template(params)?;
let mut full = finalize_chunk(template, params, abort_flag);
full.set_temperature(attempt_temperature);
Ok(full)
}
pub(super) fn compute_avg_logprob(state: &WhisperState) -> f32 {
let n = state.n_segments();
if n <= 0 {
return f32::MIN;
}
let mut seg_sum = 0.0f64;
let mut seg_count = 0i32;
for i in 0..n {
let Some(segment) = state.segment(i) else {
continue;
};
let n_tok = segment.n_tokens();
if n_tok <= 0 {
continue;
}
let mut tok_sum = 0.0f64;
let mut tok_count = 0i32;
for j in 0..n_tok {
if let Some(token) = segment.token(j) {
tok_sum += token.plog() as f64;
tok_count += 1;
}
}
if tok_count == 0 {
continue;
}
seg_sum += tok_sum / tok_count as f64;
seg_count += 1;
}
if seg_count == 0 {
f32::MIN
} else {
(seg_sum / seg_count as f64) as f32
}
}
pub(super) fn compute_avg_no_speech_prob(state: &WhisperState) -> f32 {
let n = state.n_segments();
if n <= 0 {
return 0.0;
}
let mut sum = 0.0_f32;
let mut count = 0_i32;
for i in 0..n {
if let Some(segment) = state.segment(i) {
sum += segment.no_speech_prob();
count += 1;
}
}
if count == 0 {
return 0.0;
}
sum / count as f32
}
pub(super) fn compute_compression_ratio(state: &WhisperState) -> f32 {
let n = state.n_segments();
if n <= 0 {
return 0.0;
}
let mut text = String::new();
for i in 0..n {
if let Some(segment) = state.segment(i)
&& let Ok(s) = segment.text()
{
text.push_str(s);
}
}
compression_ratio_for_text(&text)
}
pub(super) fn compression_ratio_for_text(text: &str) -> f32 {
use miniz_oxide::deflate::compress_to_vec_zlib;
const MIN_RATIO_BYTES: usize = 32;
let raw = text.len();
if raw < MIN_RATIO_BYTES {
return 0.0;
}
let compressed_len = compress_to_vec_zlib(text.as_bytes(), 6).len();
if compressed_len == 0 {
return 0.0;
}
raw as f32 / compressed_len as f32
}
pub(in crate::runner) fn run_with_temperature_ladder(
state: &mut WhisperState,
job: &AsrWorkItem,
started_at: std::time::Instant,
) -> Result<AsrResult, WorkFailure> {
let p = &job.params;
let mut temperature = p.initial_temperature();
let max = p.max_attempts() as usize;
validate_for_whisper_ffi(p)?;
for _attempt in 0..max {
if job.abort_flag.load(Ordering::Relaxed) {
return Err(WorkFailure::WorkerHang(WorkerHangTimeout::new(
WorkerKind::Asr,
started_at.elapsed(),
)));
}
if !temperature.is_finite() {
return Err(WorkFailure::Asr(AsrError::Backend(AsrFailure::new(
format_smolstr!(
"ladder temperature became non-finite ({temperature}) after starting from \
initial={} step={}; refuse to forward into whisper.cpp's sampler",
p.initial_temperature(),
p.temperature_increment(),
),
))));
}
if !(0.0..=1.0).contains(&temperature) {
return Err(WorkFailure::Asr(AsrError::AllTemperaturesExhausted(
AsrFailure::new(format_smolstr!(
"temperature ladder exhausted at attempt-derived value {temperature} \
outside [0.0, 1.0] (initial={}, step={}); WhisperX caps the \
fallback ladder at 1.0",
p.initial_temperature(),
p.temperature_increment(),
)),
)));
}
let template = build_template(p)?;
let mut full = finalize_chunk(template, p, job.abort_flag.clone());
full.set_temperature(temperature);
let outcome = state.full(&full, job.samples.as_ref());
if job.abort_flag.load(Ordering::Relaxed) {
return Err(WorkFailure::WorkerHang(WorkerHangTimeout::new(
WorkerKind::Asr,
started_at.elapsed(),
)));
}
if let Err(e) = outcome {
return Err(WorkFailure::Asr(AsrError::Backend(AsrFailure::new(
format_smolstr!("{e}"),
))));
}
if state.n_segments() == 0 {
return build_asr_result(state, temperature, p);
}
let logprob = compute_avg_logprob(state);
let cratio = compute_compression_ratio(state);
let nsp = compute_avg_no_speech_prob(state);
let logprob_ok = logprob >= p.log_prob_threshold();
let cratio_ok = cratio <= p.compression_ratio_threshold();
if nsp > p.no_speech_threshold() && !logprob_ok {
return Ok(AsrResult::new(
SmolStr::new(""),
p.language_hint()
.cloned()
.unwrap_or(Lang::Other(SmolStr::new(""))),
logprob,
nsp,
temperature,
));
}
if logprob_ok && cratio_ok {
return build_asr_result(state, temperature, p);
}
temperature += p.temperature_increment();
}
Err(WorkFailure::Asr(AsrError::AllTemperaturesExhausted(
AsrFailure::new(format_smolstr!(
"all {} temperature attempts failed for chunk {:?}",
max,
job.chunk_id,
)),
)))
}
fn build_asr_result(
state: &WhisperState,
final_temperature: f32,
params: &AsrParams,
) -> Result<AsrResult, WorkFailure> {
let n = state.n_segments();
let mut text = String::new();
let mut nsp_sum = 0.0f32;
let mut nsp_count: i32 = 0;
for i in 0..n {
if let Some(segment) = state.segment(i) {
if let Ok(s) = segment.text() {
text.push_str(s);
}
nsp_sum += segment.no_speech_prob();
nsp_count += 1;
}
}
let avg_logprob = compute_avg_logprob(state);
let no_speech_prob: f32 = if nsp_count > 0 {
nsp_sum / nsp_count as f32
} else {
0.0
};
let language = state.detected_lang().map(Lang::from).unwrap_or_else(|| {
params
.language_hint()
.cloned()
.unwrap_or(Lang::Other(SmolStr::new("")))
});
let segments: Vec<whispercpp::Segment<'_>> = state.segments_iter().collect();
let ctx_for_dispatch = state.context();
let runs = crate::align::dispatch(ctx_for_dispatch, &segments, Some(language.clone()));
Ok(
AsrResult::new(
SmolStr::new(text.trim()),
language,
avg_logprob,
no_speech_prob,
final_temperature,
)
.with_runs(runs),
)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn asr_work_item_is_send() {
fn assert_send<T: Send>() {}
assert_send::<AsrWorkItem>();
}
#[test]
fn compression_ratio_short_transcripts_pass_default_threshold() {
let threshold = AsrParams::default().compression_ratio_threshold();
for sample in ["test", "hello", "yes", "Hi.", "A.", "transcript."] {
let r = compression_ratio_for_text(sample);
assert!(
r <= threshold,
"short transcript {sample:?} ({} bytes) ratio={r} above default threshold {threshold}",
sample.len(),
);
}
}
#[test]
fn compression_ratio_runaway_repetition_above_threshold() {
let threshold = AsrParams::default().compression_ratio_threshold();
let runaway = "yes ".repeat(64);
let r = compression_ratio_for_text(&runaway);
assert!(
r > threshold,
"runaway repetition ratio {r} should exceed threshold {threshold}",
);
}
#[test]
fn compression_ratio_legit_repetition_under_threshold() {
let threshold = AsrParams::default().compression_ratio_threshold();
let legit = "thank you ".repeat(4);
let r = compression_ratio_for_text(&legit);
assert!(
r <= threshold,
"legit repetitive transcript {legit:?} ratio={r} must not exceed threshold {threshold}",
);
}
use crate::{
core::{AsrParams, SamplingStrategy},
types::Lang,
};
use core::sync::atomic::AtomicBool;
#[test]
fn full_params_from_greedy_is_finite() {
let p = AsrParams::default().with_strategy(SamplingStrategy::Greedy { best_of: 1 });
let flag = Arc::new(AtomicBool::new(false));
let _full = full_params_from(&p, 0.4, flag).expect("valid params");
}
#[test]
fn full_params_from_with_language_hint_does_not_panic() {
let p = AsrParams::default().with_language_hint(Some(Lang::En));
let flag = Arc::new(AtomicBool::new(false));
let _full = full_params_from(&p, 0.0, flag).expect("valid params");
}
#[test]
fn full_params_from_propagates_unknown_language_from_whispercpp() {
let p = AsrParams::default().with_language_hint(Some(Lang::Other(SmolStr::from("zzzz"))));
let flag = Arc::new(AtomicBool::new(false));
let res = full_params_from(&p, 0.0, flag);
match res {
Err(WorkFailure::Asr(AsrError::Backend(payload))) => {
assert!(
payload.message().contains("set_language") && payload.message().contains("zzzz"),
"expected set_language diagnostic mentioning the offending code; got {message}",
message = payload.message()
);
}
other => panic!("expected AsrFailed/BackendError for unknown language; got {other:?}"),
}
}
#[test]
fn full_params_from_rejects_interior_nul_in_language_hint() {
let p = AsrParams::default().with_language_hint(Some(Lang::Other(SmolStr::from("xx\0yy"))));
let flag = Arc::new(AtomicBool::new(false));
let res = full_params_from(&p, 0.0, flag);
match res {
Err(WorkFailure::Asr(AsrError::Backend(payload))) => {
assert!(
payload.message().contains("language hint")
&& payload.message().contains("lowercase ASCII"),
"expected charset-violation diagnostic; got {message}",
message = payload.message()
);
}
other => panic!("expected AsrFailed/BackendError; got {other:?}"),
}
}
#[test]
fn validate_language_code_accepts_iso_shapes() {
assert!(validate_language_code("en").is_ok());
assert!(validate_language_code("es").is_ok());
assert!(validate_language_code("zh").is_ok());
assert!(validate_language_code("yue").is_ok()); assert!(validate_language_code("haw").is_ok()); assert!(validate_language_code("a").is_ok()); assert!(validate_language_code("abcdefgh").is_ok()); }
#[test]
fn validate_language_code_rejects_empty() {
let err = validate_language_code("").unwrap_err();
assert!(err.contains("empty"), "got {err}");
}
#[test]
fn validate_language_code_rejects_overlong() {
let err = validate_language_code("abcdefghi").unwrap_err(); assert!(err.contains("longer than"), "got {err}");
let err2 = validate_language_code(&"x".repeat(64)).unwrap_err();
assert!(err2.contains("longer than"), "got {err2}");
}
#[test]
fn validate_language_code_rejects_uppercase() {
let err = validate_language_code("EN").unwrap_err();
assert!(err.contains("lowercase ASCII"), "got {err}");
}
#[test]
fn validate_language_code_rejects_dash_or_digits() {
assert!(validate_language_code("zh-tw").is_err());
assert!(validate_language_code("zh1").is_err());
}
#[test]
fn validate_language_code_rejects_non_ascii() {
assert!(validate_language_code("français").is_err());
assert!(validate_language_code("a\0b").is_err());
assert!(validate_language_code("a\nb").is_err());
}
#[test]
fn full_params_from_rejects_high_cardinality_language_hint() {
let p = AsrParams::default().with_language_hint(Some(Lang::Other(SmolStr::from(
"very-long-attacker-string",
))));
let flag = Arc::new(AtomicBool::new(false));
let res = full_params_from(&p, 0.0, flag);
match res {
Err(WorkFailure::Asr(AsrError::Backend(_))) => {}
other => panic!("expected AsrFailed/BackendError; got {other:?}"),
}
}
#[test]
fn full_params_from_rejects_interior_nul_in_initial_prompt() {
let p = AsrParams::default().with_initial_prompt(Some(SmolStr::from("hint\0poison")));
let flag = Arc::new(AtomicBool::new(false));
let res = full_params_from(&p, 0.0, flag);
match res {
Err(WorkFailure::Asr(AsrError::Backend(payload))) => {
assert!(
payload.message().contains("initial_prompt") && payload.message().contains("NUL"),
"expected NUL diagnostic; got {message}",
message = payload.message()
);
}
other => panic!("expected AsrFailed/BackendError; got {other:?}"),
}
}
fn compression_ratio_of_text(text: &str) -> f32 {
use std::collections::HashSet;
let raw = text.len();
if raw < 4 {
return 0.0;
}
let bytes = text.as_bytes();
let mut shingles: HashSet<[u8; 4]> = HashSet::with_capacity(raw);
for w in bytes.windows(4) {
let mut s = [0u8; 4];
s.copy_from_slice(w);
shingles.insert(s);
}
if shingles.is_empty() {
0.0
} else {
raw as f32 / shingles.len() as f32
}
}
#[test]
fn compression_ratio_low_for_diverse_text() {
let r = compression_ratio_of_text("the quick brown fox jumps over the lazy dog");
assert!(r < 1.5, "diverse text ratio = {}", r);
}
#[test]
fn compression_ratio_high_for_repeated_text() {
let r = compression_ratio_of_text("yes yes yes yes yes yes yes yes yes yes ");
assert!(
r >= 2.4,
"repeated text ratio should trip the 2.4 default; got {}",
r
);
}
#[test]
fn compression_ratio_short_input_returns_zero() {
assert_eq!(compression_ratio_of_text(""), 0.0);
assert_eq!(compression_ratio_of_text("ab"), 0.0);
}
#[test]
fn validate_for_whisper_ffi_rejects_zero_best_of() {
let p = AsrParams::default().with_strategy(SamplingStrategy::Greedy { best_of: 0 });
let err = validate_for_whisper_ffi(&p).unwrap_err();
match err {
WorkFailure::Asr(AsrError::Backend(payload)) => {
assert!(
payload.message().contains("best_of"),
"got {message}",
message = payload.message()
);
}
other => panic!("expected AsrError::Backend, got {other:?}"),
}
}
#[test]
fn validate_for_whisper_ffi_rejects_zero_beam_size() {
let p = AsrParams::default().with_strategy(SamplingStrategy::BeamSearch {
beam_size: 0,
patience: 1.0,
});
let err = validate_for_whisper_ffi(&p).unwrap_err();
match err {
WorkFailure::Asr(asr) => {
let message = asr.to_string();
assert!(message.contains("beam_size"), "got {message}");
}
other => panic!("expected AsrFailed, got {other:?}"),
}
}
#[test]
fn validate_for_whisper_ffi_rejects_non_finite_patience() {
for bad in [f32::NAN, f32::INFINITY, f32::NEG_INFINITY, 0.0, -2.0] {
let p = AsrParams::default().with_strategy(SamplingStrategy::BeamSearch {
beam_size: 5,
patience: bad,
});
let err = validate_for_whisper_ffi(&p).unwrap_err();
assert!(
matches!(&err, WorkFailure::Asr(e) if e.to_string().contains("patience")),
"patience={bad} should reject; got {err:?}",
);
}
}
#[test]
fn validate_for_whisper_ffi_accepts_negative_one_sentinel_patience() {
let p = AsrParams::default().with_strategy(SamplingStrategy::BeamSearch {
beam_size: 5,
patience: -1.0,
});
assert!(
validate_for_whisper_ffi(&p).is_ok(),
"-1.0 is whisper.cpp's `use default patience` sentinel and matches \
SamplingStrategy::default(); must be accepted",
);
}
#[test]
fn validate_for_whisper_ffi_rejects_non_finite_temperature() {
for bad in [f32::NAN, f32::INFINITY, f32::NEG_INFINITY] {
let p = AsrParams::default().with_initial_temperature(bad);
let err = validate_for_whisper_ffi(&p).unwrap_err();
assert!(
matches!(&err, WorkFailure::Asr(e) if e.to_string().contains("initial_temperature")),
"initial_temperature={bad} should reject; got {err:?}",
);
}
}
#[test]
fn validate_for_whisper_ffi_rejects_non_finite_thresholds() {
let bad = f32::NAN;
let cases: [(&str, AsrParams); 3] = [
(
"log_prob_threshold",
AsrParams::default().with_log_prob_threshold(bad),
),
(
"compression_ratio_threshold",
AsrParams::default().with_compression_ratio_threshold(bad),
),
(
"no_speech_threshold",
AsrParams::default().with_no_speech_threshold(bad),
),
];
for (knob, p) in cases {
let err = validate_for_whisper_ffi(&p).unwrap_err();
assert!(
matches!(&err, WorkFailure::Asr(e) if e.to_string().contains(knob)),
"{knob}=NaN should reject; got {err:?}",
);
}
}
#[test]
fn validate_for_whisper_ffi_rejects_negative_initial_temperature() {
let p = AsrParams::default().with_initial_temperature(-0.1);
let err = validate_for_whisper_ffi(&p).unwrap_err();
assert!(
matches!(&err, WorkFailure::Asr(e) if e.to_string().contains("initial_temperature")),
"got {err:?}",
);
}
#[test]
fn validate_for_whisper_ffi_rejects_above_one_initial_temperature() {
let p = AsrParams::default().with_initial_temperature(1.5);
let err = validate_for_whisper_ffi(&p).unwrap_err();
assert!(
matches!(&err, WorkFailure::Asr(e) if e.to_string().contains("initial_temperature")),
"got {err:?}",
);
}
#[test]
fn validate_for_whisper_ffi_rejects_out_of_range_temperature_increment() {
for bad in [-0.1_f32, 1.5_f32] {
let p = AsrParams::default().with_temperature_increment(bad);
let err = validate_for_whisper_ffi(&p).unwrap_err();
assert!(
matches!(&err, WorkFailure::Asr(e) if e.to_string().contains("temperature_increment")),
"increment={bad} should reject; got {err:?}",
);
}
}
#[test]
fn validate_for_whisper_ffi_rejects_out_of_range_no_speech_threshold() {
for bad in [-0.1_f32, 1.5_f32] {
let p = AsrParams::default().with_no_speech_threshold(bad);
let err = validate_for_whisper_ffi(&p).unwrap_err();
assert!(
matches!(&err, WorkFailure::Asr(e) if e.to_string().contains("no_speech_threshold")),
"no_speech={bad} should reject; got {err:?}",
);
}
}
#[test]
fn validate_for_whisper_ffi_rejects_non_positive_compression_ratio_threshold() {
for bad in [0.0_f32, -2.4_f32] {
let p = AsrParams::default().with_compression_ratio_threshold(bad);
let err = validate_for_whisper_ffi(&p).unwrap_err();
assert!(
matches!(&err, WorkFailure::Asr(e) if e.to_string().contains("compression_ratio_threshold")),
"cratio={bad} should reject; got {err:?}",
);
}
}
#[test]
fn validate_for_whisper_ffi_accepts_default_threshold_values() {
let p = AsrParams::default();
assert!(
validate_for_whisper_ffi(&p).is_ok(),
"AsrParams::default() must round-trip through validation",
);
}
#[test]
fn validate_for_whisper_ffi_accepts_valid_strategies() {
let g = AsrParams::default().with_strategy(SamplingStrategy::Greedy { best_of: 1 });
assert!(validate_for_whisper_ffi(&g).is_ok());
let b = AsrParams::default().with_strategy(SamplingStrategy::BeamSearch {
beam_size: 5,
patience: 1.0,
});
assert!(validate_for_whisper_ffi(&b).is_ok());
}
#[test]
fn run_with_temperature_ladder_signature_compiles() {
let _f: fn(
&mut WhisperState,
&AsrWorkItem,
std::time::Instant,
) -> Result<crate::core::AsrResult, crate::types::WorkFailure> = run_with_temperature_ladder;
}
}