use crate::backend::BackendConfig;
#[cfg(feature = "vad-silero")]
use crate::silero_audio_processor::VadConfig as SileroVadConfig;
#[cfg(feature = "backend-ctranslate2")]
use ct2rs::WhisperOptions;
use serde::{Deserialize, Serialize};
pub const SAMPLE_RATE: usize = 16000;
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(default)]
pub struct AudioProcessorConfig {
pub buffer_size: usize,
}
impl Default for AudioProcessorConfig {
fn default() -> Self {
Self { buffer_size: 1024 }
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(default)]
pub struct GeneralConfig {
pub model: String,
pub language: String,
pub transcription_mode: String,
}
impl Default for GeneralConfig {
fn default() -> Self {
Self {
model: "small.en".to_string(),
language: "en".to_string(),
transcription_mode: "manual".to_string(),
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(default)]
pub struct RealtimeModeConfig {
pub max_buffer_duration_sec: f32,
pub max_segment_count: usize,
}
impl Default for RealtimeModeConfig {
fn default() -> Self {
Self {
max_buffer_duration_sec: 30.0,
max_segment_count: 20,
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(default)]
pub struct ManualModeConfig {
pub max_recording_duration_secs: u32,
pub clear_on_new_session: bool,
pub chunk_duration_seconds: f32,
pub enable_chunk_overlap: bool,
pub chunk_overlap_seconds: f32,
pub disable_chunking: bool,
}
impl Default for ManualModeConfig {
fn default() -> Self {
Self {
max_recording_duration_secs: 120,
clear_on_new_session: true,
chunk_duration_seconds: 29.0, enable_chunk_overlap: true, chunk_overlap_seconds: 2.0, disable_chunking: false, }
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(default)]
pub struct DebugConfig {
pub log_stats_enabled: bool,
pub save_manual_audio_debug: bool,
pub recording_dir: String,
}
impl Default for DebugConfig {
fn default() -> Self {
Self {
log_stats_enabled: false,
save_manual_audio_debug: false,
recording_dir: "recordings".to_string(),
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(default)]
pub struct PostProcessConfig {
pub enabled: bool,
pub remove_leading_dashes: bool,
pub remove_trailing_dashes: bool,
pub normalize_whitespace: bool,
}
impl Default for PostProcessConfig {
fn default() -> Self {
Self {
enabled: true,
remove_leading_dashes: true,
remove_trailing_dashes: true,
normalize_whitespace: true,
}
}
}
#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq, Default)]
pub enum VadSensitivity {
Low,
#[default]
Medium,
High,
}
impl VadSensitivity {
pub fn threshold(&self) -> f32 {
match self {
VadSensitivity::Low => 0.15,
VadSensitivity::Medium => 0.10,
VadSensitivity::High => 0.05,
}
}
pub fn speech_end_threshold(&self) -> f32 {
match self {
VadSensitivity::Low => 0.12,
VadSensitivity::Medium => 0.08,
VadSensitivity::High => 0.03,
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
#[serde(default)]
pub struct SpeechConfig {
pub general_config: GeneralConfig,
pub backend_config: BackendConfig,
pub audio_processor_config: AudioProcessorConfig,
pub realtime_mode_config: RealtimeModeConfig,
pub manual_mode_config: ManualModeConfig,
pub vad_config: VadConfigSerde,
pub common_transcription_options: CommonTranscriptionOptions,
pub ctranslate2_options: CT2Options,
pub whisper_cpp_options: WhisperCppOptions,
pub moonshine_options: MoonshineOptions,
pub parakeet_options: ParakeetOptions,
pub nemotron_options: NemotronOptions,
pub debug_config: DebugConfig,
pub post_process_config: PostProcessConfig,
#[serde(skip_serializing_if = "Option::is_none")]
pub compute_type: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub device: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(default)]
pub struct CommonTranscriptionOptions {
pub beam_size: usize,
pub patience: f32,
}
impl Default for CommonTranscriptionOptions {
fn default() -> Self {
Self {
beam_size: 5,
patience: 1.0,
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(default)]
pub struct CT2Options {
pub repetition_penalty: f32,
}
impl Default for CT2Options {
fn default() -> Self {
Self {
repetition_penalty: 1.25,
}
}
}
pub const WHISPER_ENTROPY_THOLD: f32 = 2.4;
pub const WHISPER_LOGPROB_THOLD: f32 = -1.0;
pub const WHISPER_NO_SPEECH_THOLD: f32 = 0.6;
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(default)]
pub struct WhisperCppOptions {
pub temperature: f32,
pub suppress_blank: bool,
pub no_context: bool,
pub max_tokens: i32,
#[serde(skip)]
pub initial_prompt: Option<String>,
}
impl Default for WhisperCppOptions {
fn default() -> Self {
Self {
temperature: 0.2, suppress_blank: true, no_context: true, max_tokens: 0, initial_prompt: None, }
}
}
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
#[serde(default)]
pub struct MoonshineOptions {
pub enable_cache: bool,
}
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
#[serde(default)]
pub struct ParakeetOptions {}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(default)]
pub struct NemotronOptions {
pub language: String,
}
impl Default for NemotronOptions {
fn default() -> Self {
Self {
language: "en-US".to_string(),
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(default)]
pub struct VadConfigSerde {
pub sensitivity: VadSensitivity,
pub hangbefore_frames: usize,
pub hangover_frames: usize,
pub silence_tolerance_frames: usize,
pub speech_prob_smoothing: f32,
}
impl Default for VadConfigSerde {
fn default() -> Self {
Self {
sensitivity: VadSensitivity::default(), hangbefore_frames: 5, hangover_frames: 30, silence_tolerance_frames: 8, speech_prob_smoothing: 0.3, }
}
}
#[cfg(feature = "vad-silero")]
fn rescale_vad_frames(frames: usize, frame_size: usize) -> usize {
const LEGACY_HOP: usize = 160;
if frames == 0 {
0
} else {
((frames * LEGACY_HOP + frame_size / 2) / frame_size).max(1)
}
}
#[cfg(feature = "vad-silero")]
impl SileroVadConfig {
pub fn from_config(
vad_config: &VadConfigSerde,
realtime_config: &RealtimeModeConfig,
_buffer_size: usize,
sample_rate: usize,
) -> Self {
let frame_size = 512;
Self {
threshold: vad_config.sensitivity.threshold(),
frame_size,
sample_rate,
hangbefore_frames: rescale_vad_frames(vad_config.hangbefore_frames, frame_size),
hangover_frames: rescale_vad_frames(vad_config.hangover_frames, frame_size),
hop_samples: frame_size, max_buffer_duration: (realtime_config.max_buffer_duration_sec * sample_rate as f32)
as usize,
max_segment_count: realtime_config.max_segment_count,
silence_tolerance_frames: rescale_vad_frames(
vad_config.silence_tolerance_frames,
frame_size,
),
speech_end_threshold: vad_config.sensitivity.speech_end_threshold(),
speech_prob_smoothing: vad_config.speech_prob_smoothing,
}
}
}
#[cfg(feature = "vad-silero")]
impl From<(VadConfigSerde, RealtimeModeConfig, usize, usize)> for SileroVadConfig {
fn from(
(config, realtime_config, _buffer_size, sample_rate): (
VadConfigSerde,
RealtimeModeConfig,
usize,
usize,
),
) -> Self {
let frame_size = 512;
Self {
threshold: config.sensitivity.threshold(),
frame_size,
sample_rate,
hangbefore_frames: rescale_vad_frames(config.hangbefore_frames, frame_size),
hangover_frames: rescale_vad_frames(config.hangover_frames, frame_size),
hop_samples: frame_size, max_buffer_duration: (realtime_config.max_buffer_duration_sec * sample_rate as f32)
as usize,
max_segment_count: realtime_config.max_segment_count,
silence_tolerance_frames: rescale_vad_frames(
config.silence_tolerance_frames,
frame_size,
),
speech_end_threshold: config.sensitivity.speech_end_threshold(),
speech_prob_smoothing: config.speech_prob_smoothing,
}
}
}
impl SpeechConfig {
pub fn migrate_legacy_config(&mut self) {
if let (Some(compute_type), Some(device)) = (&self.compute_type, &self.device) {
let is_default_config = self.backend_config.threads == num_cpus::get().min(4)
&& !self.backend_config.gpu_enabled;
if is_default_config {
tracing::info!(
"Migrating legacy config fields (compute_type={}, device={}) to backend_config",
compute_type,
device
);
#[cfg(feature = "backend-ctranslate2")]
{
self.backend_config = crate::backend::ctranslate2::migrate_legacy_config(
compute_type,
device,
None,
);
}
#[cfg(not(feature = "backend-ctranslate2"))]
{
tracing::warn!(
"Legacy CTranslate2 config fields are present, but `backend-ctranslate2` is disabled"
);
}
self.compute_type = None;
self.device = None;
}
}
if !self.whisper_cpp_options.no_context {
tracing::info!(
"Enabling whisper_cpp_options.no_context to prevent cross-session duplication"
);
self.whisper_cpp_options.no_context = true;
}
if (self.whisper_cpp_options.temperature - 0.0).abs() < f32::EPSILON {
self.whisper_cpp_options.temperature = 0.2;
}
}
}
#[cfg(feature = "backend-ctranslate2")]
impl CT2Options {
pub fn to_whisper_options(
&self,
common_options: &CommonTranscriptionOptions,
) -> WhisperOptions {
WhisperOptions {
beam_size: common_options.beam_size,
patience: common_options.patience,
repetition_penalty: self.repetition_penalty,
..Default::default()
}
}
}
#[cfg(feature = "backend-ctranslate2")]
pub fn migrate_legacy_ctranslate2_config(
compute_type: &str,
device: &str,
threads: Option<usize>,
) -> crate::backend::BackendConfig {
crate::backend::ctranslate2::migrate_legacy_config(compute_type, device, threads)
}