use std::collections::HashMap;
use std::fmt;
use std::path::Path;
#[derive(Debug)]
pub enum VocabularyError {
Io(std::io::Error),
Invalid {
line: usize,
message: &'static str,
},
}
impl fmt::Display for VocabularyError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::Io(e) => write!(f, "cannot read vocabulary file: {e}"),
Self::Invalid { line: 0, message } => write!(f, "vocabulary file {message}"),
Self::Invalid { line, message } => write!(f, "vocabulary line {line}: {message}"),
}
}
}
impl std::error::Error for VocabularyError {
fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
match self {
Self::Io(e) => Some(e),
Self::Invalid { .. } => None,
}
}
}
impl From<std::io::Error> for VocabularyError {
fn from(e: std::io::Error) -> Self {
Self::Io(e)
}
}
fn is_latin_word(word: &str) -> bool {
let bytes = word.as_bytes();
match (bytes.first(), bytes.last()) {
(Some(first), Some(last)) if first.is_ascii_alphabetic() && last.is_ascii_alphabetic() => {}
_ => return false,
}
bytes.iter().all(|b| b.is_ascii_alphabetic() || *b == b'-' || *b == b'\'')
}
pub fn load_vocabulary_tsv(
path: impl AsRef<Path>,
) -> Result<HashMap<String, String>, VocabularyError> {
parse_vocabulary(&std::fs::read_to_string(path)?)
}
pub fn parse_vocabulary(source: &str) -> Result<HashMap<String, String>, VocabularyError> {
let mut words = HashMap::new();
let mut header_seen = false;
for (index, raw) in source.lines().enumerate() {
let line_number = index + 1;
let mut line = raw.strip_suffix('\r').unwrap_or(raw);
if line_number == 1 {
line = line.strip_prefix('\u{feff}').unwrap_or(line);
}
if line.is_empty() || line.starts_with('#') {
continue;
}
let invalid = |message| VocabularyError::Invalid { line: line_number, message };
if !header_seen {
if line != "latin\tcyrillic" {
return Err(invalid("expected latin<TAB>cyrillic header"));
}
header_seen = true;
continue;
}
let mut fields = line.split('\t');
let (Some(key), Some(reading), None) = (fields.next(), fields.next(), fields.next()) else {
return Err(invalid("expected two columns"));
};
if !is_latin_word(key) {
return Err(invalid("latin must be one ASCII Latin word"));
}
if reading.is_empty() || reading.trim() != reading {
return Err(invalid("cyrillic must be nonempty without surrounding whitespace"));
}
if words.insert(key.to_ascii_lowercase(), reading.to_owned()).is_some() {
return Err(invalid("duplicate latin word"));
}
}
if !header_seen {
return Err(VocabularyError::Invalid {
line: 0,
message: "is missing latin<TAB>cyrillic header",
});
}
Ok(words)
}
pub fn load_asr_vocabulary_tsv(path: impl AsRef<Path>) -> Result<Vec<String>, VocabularyError> {
parse_asr_vocabulary(&std::fs::read_to_string(path)?)
}
pub fn parse_asr_vocabulary(source: &str) -> Result<Vec<String>, VocabularyError> {
let mut words = Vec::new();
let mut header_seen = false;
for (index, raw) in source.lines().enumerate() {
let line_number = index + 1;
let mut line = raw.strip_suffix('\r').unwrap_or(raw);
if line_number == 1 {
line = line.strip_prefix('\u{feff}').unwrap_or(line);
}
if line.is_empty() || line.starts_with('#') {
continue;
}
let invalid = |message| VocabularyError::Invalid { line: line_number, message };
if !header_seen {
if line != "word" {
return Err(invalid("expected a `word` header"));
}
header_seen = true;
continue;
}
if line.contains('\t') {
return Err(invalid("expected a single column"));
}
if line.trim() != line || line.is_empty() {
return Err(invalid("word must be nonempty without surrounding whitespace"));
}
words.push(line.to_owned());
}
if !header_seen {
return Err(VocabularyError::Invalid { line: 0, message: "is missing a `word` header" });
}
Ok(words)
}