#[cfg(test)]
mod tests;
use std::collections::HashMap;
#[derive(Debug, Clone)]
pub struct HotwordConfig {
pub default_bias: f32,
pub max_bias: f32,
pub min_tokens: usize,
pub case_sensitive: bool,
pub partial_match_decay: f32,
}
impl HotwordConfig {
#[must_use]
pub fn new() -> Self {
Self {
default_bias: 1.0,
max_bias: 5.0,
min_tokens: 1,
case_sensitive: false,
partial_match_decay: 0.9,
}
}
#[must_use]
pub fn with_default_bias(mut self, bias: f32) -> Self {
self.default_bias = bias;
self
}
#[must_use]
pub fn with_max_bias(mut self, max: f32) -> Self {
self.max_bias = max;
self
}
#[must_use]
pub fn with_min_tokens(mut self, min: usize) -> Self {
self.min_tokens = min;
self
}
#[must_use]
pub fn with_case_sensitive(mut self, case_sensitive: bool) -> Self {
self.case_sensitive = case_sensitive;
self
}
#[must_use]
pub fn with_partial_match_decay(mut self, decay: f32) -> Self {
self.partial_match_decay = decay;
self
}
}
impl Default for HotwordConfig {
fn default() -> Self {
Self::new()
}
}
#[derive(Debug, Clone)]
pub struct Hotword {
pub text: String,
pub tokens: Vec<u32>,
pub bias: f32,
pub priority: u32,
}
impl Hotword {
#[must_use]
pub fn new(text: String, tokens: Vec<u32>, bias: f32) -> Self {
Self {
text,
tokens,
bias,
priority: 0,
}
}
#[must_use]
pub fn with_priority(mut self, priority: u32) -> Self {
self.priority = priority;
self
}
#[must_use]
pub fn len(&self) -> usize {
self.tokens.len()
}
#[must_use]
pub fn is_empty(&self) -> bool {
self.tokens.is_empty()
}
#[must_use]
pub fn prefix_match_len(&self, context: &[u32]) -> usize {
for prefix_len in (1..=self.tokens.len().min(context.len())).rev() {
let hotword_prefix = &self.tokens[..prefix_len];
let context_suffix = &context[context.len() - prefix_len..];
if hotword_prefix == context_suffix {
return prefix_len;
}
}
0
}
#[must_use]
pub fn next_token(&self, prefix_len: usize) -> Option<u32> {
if prefix_len < self.tokens.len() {
Some(self.tokens[prefix_len])
} else {
None
}
}
}
#[derive(Debug, Clone)]
pub struct HotwordBooster {
config: HotwordConfig,
hotwords: Vec<Hotword>,
first_token_map: HashMap<u32, Vec<usize>>,
}
impl HotwordBooster {
#[must_use]
pub fn new() -> Self {
Self::with_config(HotwordConfig::default())
}
#[must_use]
pub fn with_config(config: HotwordConfig) -> Self {
Self {
config,
hotwords: Vec::new(),
first_token_map: HashMap::new(),
}
}
pub fn add_hotword_with_tokens(&mut self, text: &str, tokens: Vec<u32>, bias: f32) {
if tokens.is_empty() {
return;
}
let clamped_bias = bias.clamp(-self.config.max_bias, self.config.max_bias);
let first_token = tokens[0];
let hotword_idx = self.hotwords.len();
self.hotwords
.push(Hotword::new(text.to_string(), tokens, clamped_bias));
self.first_token_map
.entry(first_token)
.or_default()
.push(hotword_idx);
}
pub fn add_hotword_with_tokens_default(&mut self, text: &str, tokens: Vec<u32>) {
self.add_hotword_with_tokens(text, tokens, self.config.default_bias);
}
pub fn clear(&mut self) {
self.hotwords.clear();
self.first_token_map.clear();
}
#[must_use]
pub fn len(&self) -> usize {
self.hotwords.len()
}
#[must_use]
pub fn is_empty(&self) -> bool {
self.hotwords.is_empty()
}
#[must_use]
pub fn config(&self) -> &HotwordConfig {
&self.config
}
#[must_use]
pub fn hotwords(&self) -> &[Hotword] {
&self.hotwords
}
pub fn apply_bias(&self, logits: &mut [f32], context: &[u32]) {
if self.hotwords.is_empty() {
return;
}
let biases = self.compute_biases(context);
for (token_id, bias) in biases {
if (token_id as usize) < logits.len() {
logits[token_id as usize] += bias;
}
}
}
fn compute_biases(&self, context: &[u32]) -> Vec<(u32, f32)> {
let mut biases: HashMap<u32, f32> = HashMap::new();
for hotword in &self.hotwords {
let match_len = hotword.prefix_match_len(context);
if match_len > 0 {
if let Some(next_token) = hotword.next_token(match_len) {
let progress = match_len as f32 / hotword.tokens.len() as f32;
let scaled_bias = hotword.bias * (1.0 + progress);
*biases.entry(next_token).or_insert(0.0) += scaled_bias;
}
} else if context.is_empty() || !self.has_recent_hotword_match(context) {
let first_token = hotword.tokens[0];
let scaled_bias = hotword.bias * self.config.partial_match_decay;
*biases.entry(first_token).or_insert(0.0) += scaled_bias;
}
}
biases
.into_iter()
.map(|(token, bias)| {
(
token,
bias.clamp(-self.config.max_bias, self.config.max_bias),
)
})
.collect()
}
fn has_recent_hotword_match(&self, context: &[u32]) -> bool {
for hotword in &self.hotwords {
if hotword.prefix_match_len(context) > 0 {
return true;
}
}
false
}
#[must_use]
pub fn get_completion_tokens(&self, context: &[u32]) -> Vec<(u32, f32)> {
self.compute_biases(context)
}
}
impl Default for HotwordBooster {
fn default() -> Self {
Self::new()
}
}