use super::error::TextError;
use super::limits::TextLimits;
use super::ngram::{ngram_count, ngrams};
use super::split::{Split, split};
use super::standardize::{Standardize, standardize};
use super::vocab::{PADDING_ID, Vocabulary};
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub(crate) struct TransformOptions {
pub(crate) standardize: Standardize,
pub(crate) split: Split,
pub(crate) ngrams: Option<usize>,
pub(crate) output_sequence_length: Option<usize>,
}
fn token_count(
input: &str,
opts: &TransformOptions,
limits: &TextLimits,
index: usize,
) -> Result<usize, TextError> {
let std = standardize(input, opts.standardize, limits, index)?;
Ok(split(&std, opts.split).count())
}
fn fill_row(
index: usize,
input: &str,
vocab: &Vocabulary,
opts: &TransformOptions,
limits: &TextLimits,
dst: &mut [i32],
) -> Result<(), TextError> {
let std = standardize(input, opts.standardize, limits, index)?;
let toks: Vec<&str> = split(&std, opts.split).collect();
match opts.ngrams {
Some(n) => {
let grams = ngrams(&toks, n, limits)?;
for (slot, g) in dst.iter_mut().zip(grams.iter()) {
*slot = vocab.lookup(g);
}
}
None => {
for (slot, t) in dst.iter_mut().zip(toks.iter()) {
*slot = vocab.lookup(t);
}
}
}
Ok(())
}
pub(crate) fn transform<S: AsRef<str>>(
vocab: &Vocabulary,
opts: &TransformOptions,
limits: &TextLimits,
inputs: &[S],
) -> Result<crate::Tensor<i32>, TextError> {
if let Some(n) = opts.ngrams {
limits.check_ngrams(n)?;
}
if let Some(l) = opts.output_sequence_length {
limits.check_output_sequence_length(l)?;
}
limits.check_inputs(inputs)?;
let len = match opts.output_sequence_length {
Some(l) => l,
None => {
let mut max = 0usize;
for (i, s) in inputs.iter().enumerate() {
let t = token_count(s.as_ref(), opts, limits, i)?;
let row = match opts.ngrams {
Some(n) => ngram_count(t, n, limits)?,
None => t,
};
max = max.max(row);
}
max
}
};
let batch = inputs.len();
let elements = limits.check_output_elements(batch, len)?;
let mut data = vec![PADDING_ID; elements];
if len > 0 {
for (i, (row, s)) in data.chunks_exact_mut(len).zip(inputs.iter()).enumerate() {
fill_row(i, s.as_ref(), vocab, opts, limits, row)?;
}
} else {
for (i, s) in inputs.iter().enumerate() {
let t = token_count(s.as_ref(), opts, limits, i)?;
if let Some(n) = opts.ngrams {
ngram_count(t, n, limits)?;
}
}
}
crate::Tensor::new(data, &[batch, len]).map_err(TextError::Shape)
}
#[cfg(test)]
mod tests {
use super::*;
const SECRET: &str = "SECRET_MARKER_TOKEN";
fn vocab(words: &[&str]) -> Vocabulary {
let Ok(v) = Vocabulary::from_tokens(&TextLimits::default(), words) else {
panic!("vocab must build");
};
v
}
fn run(
v: &Vocabulary,
opts: &TransformOptions,
limits: &TextLimits,
inputs: &[&str],
) -> Result<(Vec<usize>, Vec<i32>), TextError> {
transform(v, opts, limits, inputs).map(|t| (t.shape().to_vec(), t.host_slice().to_vec()))
}
fn base() -> Vocabulary {
vocab(&["cat", "dog", "the"])
}
fn go(opts: TransformOptions, inputs: &[&str]) -> (Vec<usize>, Vec<i32>) {
match run(&base(), &opts, &TextLimits::default(), inputs) {
Ok(r) => r,
Err(e) => panic!("transform failed: {e}"),
}
}
#[test]
fn basic_pads_to_longest() {
let (s, d) = go(TransformOptions::default(), &["the cat", "dog"]);
assert_eq!(s, vec![2, 2]);
assert_eq!(d, vec![4, 2, 3, 0]);
}
#[test]
fn truncate_and_pad_with_fixed_length() {
let o = TransformOptions {
output_sequence_length: Some(2),
..Default::default()
};
let (s, d) = go(o, &["the cat dog", "dog"]);
assert_eq!(s, vec![2, 2]);
assert_eq!(d, vec![4, 2, 3, 0]);
let o = TransformOptions {
output_sequence_length: Some(4),
..Default::default()
};
let (s, d) = go(o, &["the cat"]);
assert_eq!((s, d), (vec![1, 4], vec![4, 2, 0, 0]));
}
#[test]
fn zero_length() {
let o = TransformOptions {
output_sequence_length: Some(0),
..Default::default()
};
let (s, d) = go(o, &["a", "b"]);
assert_eq!((s, d.len()), (vec![2, 0], 0));
let (s, d) = go(TransformOptions::default(), &["", " "]);
assert_eq!((s, d.len()), (vec![2, 0], 0));
}
#[test]
fn empty_batch() {
let (s, d) = go(TransformOptions::default(), &[]);
assert_eq!((s, d.len()), (vec![0, 0], 0));
let o = TransformOptions {
output_sequence_length: Some(3),
..Default::default()
};
let (s, d) = go(o, &[]);
assert_eq!((s, d.len()), (vec![0, 3], 0));
}
#[test]
fn empty_batch_still_validates_config() {
let l = TextLimits::default();
let o = TransformOptions {
ngrams: Some(0),
..Default::default()
};
assert!(matches!(
run(&base(), &o, &l, &[]),
Err(TextError::InvalidNgrams { .. })
));
let o = TransformOptions {
output_sequence_length: Some(usize::MAX),
..Default::default()
};
assert!(matches!(
run(&base(), &o, &l, &[]),
Err(TextError::OutputSequenceLengthTooLarge { .. })
));
}
#[test]
fn oov_and_standardize_before_lookup() {
let (_, d) = go(TransformOptions::default(), &["Cat! zebra"]);
assert_eq!(d, vec![2, 1]);
let o = TransformOptions {
standardize: Standardize::None,
..Default::default()
};
let (_, d) = go(o, &["Cat!"]);
assert_eq!(d, vec![1]);
}
#[test]
fn split_modes() {
let v = vocab(&["é", "a", " "]);
let o = TransformOptions {
split: Split::Character,
standardize: Standardize::None,
..Default::default()
};
let Ok((s, d)) = run(&v, &o, &TextLimits::default(), &["éa b"]) else {
panic!("must succeed");
};
assert_eq!((s, d), (vec![1, 4], vec![2, 3, 4, 1]));
let o = TransformOptions {
split: Split::None,
..Default::default()
};
let (s, d) = go(o, &[""]);
assert_eq!((s, d), (vec![1, 1], vec![0]));
}
#[test]
fn ngrams_order_and_lookup() {
let v = vocab(&["a", "b", "a b"]);
let o = TransformOptions {
ngrams: Some(2),
..Default::default()
};
let Ok((s, d)) = run(&v, &o, &TextLimits::default(), &["a b", "a"]) else {
panic!("must succeed");
};
assert_eq!(s, vec![2, 3]);
assert_eq!(d, vec![2, 3, 4, 2, 0, 0]);
}
#[test]
fn output_too_large_both_paths() {
let Ok(l) = TextLimits::default().with_max_output_elements(3) else {
panic!("limit");
};
let none = TransformOptions::default();
assert!(matches!(
run(&base(), &none, &l, &["a b", "a b"]),
Err(TextError::OutputTooLarge {
elements: 4,
max: 3
})
));
let some = TransformOptions {
output_sequence_length: Some(2),
..Default::default()
};
assert!(matches!(
run(&base(), &some, &l, &["a", "b"]),
Err(TextError::OutputTooLarge {
elements: 4,
max: 3
})
));
assert!(run(&base(), &some, &l, &["a"]).is_ok());
}
#[test]
fn input_limit_errors() {
let Ok(l) = TextLimits::default()
.with_max_batch(2)
.and_then(|l| l.with_max_input_bytes(3))
else {
panic!("limit");
};
let o = TransformOptions::default();
assert!(matches!(
run(&base(), &o, &l, &["a", "b", "c"]),
Err(TextError::BatchTooLarge { .. })
));
assert!(matches!(
run(&base(), &o, &l, &["a", "abcd"]),
Err(TextError::InputTooLong { index: 1, .. })
));
}
#[test]
fn standardized_length_checked_for_every_output_length() {
let Ok(l) = TextLimits::default().with_max_input_bytes(2) else {
panic!("limit");
};
for out in [None, Some(2), Some(0)] {
let o = TransformOptions {
standardize: Standardize::UnicodeLower,
output_sequence_length: out,
..Default::default()
};
assert!(
matches!(
run(&base(), &o, &l, &["İ"]),
Err(TextError::InputTooLong { index: 0, .. })
),
"output_sequence_length = {out:?}"
);
}
}
#[test]
fn config_errors_precede_input_errors() {
let Ok(l) = TextLimits::default().with_max_batch(1) else {
panic!("limit");
};
let o = TransformOptions {
ngrams: Some(9),
..Default::default()
};
assert!(matches!(
run(&base(), &o, &l, &["a", "b"]),
Err(TextError::InvalidNgrams { .. })
));
let o = TransformOptions::default();
assert!(matches!(
run(&base(), &o, &l, &["a", "b"]),
Err(TextError::BatchTooLarge { .. })
));
}
#[test]
fn errors_do_not_leak_input() {
let Ok(l) = TextLimits::default().with_max_batch(1) else {
panic!("limit");
};
let inputs = [SECRET, SECRET];
let cases = [
TransformOptions::default(),
TransformOptions {
ngrams: Some(0),
..Default::default()
},
TransformOptions {
output_sequence_length: Some(usize::MAX),
..Default::default()
},
];
for o in cases {
let Err(e) = run(&base(), &o, &l, &inputs) else {
panic!("must fail");
};
assert!(!format!("{e}").contains(SECRET));
assert!(!format!("{e:?}").contains(SECRET));
}
let Ok(l2) = TextLimits::default().with_max_output_elements(1) else {
panic!("limit");
};
let Err(e) = run(
&base(),
&TransformOptions::default(),
&l2,
&[SECRET, SECRET],
) else {
panic!("must fail");
};
assert!(!format!("{e}").contains(SECRET) && !format!("{e:?}").contains(SECRET));
}
}