use super::tokenizer::Tokenizer;
fn encode_representable(tokenizer: &Tokenizer, phrase: &str) -> Option<Vec<usize>> {
if let Some(ids) = tokenizer.encode_phrase(phrase) {
return Some(ids);
}
let lowercased = phrase.to_lowercase();
if lowercased != phrase
&& let Some(ids) = tokenizer.encode_phrase(&lowercased)
{
return Some(ids);
}
let folded = lowercased.replace('ё', "е");
if folded != lowercased {
return tokenizer.encode_phrase(&folded);
}
None
}
struct TrieNode {
children: std::collections::HashMap<usize, usize>,
is_end: bool,
shortest_phrase: usize,
depth: usize,
is_entry: bool,
grant: f32,
}
impl TrieNode {
fn new(depth: usize) -> Self {
Self {
children: std::collections::HashMap::new(),
is_end: false,
shortest_phrase: usize::MAX,
depth,
is_entry: false,
grant: 0.0,
}
}
}
#[derive(Clone, Copy, Default)]
pub(crate) struct BiasPath {
node: usize,
pending: f32,
}
impl BiasPath {
pub(crate) fn pending(&self) -> f32 {
self.pending
}
}
pub struct Biaser {
nodes: Vec<TrieNode>,
boost: f32,
phrase_count: usize,
}
impl Biaser {
#[cfg(test)]
pub(crate) fn from_sequences(sequences: Vec<Vec<usize>>, boost: f32) -> Option<Self> {
Self::build(sequences, boost, false)
}
fn build(sequences: Vec<Vec<usize>>, boost: f32, leading_is_entry: bool) -> Option<Self> {
let mut nodes = vec![TrieNode::new(0)];
let mut phrase_count = 0;
let entry_tokens = usize::from(leading_is_entry);
for seq in sequences {
if seq.is_empty() {
continue;
}
phrase_count += 1;
let scored = seq.len().saturating_sub(entry_tokens).max(1);
let mut node = 0usize;
for tok in seq {
node = match nodes[node].children.get(&tok) {
Some(&child) => child,
None => {
let depth = nodes[node].depth + 1;
let child = nodes.len();
nodes.push(TrieNode::new(depth));
nodes[node].children.insert(tok, child);
child
}
};
nodes[node].shortest_phrase = nodes[node].shortest_phrase.min(scored);
}
nodes[node].is_end = true;
}
if phrase_count == 0 {
return None;
}
for node in nodes.iter_mut().skip(1) {
node.is_entry = leading_is_entry && node.depth == 1;
node.grant = if node.is_entry {
0.0
} else {
boost / node.shortest_phrase.max(1) as f32
};
}
Some(Self {
nodes,
boost,
phrase_count,
})
}
pub fn from_phrases(
tokenizer: &Tokenizer,
phrases: &[(String, f32)],
boost: f32,
) -> Option<Self> {
if boost <= 0.0 {
return None;
}
let mut sequences = Vec::new();
let mut dropped: Vec<&str> = Vec::new();
for (phrase, weight) in phrases {
if *weight <= 0.0 {
continue;
}
match encode_representable(tokenizer, phrase) {
Some(ids) => sequences.push(ids),
None => dropped.push(phrase),
}
}
if !dropped.is_empty() {
tracing::warn!(
"{} hotword phrase(s) dropped, not representable in the active vocab: {}",
dropped.len(),
dropped.join(", ")
);
}
Self::build(sequences, boost, true)
}
pub fn phrase_count(&self) -> usize {
self.phrase_count
}
pub(crate) fn score_token(&self, path: BiasPath, tok: usize) -> (f32, BiasPath) {
let enter = |from: BiasPath, child: usize| {
let share = self.nodes[child].grant;
(
share,
BiasPath {
node: child,
pending: if self.nodes[child].is_end {
0.0
} else {
from.pending + share
},
},
)
};
if let Some(&child) = self.nodes[path.node].children.get(&tok) {
return enter(path, child);
}
let refund = -path.pending;
match self.nodes[0].children.get(&tok) {
Some(&child) => {
let (delta, next) = enter(BiasPath::default(), child);
(refund + delta, next)
}
None => (refund, BiasPath::default()),
}
}
pub(crate) fn continuations(&self, path: BiasPath, out: &mut Vec<usize>) {
out.extend(self.nodes[path.node].children.keys().copied());
if path.node != 0 {
out.extend(self.nodes[0].children.keys().copied());
}
}
pub(crate) fn new_state(&self) -> BiasState {
BiasState {
active: vec![0],
}
}
pub(crate) fn boost_logits(&self, state: &BiasState, logits: &mut [f32]) {
for &node in &state.active {
for (&tok, &child) in &self.nodes[node].children {
if tok < logits.len() && !self.nodes[child].is_entry {
logits[tok] += self.boost;
}
}
}
}
pub(crate) fn advance(&self, state: &mut BiasState, tok: usize) {
let mut next = Vec::new();
for &node in &state.active {
if let Some(&child) = self.nodes[node].children.get(&tok)
&& !next.contains(&child)
{
next.push(child);
}
}
if !next.contains(&0) {
next.push(0);
}
state.active = next;
}
}
pub(crate) struct BiasState {
active: Vec<usize>,
}
#[cfg(test)]
mod tests;