cloudiful_docling_convert/
conversion.rs1use 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}