pub mod chunker;
pub mod gemini_client;
pub mod pdf_reader;
pub mod ui;
use crate::ui::VerboseUI;
use anyhow::Result;
use std::sync::Arc;
use tokio::sync::Mutex;
pub struct Config {
pub api_key: String,
pub chunk_size: usize,
}
pub async fn process_pdf(path: &str, config: &Config) -> Result<String> {
let ui = VerboseUI::new();
ui.print_banner();
ui.print_file_info(path, 0); let pdf_data = pdf_reader::read_pdf(path)?;
let total_size = pdf_data.len();
ui.print_file_info(path, total_size);
ui.print_upload_start();
let client = gemini_client::GeminiClient::new(&config.api_key);
let upload_pb = ui.create_upload_progress(total_size as u64);
tokio::spawn(async move {
for i in 0..=100 {
upload_pb.set_position((total_size as u64 * i / 100) as u64);
tokio::time::sleep(tokio::time::Duration::from_millis(20)).await;
}
upload_pb.finish();
});
let file_uri = client.upload_full_pdf(&pdf_data, "arabic_pdf").await?;
ui.print_upload_complete(&file_uri);
let pages_per_chunk = 5; let total_pages: usize = 30; #[allow(clippy::manual_div_ceil)]
let num_chunks = (total_pages + pages_per_chunk - 1) / pages_per_chunk;
ui.print_processing_start(num_chunks, pages_per_chunk);
let client = Arc::new(client);
let file_uri = Arc::new(file_uri);
let ui = Arc::new(ui);
let progress_bars: Vec<_> = (0..num_chunks)
.map(|i| {
let start = i * pages_per_chunk + 1;
let end = ((i + 1) * pages_per_chunk).min(total_pages);
Arc::new(Mutex::new(ui.create_chunk_progress(start, end)))
})
.collect();
let semaphore = Arc::new(tokio::sync::Semaphore::new(2));
let tasks: Vec<_> = (0..num_chunks)
.map(|chunk_idx| {
let client = client.clone();
let sem = semaphore.clone();
let file_uri = file_uri.clone();
let start_page = chunk_idx * pages_per_chunk + 1;
let end_page = ((chunk_idx + 1) * pages_per_chunk).min(total_pages);
let pb = progress_bars[chunk_idx].clone();
let ui = ui.clone();
tokio::spawn(async move {
let _permit = sem.acquire().await.unwrap();
{
let pb_lock = pb.lock().await;
ui.update_chunk_progress(&pb_lock, "processing", 20);
}
if chunk_idx > 0 {
tokio::time::sleep(tokio::time::Duration::from_secs(6)).await;
}
{
let pb_lock = pb.lock().await;
ui.update_chunk_progress(&pb_lock, "processing", 50);
}
let mut retries = 3;
let mut result = client
.extract_page_range(&file_uri, start_page, end_page)
.await;
while retries > 0 && result.is_err() {
if let Err(e) = &result {
if e.to_string().contains("RESOURCE_EXHAUSTED") {
{
let pb_lock = pb.lock().await;
ui.update_chunk_progress(&pb_lock, "rate_limited", 70);
}
ui.print_rate_limit_warning(chunk_idx + 1, 30);
retries -= 1;
result = client
.extract_page_range(&file_uri, start_page, end_page)
.await;
} else {
break;
}
}
}
match &result {
Ok(text) => {
let pb_lock = pb.lock().await;
if text.trim().is_empty() {
ui.update_chunk_progress(&pb_lock, "completed", 100);
} else {
ui.update_chunk_progress(&pb_lock, "completed", 100);
ui.print_extraction_stats(chunk_idx + 1, text.len());
}
}
Err(e) => {
let pb_lock = pb.lock().await;
ui.update_chunk_progress(&pb_lock, "failed", 100);
ui.print_error(&format!("Failed pages {start_page}-{end_page}: {e}"));
}
}
(chunk_idx, result)
})
})
.collect();
let mut results = vec![String::new(); tasks.len()];
let mut failed_ranges = Vec::new();
let mut last_non_empty_chunk = 0;
for task in tasks {
let (index, result) = task.await?;
match result {
Ok(text) => {
if !text.trim().is_empty() {
results[index] = text;
last_non_empty_chunk = index;
}
}
Err(e) => {
let start = index * pages_per_chunk + 1;
let end = ((index + 1) * pages_per_chunk).min(total_pages);
failed_ranges.push((start, end));
eprintln!("Pages {start}-{end} failed: {e}");
results[index] = format!("[Pages {start}-{end} failed to process]");
}
}
}
results.truncate(last_non_empty_chunk + 1);
let success_count = results
.iter()
.filter(|s| !s.is_empty() && !s.starts_with("[Pages"))
.count();
let total_chars: usize = results.iter().map(|s| s.len()).sum();
ui.print_final_summary(num_chunks, success_count, failed_ranges.len(), total_chars);
let final_text = results
.into_iter()
.filter(|s| !s.is_empty() && !s.starts_with("[Pages"))
.collect::<Vec<_>>()
.join("\n\n--- Page Break ---\n\n");
Ok(final_text)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_chunk_size_calculation() {
let data = vec![0u8; 1000];
let chunks = chunker::chunk_pdf(&data, 100);
assert_eq!(chunks.len(), 10);
}
#[tokio::test]
async fn test_gemini_client_mock() {
let _server = mockito::Server::new_async().await;
}
}