Skip to main content

xberg/
lib.rs

1//! Xberg - High-Performance Document Intelligence Library
2//!
3//! Xberg is a Rust-first document extraction library with language-agnostic plugin support.
4//! It provides fast, accurate extraction from PDFs, images, Office documents, emails, and more.
5//!
6//! # Quick Start
7//!
8//! ```rust,no_run
9//! use xberg::{extract, ExtractInput, ExtractionConfig};
10//!
11//! # async fn run() -> xberg::Result<()> {
12//! let config = ExtractionConfig::default();
13//! let output = extract(ExtractInput::from_uri("document.pdf"), &config).await?;
14//! println!("Extracted: {}", output.results[0].content);
15//! # Ok(())
16//! # }
17//! ```
18//!
19//! # Architecture
20//!
21//! - **Core Module** (`core`): Main extraction orchestration, MIME detection, config loading
22//! - **Plugin System**: Language-agnostic plugin architecture
23//! - **Extractors**: Format-specific extraction (PDF, images, Office docs, email, etc.)
24//! - **OCR**: Multiple OCR backend support (Tesseract, PaddleOCR, VLM)
25//!
26//! # Features
27//!
28//! - Fast parallel processing with async/await
29//! - Priority-based extractor selection
30//! - Comprehensive MIME type detection (118+ file extensions)
31//! - Configurable caching and quality processing
32//! - Cross-language plugin support (Python, Node.js planned)
33
34#![deny(unsafe_code)]
35
36pub mod cache;
37pub(crate) mod cache_dir;
38pub mod cancellation;
39pub mod core;
40pub mod engine;
41pub mod error;
42/// Format-specific document extraction implementations and office metadata types.
43pub mod extraction;
44pub mod extractors;
45#[cfg(all(
46    feature = "layout-detection",
47    any(feature = "pdf", feature = "ocr", feature = "ocr-wasm")
48))]
49pub mod model_cache;
50pub mod plugins;
51pub mod rendering;
52pub mod telemetry;
53/// Text post-processing: NER, summarisation, redaction, token reduction, and translation.
54pub mod text;
55pub mod types;
56pub mod utils;
57
58#[cfg(any(feature = "ocr", feature = "pdf", feature = "paddle-ocr"))]
59pub mod table_core;
60
61#[cfg(feature = "tower-service")]
62pub mod service;
63
64#[cfg(feature = "api")]
65pub mod api;
66
67#[cfg(feature = "mcp")]
68pub mod mcp;
69
70#[cfg(feature = "chunking")]
71pub mod chunking;
72
73#[cfg(feature = "diff")]
74pub mod diff;
75
76#[cfg(all(feature = "liter-llm", not(target_arch = "wasm32")))]
77pub mod llm;
78
79#[cfg(feature = "embedding-presets")]
80pub mod embeddings;
81
82#[cfg(any(feature = "reranker-presets", feature = "reranker"))]
83pub mod reranking;
84
85/// Shared ONNX Runtime model-loading helpers (download, tokenizer, session).
86#[cfg(feature = "onnx-runtime")]
87pub(crate) mod onnx;
88
89/// Sparse (SPLADE) learned embeddings for hybrid dense+sparse retrieval.
90#[cfg(any(feature = "sparse-embedding-presets", feature = "sparse-embeddings"))]
91pub mod sparse_embeddings;
92
93/// ColBERT late-interaction (multi-vector) embeddings for MaxSim retrieval.
94#[cfg(any(feature = "late-interaction-presets", feature = "late-interaction"))]
95pub mod late_interaction;
96
97#[cfg(feature = "ocr")]
98/// Image preprocessing and DPI utilities for OCR pipelines.
99pub mod image;
100
101#[cfg(feature = "language-detection")]
102pub mod language_detection;
103
104#[cfg(feature = "stopwords")]
105pub mod stopwords;
106
107#[cfg(any(feature = "keywords-yake", feature = "keywords-rake"))]
108pub mod keywords;
109
110#[cfg(feature = "enrichment")]
111pub mod enrichment;
112
113#[cfg(feature = "heuristics")]
114pub mod heuristics;
115
116#[cfg(feature = "heuristics")]
117pub use heuristics::{
118    BoundaryReason, ChunkInfo, ChunkPlan, ChunkingDecision, ChunkingReason, ConfidenceSignals, ConfidenceWeights,
119    DocumentBoundary, DocumentMetadata, HeuristicsConfig, HeuristicsError, MultidocInput, MultidocThresholds,
120    NoChunkingReason, PageRange, PageSignals, SchemaCompliance, StructuredCallMode, StructuredInput,
121    StructuredThresholds, UserChunkConfig, analyze_document, analyze_with_user_chunks,
122    boundaries_from_extraction_result, calculate_chunk_plan, calculate_plan_from_overrides, check_format_limits,
123    choose_call_mode, detect_boundaries, score_confidence,
124};
125
126#[cfg(feature = "presets")]
127pub mod presets;
128
129#[cfg(any(feature = "ocr", feature = "ocr-wasm"))]
130pub mod ocr;
131
132#[cfg(any(
133    feature = "paddle-ocr",
134    feature = "embeddings",
135    feature = "reranker",
136    feature = "onnx-runtime",
137    feature = "layout-detection",
138    feature = "auto-rotate",
139    feature = "transcription"
140))]
141pub mod ort_discovery;
142
143#[cfg(not(target_arch = "wasm32"))]
144pub(crate) mod model_download;
145
146/// Engine-neutral inference seam (issue #1275): backend/session traits over ONNX
147/// Runtime on native builds and the pure-Rust `tract` engine on no-ORT targets
148/// (Android x86_64; WASM once embedded-weight loading lands). `auto_rotate` covers
149/// both the ORT `auto-rotate` and the tract `auto-rotate-tract` variants; `layout_detection`
150/// covers both the ORT `layout-detection` and the tract `layout-tract` variants.
151#[cfg(any(layout_detection, auto_rotate))]
152pub(crate) mod inference;
153
154#[cfg(any(feature = "paddle-ocr", feature = "paddle-ocr-types"))]
155pub mod paddle_ocr;
156
157#[cfg(feature = "candle-ocr")]
158pub mod candle_ocr;
159
160#[cfg(feature = "auto-rotate-types")]
161pub mod doc_orientation;
162
163#[cfg(feature = "layout-types")]
164pub mod layout;
165
166#[cfg(feature = "pdf")]
167pub mod pdf;
168
169#[cfg(feature = "transcription")]
170pub mod transcription;
171
172#[cfg(feature = "captioning")]
173pub mod captioning;
174
175// NOTE: `CancellationToken` is intentionally NOT re-exported here.
176pub use error::{Result, XbergError};
177pub use types::*;
178
179// root (`#[frb(mirror(CoreProperties))]` → `xberg::CoreProperties`).
180#[cfg(feature = "office")]
181pub use extraction::office_metadata::{CoreProperties, DocxAppProperties};
182
183#[cfg(feature = "url-ingestion")]
184pub use core::extract::map_url;
185pub use core::extract::{extract, extract_batch};
186#[cfg(feature = "pdf")]
187pub use core::split::{SplitConfig, SplitSegment, SplitStrategy, split_and_extract};
188
189pub use core::config::{
190    AccelerationConfig, CallMode, CaptioningConfig, ChunkClassificationConfig, ChunkClassificationDefinition,
191    ChunkSizing, ChunkerType, ChunkingConfig, ContentFilterConfig, EmailConfig, EmbeddingConfig, EmbeddingModelType,
192    ExecutionProviderType, ExtractInput, ExtractInputKind, ExtractionConfig, ExtractionErrorItem, ExtractionResult,
193    ExtractionSummary, FileExtractionConfig, ImageExtractionConfig, JupyterCellRendering, LanguageDetectionConfig,
194    LlmConfig, MergeMode, NerBackendKind, NerConfig, OcrConfig, OutputFormat, PageClassificationConfig, PageConfig,
195    PostProcessorConfig, RedactionConfig, RedactionPattern, RedactionTerm, RerankerConfig, RerankerHead,
196    RerankerModelType, StructuredExtractionConfig, SummarizationConfig, TableChunkingMode, TokenReductionOptions,
197    TranslationConfig, UrlExtractionConfig, UrlExtractionMode,
198};
199pub use core::config::{
200    LateInteractionConfig, LateInteractionModelType, SparseEmbeddingConfig, SparseEmbeddingModelType,
201};
202#[cfg(feature = "transcription-types")]
203pub use core::config::{TranscriptionConfig, WhisperModel};
204#[cfg(any(feature = "url-ingestion", feature = "url-config-types"))]
205pub use crawlberg::{
206    AssetCategory, AuthConfig, BrowserBackend, BrowserConfig, BrowserMode, BrowserWait, ContentConfig, CrawlConfig,
207    ProxyConfig, SsrfPolicy,
208};
209#[cfg(feature = "url-ingestion")]
210pub use crawlberg::{MapResult, SitemapUrl};
211pub use extractors::security::SecurityLimits;
212
213#[cfg(feature = "presets")]
214pub use presets::{
215    LoadError, MetaSchema, Preset, PresetCategory, PresetSample, PresetSummary, Registry, ResolveError, ResolvedPreset,
216    resolve,
217};
218
219#[cfg(feature = "quality")]
220pub use text::{ReductionLevel, TokenReductionConfig};
221
222#[cfg(all(
223    feature = "ner-llm",
224    not(target_arch = "wasm32"),
225    not(all(target_os = "android", target_arch = "x86_64"))
226))]
227#[cfg_attr(alef, alef(skip))]
228pub use text::ner::llm::LlmBackend;
229
230#[cfg(feature = "ner-llm")]
231pub use text::ner::NerBackend;
232
233#[cfg(any(not(feature = "ner-llm"), all(target_os = "android", target_arch = "x86_64")))]
234#[derive(Clone, Debug)]
235#[cfg_attr(alef, alef(skip))]
236pub struct LlmBackend {
237    _config: LlmConfig,
238}
239
240#[cfg(any(not(feature = "ner-llm"), all(target_os = "android", target_arch = "x86_64")))]
241impl LlmBackend {
242    pub fn new(config: LlmConfig) -> Self {
243        Self { _config: config }
244    }
245
246    pub async fn detect(&self, _text: &str, _categories: &[crate::EntityCategory]) -> Result<Vec<crate::Entity>> {
247        Err(crate::XbergError::Other(
248            "ner-llm feature not available on this target".into(),
249        ))
250    }
251
252    pub async fn detect_with_custom(
253        &self,
254        _text: &str,
255        _categories: &[crate::EntityCategory],
256        _custom_labels: &[String],
257    ) -> Result<Vec<crate::Entity>> {
258        Err(crate::XbergError::Other(
259            "ner-llm feature not available on this target".into(),
260        ))
261    }
262}
263
264#[cfg(feature = "ner-onnx")]
265pub use text::ner::gline::GlineBackend;
266
267#[cfg(not(feature = "ner-onnx"))]
268#[derive(Clone, Debug)]
269pub struct GlineBackend {
270    pub repo_id: String,
271    pub model_path: std::path::PathBuf,
272    pub tokenizer_path: std::path::PathBuf,
273}
274
275#[cfg(not(feature = "ner-onnx"))]
276impl GlineBackend {
277    pub fn new(_repo_id: Option<&str>) -> Result<Self> {
278        Err(crate::XbergError::Other(
279            "ner-onnx feature not available on this target".into(),
280        ))
281    }
282
283    pub async fn detect(&self, _text: &str, _categories: &[crate::EntityCategory]) -> Result<Vec<crate::Entity>> {
284        Err(crate::XbergError::Other(
285            "ner-onnx feature not available on this target".into(),
286        ))
287    }
288
289    pub async fn detect_with_custom(
290        &self,
291        _text: &str,
292        _categories: &[crate::EntityCategory],
293        _custom_labels: &[String],
294    ) -> Result<Vec<crate::Entity>> {
295        Err(crate::XbergError::Other(
296            "ner-onnx feature not available on this target".into(),
297        ))
298    }
299}
300
301#[cfg(all(feature = "liter-llm", not(target_arch = "wasm32")))]
302pub use llm::region_extractor::RegionKind;
303
304#[cfg(not(all(feature = "liter-llm", not(target_arch = "wasm32"))))]
305/// Per-region VLM extraction type stub.
306///
307/// Identifies the semantic kind of a document region for VLM-based extraction.
308/// This stub is emitted on targets where the `liter-llm` feature is unavailable
309/// (WASM, Android x86_64 emulator) so that alef-generated bindings compile.
310#[derive(Clone, Copy, Debug, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
311pub enum RegionKind {
312    /// A figure or illustration region.
313    Figure,
314    /// A data-dense table region.
315    DenseTable,
316    /// A region with complex multi-column or mixed layout.
317    ComplexLayout,
318    /// A figure or table caption.
319    Caption,
320}
321
322#[cfg(not(all(feature = "liter-llm", not(target_arch = "wasm32"))))]
323impl RegionKind {
324    /// Returns an empty default prompt string for this stub implementation.
325    pub fn default_prompt(self) -> &'static str {
326        ""
327    }
328}
329
330#[cfg(feature = "ner")]
331#[cfg_attr(alef, alef(skip))]
332pub use text::ner::detect_entities;
333
334#[cfg(feature = "classification")]
335#[cfg_attr(alef, alef(skip))]
336pub use text::classification::classify_document;
337
338#[cfg(feature = "redaction")]
339pub use text::redaction::strategy::TokenCounter;
340
341#[cfg(feature = "api-types")]
342pub use core::server_config::ServerConfig;
343
344#[cfg(feature = "pdf")]
345pub use core::config::{HierarchyConfig, PdfConfig};
346
347#[cfg(feature = "html")]
348pub use core::config::{HtmlOutputConfig, HtmlTheme};
349#[cfg(feature = "html")]
350pub use rendering::StyledHtmlRenderer;
351
352#[cfg(feature = "paddle-ocr-types")]
353pub use paddle_ocr::{ModelPaths, PaddleLanguage, PaddleOcrConfig};
354
355#[cfg(feature = "paddle-ocr")]
356pub use paddle_ocr::{ModelCacheStats, ModelManager, ModelManifestEntry, PaddleOcrBackend};
357
358pub use cache::CacheStats;
359
360#[cfg(feature = "layout-types")]
361pub use core::config::{LayoutDetectionConfig, LayoutStrategy, TableModel};
362
363#[cfg(feature = "layout-types")]
364pub use layout::types::{BBox, DetectionResult, LayoutClass, LayoutDetection};
365
366#[cfg(feature = "layout-types")]
367pub use layout::types::RecognizedTable;
368#[cfg(any(feature = "ocr", feature = "ocr-wasm"))]
369pub use ocr::types::PSMMode;
370
371pub use core::config::{OcrPipelineConfig, OcrPipelineStage, OcrQualityThresholds, OcrStrategy, VlmFallbackPolicy};
372
373#[cfg(feature = "auto-rotate-types")]
374pub use doc_orientation::OrientationResult;
375
376#[cfg(any(feature = "keywords-yake", feature = "keywords-rake"))]
377pub use keywords::{Keyword, KeywordAlgorithm, KeywordConfig};
378
379#[cfg(feature = "keywords-rake")]
380pub use keywords::RakeParams;
381
382#[cfg(feature = "keywords-yake")]
383pub use keywords::YakeParams;
384
385#[cfg(feature = "markdown-footnotes")]
386pub use text::markdown_footnotes::{
387    Citation, FootnoteAnchor, FootnoteConfig, FootnoteDefinition, find_footnote_anchors, find_inference_markers,
388    find_unmarked_claims, parse_citations, parse_footnote_definitions, verify_excerpt,
389};
390
391#[cfg(feature = "diff")]
392pub use diff::{DiffHunk, DiffOptions, EmbeddedChanges, EmbeddedDiff, ExtractionDiff, TableDiff, compare};
393
394#[cfg(feature = "tree-sitter")]
395pub use core::config::{CodeContentMode, TreeSitterConfig, TreeSitterProcessConfig};
396#[cfg(feature = "tree-sitter")]
397pub use tree_sitter_language_pack::{
398    ChunkContext, CodeChunk, CommentInfo, CommentKind, Diagnostic, DiagnosticSeverity, DocstringFormat, DocstringInfo,
399    ExportInfo, ExportKind, FileMetrics, ImportInfo, ProcessConfig, ProcessResult, Span, StructureItem, StructureKind,
400    SymbolInfo, SymbolKind, process as process_code,
401};
402
403pub use core::mime::{SupportedFormat, detect_mime_type_from_bytes, get_extensions_for_mime, list_supported_formats};
404
405/// Detect the MIME type of a file at the given path.
406///
407/// Uses the file extension and optionally the file content to determine the MIME type.
408/// Set `check_exists` to `true` to verify the file exists before detection.
409pub fn detect_mime_type(path: String, check_exists: bool) -> crate::Result<String> {
410    core::mime::detect_mime_type(path, check_exists)
411}
412
413#[cfg(feature = "pdf")]
414pub use pdf::render::{pdf_page_count, render_pdf_page_to_png};
415
416#[cfg_attr(alef, alef(skip))]
417pub use plugins::{
418    clear_document_extractors, clear_embedding_backends, clear_ocr_backends, clear_post_processors, clear_renderers,
419    clear_reranker_backends, clear_tokenizer_backends, clear_validators, list_document_extractors,
420    list_embedding_backends, list_ocr_backends, list_post_processors, list_renderers, list_reranker_backends,
421    list_tokenizer_backends, list_validators, register_document_extractor, register_embedding_backend,
422    register_ocr_backend, register_post_processor, register_renderer, register_reranker_backend,
423    register_tokenizer_backend, register_validator, unregister_document_extractor, unregister_embedding_backend,
424    unregister_ocr_backend, unregister_post_processor, unregister_renderer, unregister_reranker_backend,
425    unregister_tokenizer_backend, unregister_validator,
426};
427
428#[cfg_attr(alef, alef(skip))]
429pub use plugins::{
430    DocumentExtractor, EmbeddingBackend, OcrBackend, OcrBackendType, PostProcessor, ProcessingStage, Renderer,
431    RerankerBackend, TokenizerBackend, Validator,
432};
433
434#[cfg(feature = "embedding-presets")]
435pub use embeddings::EmbeddingPreset;
436
437/// Embed a list of texts using the configured embedding model.
438///
439/// Returns a 2D vector where each inner vector is the embedding for the corresponding text.
440#[cfg(any(feature = "embeddings", feature = "static-embeddings"))]
441#[cfg_attr(alef, alef(skip))]
442pub fn embed_texts(texts: Vec<String>, config: &core::config::EmbeddingConfig) -> crate::Result<Vec<Vec<f32>>> {
443    embeddings::embed_texts(&texts, config)
444}
445
446/// Stub for builds without the `embeddings` or `static-embeddings` feature —
447/// keeps the symbol available so language bindings that mirror the public API
448/// compile; the runtime call returns an unsupported error.
449#[cfg(all(
450    feature = "embedding-presets",
451    not(feature = "embeddings"),
452    not(feature = "static-embeddings")
453))]
454#[cfg_attr(alef, alef(skip))]
455pub fn embed_texts(_texts: Vec<String>, _config: &core::config::EmbeddingConfig) -> crate::Result<Vec<Vec<f32>>> {
456    Err(XbergError::validation(
457        "embed_texts requires the `embeddings` (ONNX Runtime) or `static-embeddings` (pure-Rust) feature; \
458         neither is enabled on this build",
459    ))
460}
461
462#[cfg(all(
463    feature = "tokio-runtime",
464    any(feature = "embeddings", feature = "static-embeddings")
465))]
466#[cfg_attr(alef, alef(skip))]
467pub use embeddings::embed_texts_async;
468
469#[cfg(all(
470    feature = "embedding-presets",
471    not(feature = "embeddings"),
472    not(feature = "static-embeddings"),
473    feature = "tokio-runtime"
474))]
475#[cfg_attr(alef, alef(skip))]
476pub async fn embed_texts_async(
477    _texts: Vec<String>,
478    _config: &core::config::EmbeddingConfig,
479) -> crate::Result<Vec<Vec<f32>>> {
480    Err(XbergError::validation(
481        "embed_texts_async requires the `embeddings` (ONNX Runtime) or `static-embeddings` (pure-Rust) feature; \
482         neither is enabled on this build",
483    ))
484}
485
486/// Get an embedding preset by name.
487///
488/// Returns `None` if no preset with the given name exists. Returns an owned
489/// clone so the value is safe to pass across FFI boundaries.
490#[cfg(feature = "embedding-presets")]
491#[cfg_attr(alef, alef(skip))]
492pub fn get_embedding_preset(name: &str) -> Option<embeddings::EmbeddingPreset> {
493    embeddings::get_preset(name)
494}
495
496/// List the names of all available embedding presets.
497///
498/// Returns owned `String`s so the values are safe to pass across FFI boundaries.
499#[cfg(feature = "embedding-presets")]
500#[cfg_attr(alef, alef(skip))]
501pub fn list_embedding_presets() -> Vec<String> {
502    embeddings::list_presets()
503}
504
505/// Query-side instruction prefix for an embedding config, if its preset defines
506/// one (asymmetric retrieval models such as Arctic-Embed). The RAG query path
507/// prepends this to query text; document text is embedded verbatim. Returns
508/// `None` for symmetric presets, custom models, and non-preset backends.
509#[cfg(feature = "embedding-presets")]
510#[cfg_attr(alef, alef(skip))]
511pub fn embedding_query_prefix(config: &EmbeddingConfig) -> Option<String> {
512    embeddings::embedding_query_prefix(config)
513}
514
515/// Stub preset type for builds without the `embedding-presets` feature.
516///
517/// Field names match the real type so JSON round-trips through
518/// `xberg_embedding_preset_from_json` remain schema-compatible. When the
519/// feature is absent, `get_embedding_preset` always returns `None`, so the
520/// stub is never allocated in practice.
521#[cfg(not(feature = "embedding-presets"))]
522#[cfg_attr(alef, alef(skip))]
523#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
524pub struct EmbeddingPreset {
525    /// Unique preset identifier (e.g. "balanced", "multilingual").
526    pub name: String,
527    /// Maximum input size in Unicode characters for chunking.
528    pub chunk_size: usize,
529    /// Overlap in characters between adjacent chunks.
530    pub overlap: usize,
531    /// HuggingFace repository ID for the model (e.g. "BAAI/bge-small-en-v1.5").
532    pub model_repo: String,
533    /// Pooling strategy used to aggregate token embeddings (e.g. "mean", "cls").
534    pub pooling: String,
535    /// ONNX model file name within the repository.
536    pub model_file: String,
537    /// Number of dimensions in the embedding output vectors.
538    pub dimensions: usize,
539    /// Human-readable description of the preset's intended use case.
540    pub description: String,
541}
542
543/// Returns `None` for builds without the `embedding-presets` feature.
544#[cfg(not(feature = "embedding-presets"))]
545#[cfg_attr(alef, alef(skip))]
546pub fn get_embedding_preset(_name: &str) -> Option<EmbeddingPreset> {
547    None
548}
549
550/// Returns an empty list for builds without the `embedding-presets` feature.
551#[cfg(not(feature = "embedding-presets"))]
552#[cfg_attr(alef, alef(skip))]
553pub fn list_embedding_presets() -> Vec<String> {
554    Vec::new()
555}
556
557/// Re-export `RerankerPreset` when the `reranker-presets` feature is active.
558///
559/// Since v5.0.0.
560#[cfg(feature = "reranker-presets")]
561pub use reranking::RerankerPreset;
562
563/// Re-export `RerankedDocument` — needed for stub signatures and result types.
564///
565/// Since v5.0.0.
566#[cfg(any(feature = "reranker-presets", feature = "reranker"))]
567pub use reranking::RerankedDocument;
568
569/// Rerank a list of documents by relevance to a query.
570///
571/// Returns documents sorted descending by score. Applies `top_k` truncation if
572/// configured.
573///
574/// # Errors
575///
576/// - [`XbergError::Validation`] if `query` is empty or blank.
577/// - [`XbergError::MissingDependency`] if ONNX Runtime is not installed (ONNX path).
578/// - [`XbergError::Reranking`] if the preset is unknown or model download fails.
579///
580/// Since v5.0.0.
581#[cfg(feature = "reranker")]
582#[cfg_attr(alef, alef(skip))]
583pub fn rerank(
584    query: String,
585    documents: Vec<String>,
586    config: &core::config::RerankerConfig,
587) -> crate::Result<Vec<reranking::RerankedDocument>> {
588    reranking::rerank(query, documents, config)
589}
590
591/// Stub for builds without the `reranker` feature — keeps the symbol available
592/// on no-ORT targets (Android x86_64 emulator, WASM) so language bindings compile.
593///
594/// Since v5.0.0.
595#[cfg(all(feature = "reranker-presets", not(feature = "reranker")))]
596#[cfg_attr(alef, alef(skip))]
597pub fn rerank(
598    _query: String,
599    _documents: Vec<String>,
600    _config: &core::config::RerankerConfig,
601) -> crate::Result<Vec<reranking::RerankedDocument>> {
602    Err(XbergError::validation(
603        "rerank requires the `reranker` feature, which depends on ONNX Runtime; \
604         not available on this target (Android x86_64 emulator or WASM)",
605    ))
606}
607
608#[cfg(all(feature = "reranker", feature = "tokio-runtime"))]
609#[cfg_attr(alef, alef(skip))]
610pub use reranking::rerank_async;
611
612/// Stub for builds without the `reranker` feature.
613///
614/// Since v5.0.0.
615#[doc(alias = "rerank")]
616#[cfg(all(feature = "reranker-presets", not(feature = "reranker"), feature = "tokio-runtime"))]
617#[cfg_attr(alef, alef(skip))]
618pub async fn rerank_async(
619    _query: String,
620    _documents: Vec<String>,
621    _config: &core::config::RerankerConfig,
622) -> crate::Result<Vec<reranking::RerankedDocument>> {
623    Err(XbergError::validation(
624        "rerank_async requires the `reranker` feature, which depends on ONNX Runtime; \
625         not available on this target (Android x86_64 emulator or WASM)",
626    ))
627}
628
629/// Get a reranker preset by name.
630///
631/// Returns `None` if no preset with the given name exists. Returns an owned
632/// clone so the value is safe to pass across FFI boundaries.
633///
634/// Since v5.0.0.
635#[cfg(feature = "reranker-presets")]
636#[cfg_attr(alef, alef(skip))]
637pub fn get_reranker_preset(name: &str) -> Option<reranking::RerankerPreset> {
638    reranking::get_preset(name)
639}
640
641/// List the names of all available reranker presets.
642///
643/// Returns owned `String`s so the values are safe to pass across FFI boundaries.
644///
645/// Since v5.0.0.
646#[cfg(feature = "reranker-presets")]
647#[cfg_attr(alef, alef(skip))]
648pub fn list_reranker_presets() -> Vec<String> {
649    reranking::list_presets()
650}
651
652/// Stub preset type for builds without the `reranker-presets` feature.
653///
654/// Field names match the real type so JSON round-trips remain schema-compatible.
655/// When the feature is absent, `get_reranker_preset` always returns `None`, so
656/// the stub is never allocated in practice.
657///
658/// Since v5.0.0.
659#[cfg(not(feature = "reranker-presets"))]
660#[cfg_attr(alef, alef(skip))]
661#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
662pub struct RerankerPreset {
663    /// Unique preset identifier (e.g. "balanced", "multilingual").
664    pub name: String,
665    /// HuggingFace repository ID for the model.
666    pub model_repo: String,
667    /// ONNX model file name within the repository.
668    pub model_file: String,
669    /// Maximum token sequence length the model supports.
670    pub max_length: usize,
671    /// Human-readable description of the preset's intended use case.
672    pub description: String,
673}
674
675/// Stub result document type for builds without `reranker-presets`.
676///
677/// Since v5.0.0.
678#[cfg(not(feature = "reranker-presets"))]
679#[cfg_attr(alef, alef(skip))]
680#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
681#[cfg_attr(feature = "api", derive(utoipa::ToSchema))]
682pub struct RerankedDocument {
683    /// Position of this document in the original input slice.
684    pub index: usize,
685    /// Relevance score in `[0, 1]`.
686    pub score: f32,
687    /// The document text.
688    pub document: String,
689}
690
691/// Returns `None` for builds without the `reranker-presets` feature.
692///
693/// Since v5.0.0.
694#[cfg(not(feature = "reranker-presets"))]
695#[cfg_attr(alef, alef(skip))]
696pub fn get_reranker_preset(_name: &str) -> Option<RerankerPreset> {
697    None
698}
699
700/// Returns an empty list for builds without the `reranker-presets` feature.
701///
702/// Since v5.0.0.
703#[cfg(not(feature = "reranker-presets"))]
704#[cfg_attr(alef, alef(skip))]
705pub fn list_reranker_presets() -> Vec<String> {
706    Vec::new()
707}
708
709/// Re-export the sparse-embedding result and preset types when the presets
710/// feature is active.
711///
712/// Since v5.0.0.
713#[cfg(feature = "sparse-embedding-presets")]
714pub use sparse_embeddings::{SparseEmbedding, SparseEmbeddingPreset};
715
716/// Generate sparse (SPLADE) embeddings for a list of texts.
717///
718/// Returns one [`SparseEmbedding`] per input text, in order.
719///
720/// Since v5.0.0.
721#[cfg(feature = "sparse-embeddings")]
722#[cfg_attr(alef, alef(skip))]
723pub fn embed_sparse(
724    texts: Vec<String>,
725    config: &core::config::SparseEmbeddingConfig,
726) -> crate::Result<Vec<SparseEmbedding>> {
727    sparse_embeddings::embed_sparse(&texts, config)
728}
729
730/// Stub for builds without the `sparse-embeddings` feature — keeps the symbol
731/// available on no-ORT targets so language bindings compile; the runtime call
732/// returns an unsupported error.
733///
734/// Since v5.0.0.
735#[cfg(all(feature = "sparse-embedding-presets", not(feature = "sparse-embeddings")))]
736#[cfg_attr(alef, alef(skip))]
737pub fn embed_sparse(
738    _texts: Vec<String>,
739    _config: &core::config::SparseEmbeddingConfig,
740) -> crate::Result<Vec<SparseEmbedding>> {
741    Err(XbergError::validation(
742        "embed_sparse requires the `sparse-embeddings` feature, which depends on ONNX Runtime; \
743         not available on this target (Android x86_64 emulator or WASM)",
744    ))
745}
746
747#[cfg(all(feature = "sparse-embeddings", feature = "tokio-runtime"))]
748#[cfg_attr(alef, alef(skip))]
749pub use sparse_embeddings::embed_sparse_async;
750
751/// Stub for builds without the `sparse-embeddings` feature.
752///
753/// Since v5.0.0.
754#[cfg(all(
755    feature = "sparse-embedding-presets",
756    not(feature = "sparse-embeddings"),
757    feature = "tokio-runtime"
758))]
759#[cfg_attr(alef, alef(skip))]
760pub async fn embed_sparse_async(
761    _texts: Vec<String>,
762    _config: &core::config::SparseEmbeddingConfig,
763) -> crate::Result<Vec<SparseEmbedding>> {
764    Err(XbergError::validation(
765        "embed_sparse_async requires the `sparse-embeddings` feature, which depends on ONNX Runtime; \
766         not available on this target (Android x86_64 emulator or WASM)",
767    ))
768}
769
770/// Get a sparse-embedding preset by name.
771///
772/// Since v5.0.0.
773#[cfg(feature = "sparse-embedding-presets")]
774#[cfg_attr(alef, alef(skip))]
775pub fn get_sparse_embedding_preset(name: &str) -> Option<sparse_embeddings::SparseEmbeddingPreset> {
776    sparse_embeddings::get_preset(name)
777}
778
779/// List the names of all available sparse-embedding presets.
780///
781/// Since v5.0.0.
782#[cfg(feature = "sparse-embedding-presets")]
783#[cfg_attr(alef, alef(skip))]
784pub fn list_sparse_embedding_presets() -> Vec<String> {
785    sparse_embeddings::list_presets()
786}
787
788/// Stub result type for builds without the `sparse-embedding-presets` feature.
789///
790/// Field names match the real type so JSON round-trips remain schema-compatible.
791///
792/// Since v5.0.0.
793#[cfg(not(feature = "sparse-embedding-presets"))]
794#[cfg_attr(alef, alef(skip))]
795#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
796#[cfg_attr(feature = "api", derive(utoipa::ToSchema))]
797pub struct SparseEmbedding {
798    /// Vocabulary token ids with non-zero weight, ascending.
799    pub indices: Vec<u32>,
800    /// Weights parallel to `indices`.
801    pub values: Vec<f32>,
802}
803
804/// Stub preset type for builds without the `sparse-embedding-presets` feature.
805///
806/// Since v5.0.0.
807#[cfg(not(feature = "sparse-embedding-presets"))]
808#[cfg_attr(alef, alef(skip))]
809#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
810pub struct SparseEmbeddingPreset {
811    /// Unique preset identifier (e.g. "splade").
812    pub name: String,
813    /// HuggingFace repository ID for the model.
814    pub model_repo: String,
815    /// ONNX model file name within the repository.
816    pub model_file: String,
817    /// Sibling files that must be downloaded alongside `model_file`.
818    pub additional_files: Vec<String>,
819    /// Maximum token sequence length the model supports.
820    pub max_length: usize,
821    /// Human-readable description of the preset's intended use case.
822    pub description: String,
823}
824
825/// Returns `None` for builds without the `sparse-embedding-presets` feature.
826///
827/// Since v5.0.0.
828#[cfg(not(feature = "sparse-embedding-presets"))]
829#[cfg_attr(alef, alef(skip))]
830pub fn get_sparse_embedding_preset(_name: &str) -> Option<SparseEmbeddingPreset> {
831    None
832}
833
834/// Returns an empty list for builds without the `sparse-embedding-presets` feature.
835///
836/// Since v5.0.0.
837#[cfg(not(feature = "sparse-embedding-presets"))]
838#[cfg_attr(alef, alef(skip))]
839pub fn list_sparse_embedding_presets() -> Vec<String> {
840    Vec::new()
841}
842
843/// Re-export the multi-vector result/preset types and the pure-CPU MaxSim
844/// primitives when the presets feature is active.
845///
846/// Since v5.0.0.
847#[cfg(feature = "late-interaction-presets")]
848pub use late_interaction::{
849    LateInteractionMatch, LateInteractionPreset, MultiVectorEmbedding, max_sim_rank, max_sim_score,
850};
851
852/// Generate ColBERT multi-vector embeddings for a list of texts.
853///
854/// `is_query` selects `[Q]`/`[D]` marker insertion and, when `true`, query
855/// augmentation padding.
856///
857/// Since v5.0.0.
858#[cfg(feature = "late-interaction")]
859#[cfg_attr(alef, alef(skip))]
860pub fn embed_multi_vector(
861    texts: Vec<String>,
862    config: &core::config::LateInteractionConfig,
863    is_query: bool,
864) -> crate::Result<Vec<MultiVectorEmbedding>> {
865    late_interaction::embed_multi_vector(&texts, config, is_query)
866}
867
868/// Stub for builds without the `late-interaction` feature — keeps the symbol
869/// available on no-ORT targets so language bindings compile.
870///
871/// Since v5.0.0.
872#[cfg(all(feature = "late-interaction-presets", not(feature = "late-interaction")))]
873#[cfg_attr(alef, alef(skip))]
874pub fn embed_multi_vector(
875    _texts: Vec<String>,
876    _config: &core::config::LateInteractionConfig,
877    _is_query: bool,
878) -> crate::Result<Vec<MultiVectorEmbedding>> {
879    Err(XbergError::validation(
880        "embed_multi_vector requires the `late-interaction` feature, which depends on ONNX Runtime; \
881         not available on this target (Android x86_64 emulator or WASM)",
882    ))
883}
884
885#[cfg(all(feature = "late-interaction", feature = "tokio-runtime"))]
886#[cfg_attr(alef, alef(skip))]
887pub use late_interaction::embed_multi_vector_async;
888
889/// Stub for builds without the `late-interaction` feature.
890///
891/// Since v5.0.0.
892#[cfg(all(
893    feature = "late-interaction-presets",
894    not(feature = "late-interaction"),
895    feature = "tokio-runtime"
896))]
897#[cfg_attr(alef, alef(skip))]
898pub async fn embed_multi_vector_async(
899    _texts: Vec<String>,
900    _config: &core::config::LateInteractionConfig,
901    _is_query: bool,
902) -> crate::Result<Vec<MultiVectorEmbedding>> {
903    Err(XbergError::validation(
904        "embed_multi_vector_async requires the `late-interaction` feature, which depends on ONNX Runtime; \
905         not available on this target (Android x86_64 emulator or WASM)",
906    ))
907}
908
909/// Get a late-interaction preset by name.
910///
911/// Since v5.0.0.
912#[cfg(feature = "late-interaction-presets")]
913#[cfg_attr(alef, alef(skip))]
914pub fn get_late_interaction_preset(name: &str) -> Option<late_interaction::LateInteractionPreset> {
915    late_interaction::get_preset(name)
916}
917
918/// List the names of all available late-interaction presets.
919///
920/// Since v5.0.0.
921#[cfg(feature = "late-interaction-presets")]
922#[cfg_attr(alef, alef(skip))]
923pub fn list_late_interaction_presets() -> Vec<String> {
924    late_interaction::list_presets()
925}
926
927/// Stub multi-vector result type for builds without the `late-interaction-presets` feature.
928///
929/// Since v5.0.0.
930#[cfg(not(feature = "late-interaction-presets"))]
931#[cfg_attr(alef, alef(skip))]
932#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
933#[cfg_attr(feature = "api", derive(utoipa::ToSchema))]
934pub struct MultiVectorEmbedding {
935    /// Number of attention-live token rows.
936    pub num_tokens: u32,
937    /// Dimensionality of each per-token vector.
938    pub dim: u32,
939    /// Flat row-major buffer, length `num_tokens * dim`.
940    pub data: Vec<f32>,
941}
942
943/// Stub match type for builds without the `late-interaction-presets` feature.
944///
945/// Since v5.0.0.
946#[cfg(not(feature = "late-interaction-presets"))]
947#[cfg_attr(alef, alef(skip))]
948#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
949#[cfg_attr(feature = "api", derive(utoipa::ToSchema))]
950pub struct LateInteractionMatch {
951    /// Position of this document in the original input slice.
952    pub index: usize,
953    /// MaxSim relevance score.
954    pub score: f32,
955}
956
957/// Stub preset type for builds without the `late-interaction-presets` feature.
958///
959/// Since v5.0.0.
960#[cfg(not(feature = "late-interaction-presets"))]
961#[cfg_attr(alef, alef(skip))]
962#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
963pub struct LateInteractionPreset {
964    /// Unique preset identifier (e.g. "colbert").
965    pub name: String,
966    /// HuggingFace repository ID for the model.
967    pub model_repo: String,
968    /// ONNX model file name within the repository.
969    pub model_file: String,
970    /// Sibling files that must be downloaded alongside `model_file`.
971    pub additional_files: Vec<String>,
972    /// Maximum document token sequence length.
973    pub max_length: usize,
974    /// Fixed padded query length (ColBERT query augmentation).
975    pub query_max_length: usize,
976    /// Per-token embedding dimensionality.
977    pub dim: usize,
978    /// Human-readable description of the preset's intended use case.
979    pub description: String,
980}
981
982/// Returns `None` for builds without the `late-interaction-presets` feature.
983///
984/// Since v5.0.0.
985#[cfg(not(feature = "late-interaction-presets"))]
986#[cfg_attr(alef, alef(skip))]
987pub fn get_late_interaction_preset(_name: &str) -> Option<LateInteractionPreset> {
988    None
989}
990
991/// Returns an empty list for builds without the `late-interaction-presets` feature.
992///
993/// Since v5.0.0.
994#[cfg(not(feature = "late-interaction-presets"))]
995#[cfg_attr(alef, alef(skip))]
996pub fn list_late_interaction_presets() -> Vec<String> {
997    Vec::new()
998}
999
1000/// Caption a single image from bytes using a configured LLM.
1001///
1002/// # Arguments
1003///
1004/// * `image_bytes` - The image data.
1005/// * `llm_config` - LLM configuration for the VLM call.
1006/// * `custom_prompt` - Optional custom caption prompt. Uses the default
1007///   `RegionKind::Caption` prompt when `None`.
1008///
1009/// # Returns
1010///
1011/// The generated caption text.
1012///
1013/// # Errors
1014///
1015/// Returns an error if the VLM call fails or if image format detection fails.
1016///
1017/// # Example
1018///
1019/// ```ignore
1020/// use xberg::captioning::caption_image;
1021/// use xberg::LlmConfig;
1022///
1023/// # async fn example() -> xberg::Result<()> {
1024/// let image_bytes = std::fs::read("photo.jpg")?;
1025/// let config = LlmConfig {
1026///     model: "openai/gpt-4o-mini".to_string(),
1027///     ..Default::default()
1028/// };
1029/// let caption = caption_image(&image_bytes, &config, None).await?;
1030/// println!("Caption: {}", caption);
1031/// # Ok(())
1032/// # }
1033/// ```
1034#[cfg(all(feature = "captioning", feature = "tokio-runtime"))]
1035#[cfg_attr(alef, alef(skip))]
1036pub use captioning::caption_image;
1037
1038/// Caption a single image from a file path using a configured LLM.
1039///
1040/// # Arguments
1041///
1042/// * `path` - Path to the image file.
1043/// * `llm_config` - LLM configuration for the VLM call.
1044/// * `custom_prompt` - Optional custom caption prompt. Uses the default
1045///   `RegionKind::Caption` prompt when `None`.
1046///
1047/// # Returns
1048///
1049/// The generated caption text.
1050///
1051/// # Errors
1052///
1053/// Returns an error if the file cannot be read, if image format detection fails,
1054/// or if the VLM call fails.
1055///
1056/// # Example
1057///
1058/// ```ignore
1059/// use xberg::captioning::caption_image_file;
1060/// use xberg::LlmConfig;
1061///
1062/// # async fn example() -> xberg::Result<()> {
1063/// let config = LlmConfig {
1064///     model: "openai/gpt-4o-mini".to_string(),
1065///     ..Default::default()
1066/// };
1067/// let caption = caption_image_file("document_page_001.png", &config, None).await?;
1068/// # Ok(())
1069/// # }
1070/// ```
1071#[cfg(all(feature = "captioning", feature = "tokio-runtime"))]
1072#[cfg_attr(alef, alef(skip))]
1073pub use captioning::caption_image_file;
1074
1075/// Caption multiple images in a single batch.
1076///
1077/// Processes images sequentially (not in parallel). Returns one caption per input image
1078/// in the same order. If a caption fails, the error is returned immediately without
1079/// processing remaining images.
1080///
1081/// # Arguments
1082///
1083/// * `images` - Slice of image byte references to caption.
1084/// * `llm_config` - LLM configuration for the VLM calls.
1085/// * `custom_prompt` - Optional custom caption prompt. Uses the default
1086///   `RegionKind::Caption` prompt when `None`.
1087///
1088/// # Returns
1089///
1090/// A vector of captions, one per input image, in the same order.
1091///
1092/// # Errors
1093///
1094/// Returns an error if any VLM call fails.
1095///
1096/// # Example
1097///
1098/// ```ignore
1099/// use xberg::captioning::caption_images;
1100/// use xberg::LlmConfig;
1101///
1102/// # async fn example() -> xberg::Result<()> {
1103/// let image1 = std::fs::read("photo1.jpg")?;
1104/// let image2 = std::fs::read("photo2.jpg")?;
1105/// let images = vec![image1.as_ref(), image2.as_ref()];
1106/// let config = LlmConfig {
1107///     model: "openai/gpt-4o-mini".to_string(),
1108///     ..Default::default()
1109/// };
1110/// let captions = caption_images(&images, &config, None).await?;
1111/// assert_eq!(captions.len(), 2);
1112/// # Ok(())
1113/// # }
1114/// ```
1115#[cfg(all(feature = "captioning", feature = "tokio-runtime"))]
1116#[cfg_attr(alef, alef(skip))]
1117pub use captioning::caption_images;
1118
1119/// Unified post-extraction enrichment: classification, NER, captioning, and
1120/// (future) transcription in a single composable call.
1121pub mod enrich;
1122#[cfg_attr(alef, alef(skip))]
1123pub use enrich::enrich;
1124pub use enrich::{EnrichedResult, EnrichmentConfig};
1125
1126#[cfg(feature = "ner")]
1127pub use enrich::NerEnrichmentConfig;
1128
1129#[cfg(feature = "classification")]
1130pub use enrich::ClassificationEnrichmentConfig;
1131
1132#[cfg(feature = "captioning")]
1133pub use enrich::CaptioningEnrichmentConfig;