use ndarray::{ArrayView2, Axis};
const BLANK_ID: usize = 0;
const MIN_PROB: f32 = 1e-30;
pub(crate) fn char_column_ranges(
probs: ArrayView2<'_, f32>,
labels: &[usize],
) -> Option<Vec<[usize; 2]>> {
let (timesteps, class_num) = probs.dim();
let len = labels.len();
if len == 0 {
return Some(Vec::new());
}
if class_num == 0 {
return None;
}
if timesteps < len {
return None;
}
if labels.iter().any(|&id| id >= class_num) {
return None;
}
let states = 2 * len + 1;
let neg_inf = f32::NEG_INFINITY;
let mut blank_log = vec![0.0_f32; timesteps];
let mut label_log = vec![0.0_f32; len * timesteps];
for (t, row) in probs.axis_iter(Axis(0)).enumerate() {
blank_log[t] = ln_clamped(row[BLANK_ID]);
for (i, &id) in labels.iter().enumerate() {
label_log[i * timesteps + t] = ln_clamped(row[id]);
}
}
let mut jump_from = vec![false; states];
for j in (1..states).step_by(2) {
let prev = j / 2;
jump_from[j] = prev > 0 && labels[prev] != labels[prev - 1];
}
let mut delta = vec![neg_inf; states];
let mut next_delta = vec![neg_inf; states];
let mut emit = vec![0.0_f32; states];
let mut back = vec![0u8; timesteps * states];
for t in 0..timesteps {
for (j, e) in emit.iter_mut().enumerate() {
*e = if j % 2 == 0 {
blank_log[t]
} else {
label_log[(j / 2) * timesteps + t]
};
}
if t == 0 {
delta[0] = emit[0];
delta[1] = emit[1];
delta[2..].fill(neg_inf);
continue;
}
for j in 0..states {
let mut best = delta[j];
let mut from = 0u8;
if j > 0 && delta[j - 1] > best {
best = delta[j - 1];
from = 1;
}
if j > 1 && jump_from[j] && delta[j - 2] > best {
best = delta[j - 2];
from = 2;
}
next_delta[j] = if best == neg_inf {
neg_inf
} else {
best + emit[j]
};
back[t * states + j] = from;
}
std::mem::swap(&mut delta, &mut next_delta);
}
let mut j = if delta[states - 1] >= delta[states - 2] {
states - 1
} else {
states - 2
};
let mut ranges = vec![[usize::MAX, 0usize]; len];
for t in (0..timesteps).rev() {
if j % 2 == 1 {
let i = j / 2;
ranges[i][0] = t;
if ranges[i][1] == 0 {
ranges[i][1] = t + 1;
}
}
j -= back[t * states + j] as usize;
}
let mut previous_end = 0usize;
for range in &ranges {
if range[0] == usize::MAX
|| range[1] == 0
|| range[0] >= range[1]
|| range[0] < previous_end
{
return None;
}
previous_end = range[1];
}
Some(ranges)
}
fn ln_clamped(prob: f32) -> f32 {
prob.max(MIN_PROB).ln()
}
#[cfg(test)]
mod tests {
use ndarray::Array2;
use super::char_column_ranges;
fn peaked(frames: &[usize], class_num: usize, peak: f32) -> Array2<f32> {
let off = (1.0 - peak) / (class_num - 1) as f32;
let data: Vec<f32> = frames
.iter()
.flat_map(|&id| (0..class_num).map(move |c| if c == id { peak } else { off }))
.collect();
Array2::from_shape_vec((frames.len(), class_num), data).expect("shape matches")
}
#[test]
fn aligns_repeated_characters_to_separate_ranges() {
let probs = peaked(&[0, 1, 1, 1, 2, 2, 0], 3, 0.9);
let view = probs.view();
let ranges = char_column_ranges(view, &[1, 1, 2]).expect("alignment feasible");
assert_eq!(ranges.len(), 3);
assert!(
ranges[0][1] <= ranges[1][0],
"两个 a 必须分开:{:?} / {:?}",
ranges[0],
ranges[1]
);
assert!(ranges[0][0] >= 1 && ranges[0][1] <= 4, "{:?}", ranges[0]);
assert!(ranges[1][0] >= 1 && ranges[1][1] <= 5, "{:?}", ranges[1]);
assert_eq!(ranges[2], [4, 6]);
}
#[test]
fn aligns_when_frame_budget_is_exactly_used_up() {
let probs = peaked(&[1, 0, 2, 2], 3, 0.9);
let ranges = char_column_ranges(probs.view(), &[1, 2]).expect("exactly enough frames");
assert_eq!(ranges, vec![[0, 1], [2, 4]]);
}
#[test]
fn rejects_when_frames_cannot_hold_the_labels() {
let probs = peaked(&[1, 0, 2], 3, 0.9);
assert!(char_column_ranges(probs.view(), &[1, 2, 1, 2]).is_none());
}
#[test]
fn distinct_adjacent_labels_need_no_blank_between_them() {
let probs = peaked(&[1, 2], 3, 0.9);
let ranges = char_column_ranges(probs.view(), &[1, 2]).expect("two frames for two labels");
assert_eq!(ranges, vec![[0, 1], [1, 2]]);
}
#[test]
fn identical_adjacent_labels_cannot_share_the_frame_budget() {
let probs = peaked(&[1, 1], 3, 0.9);
assert!(char_column_ranges(probs.view(), &[1, 1]).is_none());
let probs = peaked(&[1, 0, 1], 3, 0.9);
let ranges = char_column_ranges(probs.view(), &[1, 1]).expect("three frames for a-a");
assert_eq!(ranges, vec![[0, 1], [2, 3]]);
}
#[test]
fn rejects_out_of_range_label_id() {
let probs = peaked(&[1, 1, 2, 2], 3, 0.9);
assert!(char_column_ranges(probs.view(), &[7]).is_none());
}
#[test]
fn empty_text_aligns_to_empty_ranges() {
let probs = peaked(&[0, 0], 3, 0.9);
let ranges: Vec<[usize; 2]> = char_column_ranges(probs.view(), &[]).expect("empty");
assert!(ranges.is_empty());
}
#[test]
fn wide_character_earns_more_frames_than_narrow_one() {
let mut probs = Array2::<f32>::from_elem((9, 3), 0.02);
let bright = [
(0usize, 0usize),
(1, 1),
(2, 1),
(3, 2),
(4, 2),
(5, 2),
(6, 2),
(7, 2),
(8, 0),
];
for (t, id) in bright {
probs[[t, id]] = 0.9;
}
let ranges = char_column_ranges(probs.view(), &[1, 2]).expect("alignment feasible");
let w_i = ranges[0][1] - ranges[0][0];
let w_m = ranges[1][1] - ranges[1][0];
assert_eq!(ranges[0], [1, 3]);
assert_eq!(ranges[1], [3, 8]);
assert!(
w_m > w_i,
"窄字 'i' 与宽字 'm' 拿到了同样宽度({w_i} vs {w_m}),对齐没有用上概率质量"
);
}
}