use serde_json::json;
use super::common::{
classify_chunk, collect_slide_names, open_pptx,
parse_presentation_sections, read_all_slides, split_large_text, ChunkRecordInput, ContentType,
SlideContent,
};
const MAX_SECTION_CHARS: usize = 2000;
pub fn build_section_chunks(bytes: &[u8]) -> Result<Vec<ChunkRecordInput>, String> {
let mut archive = open_pptx(bytes)?;
let slide_names = collect_slide_names(&archive);
if slide_names.is_empty() {
return Err("No slides found in PPTX archive".to_string());
}
let total_slides = slide_names.len();
let slides = read_all_slides(&mut archive, &slide_names)?;
let named_sections = parse_presentation_sections(&mut archive).unwrap_or_default();
let result = if !named_sections.is_empty() {
build_from_named_sections(slides, named_sections, total_slides)
} else {
build_from_divider_heuristic(slides, total_slides)
};
if result.is_empty() {
return Err("No section chunks generated from PPTX".to_string());
}
Ok(result)
}
fn flush_section_body(
result: &mut Vec<ChunkRecordInput>,
section_name: &str,
slide_texts: &[(usize, String, Option<String>)],
total_slides: usize,
) {
if slide_texts.is_empty() {
return;
}
let combined = slide_texts
.iter()
.map(|(_, t, _)| t.as_str())
.collect::<Vec<_>>()
.join("\n\n");
let combined = combined.trim().to_string();
if combined.is_empty() {
return;
}
let first_slide = slide_texts[0].0;
let last_slide = slide_texts.last().unwrap().0;
let parts = if combined.len() > MAX_SECTION_CHARS {
split_large_text(&combined, MAX_SECTION_CHARS)
} else {
vec![combined.clone()]
};
let part_count = parts.len();
for (i, content) in parts.into_iter().enumerate() {
result.push(ChunkRecordInput {
content_type: ContentType::Section,
content: content.clone(),
metadata: json!({
"section_heading": section_name,
"slide_range": [first_slide, last_slide],
"slide_count": slide_texts.len(),
"split_part": if part_count > 1 { json!(i+1) } else { serde_json::Value::Null },
"split_total": if part_count > 1 { json!(part_count) } else { serde_json::Value::Null },
"document_metadata": { "source_type": "pptx", "total_slides": total_slides }
}),
});
}
}
fn build_from_named_sections(
slides: Vec<(usize, SlideContent)>,
sections: Vec<(String, Vec<usize>)>,
total_slides: usize,
) -> Vec<ChunkRecordInput> {
let mut result: Vec<ChunkRecordInput> = Vec::new();
for (section_name, positions) in sections {
let pos_set: std::collections::HashSet<usize> = positions.into_iter().collect();
let section_slides: Vec<(usize, String, Option<String>)> = slides
.iter()
.filter(|(n, _)| pos_set.contains(n))
.map(|(n, s)| {
let text = s.all_text();
(*n, text, s.title.clone())
})
.filter(|(_, t, _)| !t.is_empty())
.collect();
if section_slides.is_empty() {
continue;
}
result.push(ChunkRecordInput {
content_type: ContentType::HeadingSection,
content: section_name.clone(),
metadata: json!({
"section_heading": section_name,
"slide_range": [section_slides.first().map(|(n,_,_)| *n).unwrap_or(0),
section_slides.last().map(|(n,_,_)| *n).unwrap_or(0)],
"slide_count": section_slides.len(),
"document_metadata": { "source_type": "pptx", "total_slides": total_slides }
}),
});
flush_section_body(&mut result, §ion_name, §ion_slides, total_slides);
}
result
}
fn build_from_divider_heuristic(
slides: Vec<(usize, SlideContent)>,
total_slides: usize,
) -> Vec<ChunkRecordInput> {
let mut result: Vec<ChunkRecordInput> = Vec::new();
let mut current_section: Option<String> = None;
let mut current_body: Vec<(usize, String, Option<String>)> = Vec::new();
let flush = |result: &mut Vec<ChunkRecordInput>,
section: &Option<String>,
body: &mut Vec<(usize, String, Option<String>)>,
total: usize| {
if let Some(name) = section {
flush_section_body(result, name, body, total);
} else if !body.is_empty() {
let intro_name = body
.first()
.and_then(|(_, _, t)| t.as_ref().map(|s| s.as_str()))
.unwrap_or("Introduction")
.to_string();
flush_section_body(result, &intro_name, body, total);
}
body.clear();
};
for (slide_num, slide) in &slides {
if slide.is_section_divider() {
flush(
&mut result,
¤t_section,
&mut current_body,
total_slides,
);
let heading = slide.title.clone().unwrap_or_default();
current_section = Some(heading.clone());
result.push(ChunkRecordInput {
content_type: ContentType::HeadingSection,
content: heading.clone(),
metadata: json!({
"section_heading": heading,
"slide_range": [slide_num, slide_num],
"slide_count": 0,
"document_metadata": { "source_type": "pptx", "total_slides": total_slides }
}),
});
} else {
let text = slide.all_text();
if !text.is_empty() {
current_body.push((*slide_num, text, slide.title.clone()));
}
}
}
flush(
&mut result,
¤t_section,
&mut current_body,
total_slides,
);
if result.is_empty()
|| result
.iter()
.all(|c| c.content_type == ContentType::HeadingSection)
{
result.clear();
for (slide_num, slide) in &slides {
let text = slide.all_text();
if text.is_empty() {
continue;
}
for content in split_large_text(&text, MAX_SECTION_CHARS) {
result.push(ChunkRecordInput {
content_type: classify_chunk(&content),
content: content.clone(),
metadata: json!({
"section_heading": slide.title,
"slide_range": [slide_num, slide_num],
"slide_count": 1,
"document_metadata": { "source_type": "pptx", "total_slides": total_slides }
}),
});
}
}
}
result
}