use super::vlm_ocr::vlm_ocr;
use crate::core::config::LlmConfig;
use crate::types::LlmUsage;
#[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
pub enum RegionKind {
Figure,
DenseTable,
ComplexLayout,
Caption,
}
impl RegionKind {
pub fn default_prompt(self) -> &'static str {
match self {
Self::Figure => REGION_FIGURE_TEMPLATE,
Self::DenseTable => REGION_DENSE_TABLE_TEMPLATE,
Self::ComplexLayout => REGION_COMPLEX_LAYOUT_TEMPLATE,
Self::Caption => REGION_CAPTION_TEMPLATE,
}
}
}
impl From<String> for RegionKind {
fn from(s: String) -> Self {
match s.as_str() {
"Figure" => Self::Figure,
"DenseTable" => Self::DenseTable,
"ComplexLayout" => Self::ComplexLayout,
"Caption" => Self::Caption,
_ => Self::Figure,
}
}
}
const REGION_FIGURE_TEMPLATE: &str = "\
Describe this figure or diagram in detail. Include:
- The type of figure (chart, graph, diagram, photo, illustration, etc.)
- All text visible in the figure (labels, titles, legends, axis names, annotations)
- The key data or relationships the figure conveys
- Any embedded numeric values, percentages, or measurements
Return the description as concise markdown. Do not add headings — return only \
a paragraph or a short bulleted list if appropriate. If the figure contains no \
meaningful content, return an empty string.";
const REGION_DENSE_TABLE_TEMPLATE: &str = "\
Extract the table from this image as GitHub-Flavoured Markdown.
- Preserve all columns and rows exactly as they appear.
- Use `|` column separators and a `---` separator row after the header.
- If the table has no visible header, create a row of empty header cells.
- Do not add explanatory text — return only the Markdown table.
- If the image does not contain a table, return an empty string.";
const REGION_COMPLEX_LAYOUT_TEMPLATE: &str = "\
Extract all text and structured content from this image region as Markdown.
- Preserve the original reading order (top to bottom, left to right).
- Use appropriate Markdown elements: paragraphs, lists, code blocks, tables.
- Do not add commentary or explanations beyond what the image contains.
- If the region contains no meaningful text, return an empty string.";
const REGION_CAPTION_TEMPLATE: &str = "\
Write a concise, factual caption for this image suitable for use as alt text \
or a search-index entry.
- One or two sentences at most.
- Describe what is visible: subject, action, setting, notable text.
- Do not speculate about intent, mood, or context that is not visible.
- Do not start the caption with phrases like \"This image shows\" or \
\"A picture of\" — lead with the subject.
- If the image has no recognisable content, return an empty string.";
pub async fn extract_region_with_vlm(
image_bytes: &[u8],
image_mime: &str,
region_kind: RegionKind,
llm_config: &LlmConfig,
custom_prompt: Option<&str>,
) -> crate::Result<String> {
let (text, _usage) =
extract_region_with_vlm_usage(image_bytes, image_mime, region_kind, llm_config, custom_prompt).await?;
Ok(text)
}
#[cfg_attr(alef, alef(skip))]
pub async fn extract_region_with_vlm_usage(
image_bytes: &[u8],
image_mime: &str,
region_kind: RegionKind,
llm_config: &LlmConfig,
custom_prompt: Option<&str>,
) -> crate::Result<(String, Option<LlmUsage>)> {
let prompt = custom_prompt.unwrap_or_else(|| region_kind.default_prompt());
vlm_ocr(image_bytes, image_mime, "eng", llm_config, Some(prompt)).await
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_region_kind_default_prompt_figure() {
let prompt = RegionKind::Figure.default_prompt();
assert!(
prompt.contains("diagram") || prompt.contains("figure"),
"figure prompt must mention figures/diagrams; got: {prompt}"
);
}
#[test]
fn test_region_kind_default_prompt_dense_table() {
let prompt = RegionKind::DenseTable.default_prompt();
assert!(
prompt.contains("Markdown") || prompt.contains("table"),
"dense table prompt must mention Markdown/table; got: {prompt}"
);
}
#[test]
fn test_region_kind_default_prompt_complex_layout() {
let prompt = RegionKind::ComplexLayout.default_prompt();
assert!(
prompt.contains("Markdown") || prompt.contains("reading order"),
"complex layout prompt must mention Markdown; got: {prompt}"
);
}
#[test]
fn test_region_kind_prompts_are_non_empty() {
for kind in [
RegionKind::Figure,
RegionKind::DenseTable,
RegionKind::ComplexLayout,
RegionKind::Caption,
] {
assert!(
!kind.default_prompt().is_empty(),
"{kind:?} default prompt must not be empty"
);
}
}
#[test]
fn test_region_kind_default_prompt_caption() {
let prompt = RegionKind::Caption.default_prompt();
assert!(
prompt.contains("caption") || prompt.contains("alt text"),
"caption prompt must mention captions/alt text; got: {prompt}"
);
}
#[test]
fn test_region_kind_equality() {
assert_eq!(RegionKind::Figure, RegionKind::Figure);
assert_ne!(RegionKind::Figure, RegionKind::DenseTable);
assert_ne!(RegionKind::DenseTable, RegionKind::ComplexLayout);
}
}