use candle_core::Tensor;
use ndarray::Array4;
use crate::{V2Encoded, encode_v2};
use crate::candle::decode::{MAX_COUNT, MAX_WIDTH, ScorerOutput, build_span_idx};
use crate::candle::heads::schema_gather::SchemaGather;
use crate::candle::heads::scorer::Scorer;
use crate::candle::heads::token_gather::TokenGather;
pub(crate) struct PipelineComponents<'a> {
pub(crate) tokenizer: &'a crate::V2Tokenizer,
pub(crate) splitter: &'a crate::V2Splitter,
pub(crate) device: &'a candle_core::Device,
pub(crate) encoder: &'a crate::candle::encoder::Encoder,
pub(crate) heads: &'a crate::candle::heads::AllHeads,
}
pub(crate) fn run_pipeline(
components: PipelineComponents,
text: &str,
labels: &[String],
) -> crate::candle::Result<(ScorerOutput, usize, V2Encoded)> {
let PipelineComponents {
tokenizer,
splitter,
device,
encoder,
heads,
} = components;
let encoded = encode_v2(text, labels, tokenizer, splitter)?;
let max_seq = encoder.config.max_position_embeddings;
let seq_len = encoded.input_ids.len().min(max_seq);
let input_ids = Tensor::from_slice(&encoded.input_ids[..seq_len], (1, seq_len), device)?;
let attn_data: Vec<i64> = vec![1_i64; seq_len];
let attention_mask = Tensor::from_slice(&attn_data[..], (1, seq_len), device)?;
let hidden = encoder
.forward(&input_ids, &attention_mask, None)
.map_err(|e| crate::candle::GlinerCandleError::Backend(format!("[pipeline:2 encoder.forward] {e}")))?;
let filtered_positions: Vec<u32> = encoded
.text_positions
.iter()
.filter(|&&pos| (pos as usize) < seq_len)
.map(|&p| p as u32)
.collect();
let num_words = filtered_positions.len();
if num_words == 0 {
return Ok((empty_scorer_output(), 0, encoded));
}
let span_rep_out = gather_and_span_rep(&hidden, &filtered_positions, num_words, heads, device)?;
let (pred_count, struct_proj) =
compute_pred_count_and_struct_proj(&hidden, &encoded.schema_positions, heads, device)?;
let Some(struct_proj) = struct_proj else {
return Ok((empty_scorer_output(), 0, encoded));
};
let num_labels = labels.len();
let scores_arr = score_and_reshape(span_rep_out, struct_proj, pred_count, num_words, num_labels, device)?;
Ok((ScorerOutput { scores: scores_arr }, pred_count, encoded))
}
fn gather_and_span_rep(
hidden: &Tensor,
filtered_positions: &[u32],
num_words: usize,
heads: &crate::candle::heads::AllHeads,
device: &candle_core::Device,
) -> crate::candle::Result<Tensor> {
let word_indices = Tensor::from_slice(filtered_positions, (num_words,), device)?;
let text_emb = TokenGather
.forward(hidden, &word_indices)
.map_err(|e| crate::candle::GlinerCandleError::Backend(format!("[pipeline:3 token_gather] {e}")))?;
let span_idx_arr = build_span_idx(num_words)?;
let span_idx_data: Vec<u32> = span_idx_arr.iter().map(|&v| v as u32).collect();
let span_idx = Tensor::from_slice(&span_idx_data[..], (1, num_words * MAX_WIDTH, 2), device)?;
heads
.span_rep
.forward(&text_emb, &span_idx)
.map_err(|e| crate::candle::GlinerCandleError::Backend(format!("[pipeline:4 span_rep] {e}")))
}
fn compute_pred_count_and_struct_proj(
hidden: &Tensor,
schema_positions: &[i64],
heads: &crate::candle::heads::AllHeads,
device: &candle_core::Device,
) -> crate::candle::Result<(usize, Option<Tensor>)> {
if schema_positions.is_empty() {
return Err(crate::candle::GlinerCandleError::Backend(
"schema_positions empty; encode_v2 must emit at least the [P] marker".to_string(),
));
}
let schema_idx: Vec<u32> = schema_positions.iter().map(|&p| p as u32).collect();
let schema_idx_t = Tensor::from_slice(&schema_idx[..], (schema_idx.len(),), device)?;
let sg_out = SchemaGather
.forward(hidden, &schema_idx_t)
.map_err(|e| crate::candle::GlinerCandleError::Backend(format!("[pipeline:5 schema_gather] {e}")))?;
let pred_count = heads
.count_pred
.forward(&sg_out.pc_emb)
.map_err(|e| crate::candle::GlinerCandleError::Backend(format!("[pipeline:6 count_pred] {e}")))?;
if pred_count == 0 {
return Ok((0, None));
}
let struct_proj = heads
.count_lstm
.forward(&sg_out.field_embs, pred_count, device)
.map_err(|e| crate::candle::GlinerCandleError::Backend(format!("[pipeline:7 count_lstm] {e}")))?;
Ok((pred_count, Some(struct_proj)))
}
fn score_and_reshape(
span_rep_out: Tensor,
struct_proj: Tensor,
pred_count: usize,
num_words: usize,
num_labels: usize,
device: &candle_core::Device,
) -> crate::candle::Result<Array4<f32>> {
let span_rep_per_sample = span_rep_out.squeeze(0)?;
let scores = Scorer
.forward(&span_rep_per_sample, &struct_proj)
.map_err(|e| crate::candle::GlinerCandleError::Backend(format!("[pipeline:8 scorer] {e}")))?;
let scores = scores.permute((0, 2, 3, 1))?.contiguous()?;
let scores_padded: Tensor = if pred_count < MAX_COUNT {
let pad_shape = (MAX_COUNT - pred_count, num_words, MAX_WIDTH, num_labels);
let pad = Tensor::zeros(pad_shape, scores.dtype(), device)?;
Tensor::cat(&[&scores, &pad], 0)?
} else {
scores
};
let scores_vec: Vec<f32> = scores_padded
.to_dtype(candle_core::DType::F32)?
.flatten_all()?
.to_vec1::<f32>()?;
Array4::from_shape_vec((MAX_COUNT, num_words, MAX_WIDTH, num_labels), scores_vec)
.map_err(|e| crate::candle::GlinerCandleError::Backend(format!("scores reshape: {e}")))
}
fn empty_scorer_output() -> ScorerOutput {
ScorerOutput {
scores: Array4::zeros((0, 0, 0, 0)),
}
}
#[cfg(test)]
mod tests {
#[test]
fn run_pipeline_is_declared() {
#[allow(clippy::type_complexity)]
fn _assert_signature(
f: fn(
super::PipelineComponents,
&str,
&[String],
) -> crate::candle::Result<(crate::candle::decode::ScorerOutput, usize, crate::V2Encoded)>,
) {
let _ = f;
}
_assert_signature(super::run_pipeline);
}
}