Skip to main content

cloudiful_docling_convert/api/
source.rs

1use std::collections::BTreeMap;
2
3use serde::Serialize;
4use serde_json::Value;
5
6use crate::document::{ChunkerKind, ChunkingOptions, InputKind, PipelineKind};
7
8use super::docling::DoclingConvertRequest;
9
10#[derive(Debug, Clone, Serialize)]
11#[serde(tag = "kind")]
12pub enum HttpSource {
13    #[serde(rename = "http")]
14    Http {
15        url: String,
16        #[serde(default)]
17        headers: BTreeMap<String, Value>,
18    },
19}
20
21impl HttpSource {
22    pub fn new(url: impl Into<String>) -> Self {
23        Self::Http {
24            url: url.into(),
25            headers: BTreeMap::new(),
26        }
27    }
28}
29
30#[derive(Debug, Clone, Serialize)]
31#[serde(tag = "kind")]
32pub enum TargetKind {
33    #[serde(rename = "inbody")]
34    Inbody {},
35    #[serde(rename = "zip")]
36    Zip {},
37}
38
39#[derive(Debug, Clone, Serialize)]
40pub(crate) struct ConvertDocumentsOptions {
41    pub from_formats: Vec<String>,
42    pub to_formats: Vec<String>,
43    #[serde(skip_serializing_if = "Option::is_none")]
44    pub pipeline: Option<String>,
45    #[serde(skip_serializing_if = "Option::is_none")]
46    pub page_range: Option<[u64; 2]>,
47    /// Preset ID for picture description. When set, Docling Serve uses the
48    /// admin-defined preset and ignores any `picture_description_custom_config`
49    /// the legacy VLM bundle may emit on the multipart convert endpoint.
50    #[serde(skip_serializing_if = "Option::is_none")]
51    pub picture_description_preset: Option<String>,
52}
53
54#[derive(Debug, Clone, Serialize)]
55pub(crate) struct ConvertSourcesRequest {
56    pub options: ConvertDocumentsOptions,
57    pub sources: Vec<HttpSource>,
58    pub target: TargetKind,
59}
60
61#[derive(Debug, Clone, Serialize)]
62pub(crate) struct ChunkSourcesRequest {
63    pub convert_options: ConvertDocumentsOptions,
64    pub sources: Vec<HttpSource>,
65    pub include_converted_doc: bool,
66    pub target: TargetKind,
67    pub chunking_options: ChunkingRequestOptions,
68}
69
70#[derive(Debug, Clone, Serialize)]
71#[serde(tag = "chunker")]
72pub(crate) enum ChunkingRequestOptions {
73    #[serde(rename = "hybrid")]
74    Hybrid {
75        use_markdown_tables: bool,
76        use_markdown_images: bool,
77        image_placeholder: String,
78        include_raw_text: bool,
79        max_tokens: Option<u32>,
80        tokenizer: String,
81        merge_peers: bool,
82    },
83    #[serde(rename = "hierarchical")]
84    Hierarchical {
85        use_markdown_tables: bool,
86        use_markdown_images: bool,
87        image_placeholder: String,
88        include_raw_text: bool,
89    },
90}
91
92pub(crate) fn convert_options(
93    input_kind: InputKind,
94    request: &DoclingConvertRequest,
95) -> ConvertDocumentsOptions {
96    ConvertDocumentsOptions {
97        from_formats: vec![input_kind.from_formats_value().to_string()],
98        to_formats: request
99            .output_formats
100            .iter()
101            .map(|format| format.as_api_value().to_string())
102            .collect(),
103        pipeline: request.pipeline.map(|pipeline| pipeline.to_string()),
104        page_range: request
105            .page_range
106            .map(|(start, end)| [start as u64, end as u64]),
107        picture_description_preset: request.picture_description_preset.clone(),
108    }
109}
110
111pub(crate) fn target_for(request: &DoclingConvertRequest) -> TargetKind {
112    if request
113        .output_formats
114        .iter()
115        .any(|format| format.is_archive())
116    {
117        TargetKind::Zip {}
118    } else {
119        TargetKind::Inbody {}
120    }
121}
122
123pub(crate) fn chunking_options(
124    chunker: ChunkerKind,
125    options: &ChunkingOptions,
126) -> crate::error::Result<ChunkingRequestOptions> {
127    match chunker {
128        ChunkerKind::Hybrid => {
129            let defaults = ChunkingOptions::hybrid_defaults();
130            Ok(ChunkingRequestOptions::Hybrid {
131                use_markdown_tables: options.use_markdown_tables,
132                use_markdown_images: options.use_markdown_images,
133                image_placeholder: if options.image_placeholder.is_empty() {
134                    defaults.image_placeholder
135                } else {
136                    options.image_placeholder.clone()
137                },
138                include_raw_text: options.include_raw_text,
139                max_tokens: options.max_tokens,
140                tokenizer: options
141                    .tokenizer
142                    .clone()
143                    .unwrap_or_else(|| defaults.tokenizer.unwrap_or_default()),
144                merge_peers: options.merge_peers,
145            })
146        }
147        ChunkerKind::Hierarchical => Ok(ChunkingRequestOptions::Hierarchical {
148            use_markdown_tables: options.use_markdown_tables,
149            use_markdown_images: options.use_markdown_images,
150            image_placeholder: if options.image_placeholder.is_empty() {
151                ChunkingOptions::hierarchical_defaults().image_placeholder
152            } else {
153                options.image_placeholder.clone()
154            },
155            include_raw_text: options.include_raw_text,
156        }),
157        ChunkerKind::None => Err(crate::error::PdfConvertError::validation_error(
158            "chunker",
159            "chunking options require hybrid or hierarchical",
160        )),
161    }
162}
163
164pub(crate) fn source_request(
165    url: impl Into<String>,
166    input_kind: InputKind,
167    request: &DoclingConvertRequest,
168) -> ConvertSourcesRequest {
169    ConvertSourcesRequest {
170        options: convert_options(input_kind, request),
171        sources: vec![HttpSource::new(url)],
172        target: target_for(request),
173    }
174}
175
176pub(crate) fn chunk_source_request(
177    url: impl Into<String>,
178    input_kind: InputKind,
179    request: &DoclingConvertRequest,
180) -> crate::error::Result<ChunkSourcesRequest> {
181    Ok(ChunkSourcesRequest {
182        convert_options: convert_options(input_kind, request),
183        sources: vec![HttpSource::new(url)],
184        include_converted_doc: false,
185        target: target_for(request),
186        chunking_options: chunking_options(request.chunker, &request.chunking)?,
187    })
188}
189
190impl std::fmt::Display for PipelineKind {
191    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
192        let value = match self {
193            PipelineKind::Legacy => "legacy",
194            PipelineKind::Standard => "standard",
195            PipelineKind::Vlm => "vlm",
196            PipelineKind::Asr => "asr",
197        };
198        f.write_str(value)
199    }
200}