Skip to main content

cloudiful_docling_convert/
conversion.rs

1use crate::api::{DoclingClient, DoclingConfig};
2use crate::document::{
3    ConvertOptions, GenericFileConvertOptions, InputDocument, InputKind, PdfConvertOptions,
4    TextConvertOptions,
5};
6use crate::error::{PdfConvertError, Result};
7use crate::pdf::PdfInfo;
8use crate::processor::build_pdf_chunk_plan;
9
10#[derive(Debug, Clone)]
11pub struct DoclingRuntimeConfig {
12    pub docling_base_url: String,
13    pub openai_base_url: String,
14    pub vlm_pipeline_model: String,
15    pub picture_description_model: String,
16    pub code_formula_model: String,
17    pub api_key: Option<String>,
18}
19
20impl DoclingRuntimeConfig {
21    pub fn without_vlm(docling_base_url: impl Into<String>) -> Self {
22        Self {
23            docling_base_url: docling_base_url.into(),
24            openai_base_url: String::new(),
25            vlm_pipeline_model: String::new(),
26            picture_description_model: String::new(),
27            code_formula_model: String::new(),
28            api_key: None,
29        }
30    }
31
32    pub fn into_docling_config(self) -> DoclingConfig {
33        DoclingConfig {
34            base_url: self.docling_base_url,
35            openai_base_url: self.openai_base_url,
36            vlm_pipeline_model: self.vlm_pipeline_model,
37            picture_description_model: self.picture_description_model,
38            code_formula_model: self.code_formula_model,
39            api_key: self.api_key,
40        }
41    }
42}
43
44#[derive(Debug, Clone)]
45pub struct ConversionBehavior {
46    pub pages_per_file: u32,
47    pub split_input: bool,
48    pub split_by_bookmark: bool,
49    pub chunking: bool,
50    pub batch_size: usize,
51}
52
53impl Default for ConversionBehavior {
54    fn default() -> Self {
55        Self {
56            pages_per_file: 5,
57            split_input: true,
58            split_by_bookmark: false,
59            chunking: false,
60            batch_size: 2,
61        }
62    }
63}
64
65pub fn build_docling_client(config: DoclingRuntimeConfig) -> Result<DoclingClient> {
66    DoclingClient::new(config.into_docling_config())
67}
68
69pub fn build_convert_options(
70    input_kind: InputKind,
71    behavior: &ConversionBehavior,
72) -> Result<ConvertOptions> {
73    if matches!(input_kind, InputKind::Pdf) {
74        Ok(ConvertOptions::Pdf(PdfConvertOptions {
75            pages_per_file: behavior.pages_per_file,
76            split_input: behavior.split_input,
77            split_by_bookmark: behavior.split_by_bookmark,
78            chunking: behavior.chunking,
79            batch_size: behavior.batch_size,
80        }))
81    } else if input_kind.uses_generic_convert_options() {
82        reject_pdf_only_options(input_kind, behavior)?;
83        Ok(ConvertOptions::Generic(GenericFileConvertOptions {
84            chunking: behavior.chunking,
85        }))
86    } else {
87        reject_pdf_only_options(input_kind, behavior)?;
88        Ok(ConvertOptions::Text(TextConvertOptions::default()))
89    }
90}
91
92pub fn build_pdf_options(behavior: &ConversionBehavior) -> Result<PdfConvertOptions> {
93    let options = PdfConvertOptions {
94        pages_per_file: behavior.pages_per_file,
95        split_input: behavior.split_input,
96        split_by_bookmark: behavior.split_by_bookmark,
97        chunking: behavior.chunking,
98        batch_size: behavior.batch_size,
99    };
100    options.validate()?;
101    Ok(options)
102}
103
104pub fn count_input_chunks(input: &InputDocument, behavior: &ConversionBehavior) -> Result<usize> {
105    match input.kind()? {
106        InputKind::Pdf => {
107            let pdf_info = PdfInfo::load_from_bytes(input.bytes.as_ref())?;
108            let total = build_pdf_chunk_plan(&build_pdf_options(behavior)?, &pdf_info)
109                .len()
110                .max(1);
111            Ok(total)
112        }
113        _ => Ok(1),
114    }
115}
116
117fn reject_pdf_only_options(input_kind: InputKind, behavior: &ConversionBehavior) -> Result<()> {
118    if behavior.split_by_bookmark {
119        return Err(PdfConvertError::validation_error(
120            "split_by_bookmark",
121            format!(
122                "bookmark splitting is only available for PDF inputs, got {:?}",
123                input_kind
124            ),
125        ));
126    }
127
128    Ok(())
129}
130
131#[cfg(test)]
132mod tests {
133    use super::*;
134
135    #[test]
136    fn generic_input_rejects_bookmark_splitting() {
137        let err = build_convert_options(
138            InputKind::Docx,
139            &ConversionBehavior {
140                split_by_bookmark: true,
141                ..ConversionBehavior::default()
142            },
143        )
144        .unwrap_err();
145
146        assert!(err.to_string().contains("bookmark splitting"));
147    }
148
149    #[test]
150    fn second_wave_inputs_use_generic_convert_options() {
151        let options =
152            build_convert_options(InputKind::XmlJats, &ConversionBehavior::default()).unwrap();
153
154        assert!(matches!(options, ConvertOptions::Generic(_)));
155    }
156}