use super::*;
use rand::SeedableRng;
use crate::backends::{SttBackend, SttKind};
const AUDIO_DELAY_JITTER_SPREAD: f64 = 0.25;
fn jittered_audio_delay(nominal_ms: u64) -> std::time::Duration {
guise::human::timing::jittered_step(
std::time::Duration::from_millis(nominal_ms),
AUDIO_DELAY_JITTER_SPREAD,
&mut rand::rngs::StdRng::from_entropy(),
)
}
pub struct AudioCaptchaSolver {
client: reqwest::Client,
pub(crate) stt_backend: Option<SttBackend>,
pub(crate) stt_endpoint: String,
pub(crate) config: SolveConfig,
}
impl Default for AudioCaptchaSolver {
fn default() -> Self {
Self::new()
}
}
impl AudioCaptchaSolver {
pub fn new() -> Self {
let config = SolveConfig::default();
let stt_backend = Self::probe_stt_sync();
Self {
client: crate::http_client::timed_client_or_panic(Duration::from_millis(
config.client_http_timeout_ms,
)),
stt_backend,
stt_endpoint: "http://localhost:9000/asr".to_string(),
config,
}
}
fn probe_stt_sync() -> Option<SttBackend> {
if let Some(path) =
crate::backends::which("whisper").or_else(|| crate::backends::which("whisper-cli"))
{
return Some(SttBackend {
kind: SttKind::WhisperCli,
endpoint: path,
model: Some(crate::backends::DEFAULT_WHISPER_MODEL.to_string()),
});
}
if std::env::var("OPENAI_API_KEY").is_ok() {
return Some(SttBackend {
kind: SttKind::OpenAIApi,
endpoint: "https://api.openai.com/v1/audio/transcriptions".to_string(),
model: Some("whisper-1".to_string()),
});
}
None
}
pub fn with_stt_endpoint(mut self, url: impl Into<String>) -> Self {
self.stt_endpoint = url.into();
self
}
pub fn with_stt_backend(mut self, backend: SttBackend) -> Self {
self.stt_backend = Some(backend);
self
}
pub fn with_config(mut self, config: SolveConfig) -> Self {
self.config = config;
self
}
}
pub fn normalize_transcript(s: &str) -> String {
let lowered: String = s
.to_lowercase()
.chars()
.map(|c| if c.is_alphanumeric() { c } else { ' ' })
.collect();
let tokens: Vec<&str> = lowered.split_whitespace().collect();
let mut out = Vec::with_capacity(tokens.len());
let mut all_digits = true;
for tok in &tokens {
let digit = match *tok {
"zero" | "oh" => Some("0"),
"one" => Some("1"),
"two" | "to" | "too" => Some("2"),
"three" => Some("3"),
"four" | "for" => Some("4"),
"five" => Some("5"),
"six" => Some("6"),
"seven" => Some("7"),
"eight" | "ate" => Some("8"),
"nine" => Some("9"),
t if t.chars().all(|c| c.is_ascii_digit()) => Some(*tok),
_ => None,
};
if let Some(d) = digit {
out.push(d.to_string());
} else {
all_digits = false;
out.push((*tok).to_string());
}
}
if all_digits {
out.join("")
} else {
out.join(" ")
}
}
mod transcribe;
mod solve;
#[cfg(test)]
mod tests;