// This file is auto-generated by alef. DO NOT EDIT.
// alef:hash:4e15143f4af1ae8bafbdb1506ef057da924484c66a19483966333558ad437e75
// Re-generate with: alef generate
#![allow(dead_code, unused_imports, unused_variables)]
#![allow(
    clippy::too_many_arguments,
    clippy::let_unit_value,
    clippy::needless_borrow,
    clippy::map_identity,
    clippy::just_underscores_and_digits,
    clippy::unused_unit,
    clippy::unnecessary_cast,
    clippy::unwrap_or_default,
    clippy::derivable_impls,
    clippy::needless_borrows_for_generic_args,
    clippy::unnecessary_fallible_conversions
)]

use async_trait::async_trait;
use rustler::Encoder;
use rustler::ResourceArc;
use std::collections::HashMap;
use std::sync::Arc;
use std::sync::Mutex;
use std::sync::atomic::{AtomicU64, Ordering};

#[derive(Debug, Clone, Default, serde::Serialize, serde::Deserialize, rustler::NifMap)]
pub struct AccelerationConfig {
    pub provider: ExecutionProviderType,
    pub device_id: u32,
}

impl AccelerationConfig {
    pub fn new(opts: std::collections::HashMap<String, rustler::Term>) -> Self {
        Self {
            provider: opts.get("provider").and_then(|t| t.decode().ok()).unwrap_or_default(),
            device_id: opts.get("device_id").and_then(|t| t.decode().ok()).unwrap_or_default(),
        }
    }
}

#[derive(Debug, Clone, Default, serde::Serialize, serde::Deserialize, rustler::NifMap)]
pub struct ContentFilterConfig {
    pub include_headers: bool,
    pub include_footers: bool,
    pub strip_repeating_text: bool,
    pub include_watermarks: bool,
}

impl ContentFilterConfig {
    pub fn new(opts: std::collections::HashMap<String, rustler::Term>) -> Self {
        Self {
            include_headers: opts
                .get("include_headers")
                .and_then(|t| t.decode().ok())
                .unwrap_or(false),
            include_footers: opts
                .get("include_footers")
                .and_then(|t| t.decode().ok())
                .unwrap_or(false),
            strip_repeating_text: opts
                .get("strip_repeating_text")
                .and_then(|t| t.decode().ok())
                .unwrap_or(true),
            include_watermarks: opts
                .get("include_watermarks")
                .and_then(|t| t.decode().ok())
                .unwrap_or(false),
        }
    }
}

#[derive(Debug, Clone, Default, serde::Serialize, serde::Deserialize, rustler::NifMap)]
pub struct EmailConfig {
    pub msg_fallback_codepage: Option<u32>,
}

impl EmailConfig {
    pub fn new(opts: std::collections::HashMap<String, rustler::Term>) -> Self {
        Self {
            msg_fallback_codepage: opts.get("msg_fallback_codepage").and_then(|t| t.decode().ok()),
        }
    }
}

#[derive(Debug, Clone, Default, serde::Serialize, serde::Deserialize, rustler::NifMap)]
pub struct ExtractionConfig {
    pub use_cache: bool,
    pub enable_quality_processing: bool,
    pub ocr: Option<OcrConfig>,
    pub force_ocr: bool,
    pub force_ocr_pages: Option<Vec<u32>>,
    pub disable_ocr: bool,
    pub chunking: Option<ChunkingConfig>,
    pub content_filter: Option<ContentFilterConfig>,
    pub images: Option<ImageExtractionConfig>,
    pub pdf_options: Option<PdfConfig>,
    pub token_reduction: Option<TokenReductionOptions>,
    pub language_detection: Option<LanguageDetectionConfig>,
    pub pages: Option<PageConfig>,
    pub keywords: Option<KeywordConfig>,
    pub postprocessor: Option<PostProcessorConfig>,
    pub html_options: Option<String>,
    pub html_output: Option<HtmlOutputConfig>,
    pub extraction_timeout_secs: Option<u64>,
    pub max_concurrent_extractions: Option<usize>,
    pub result_format: ResultFormat,
    pub security_limits: Option<SecurityLimits>,
    pub max_embedded_file_bytes: Option<u64>,
    pub output_format: OutputFormat,
    pub layout: Option<LayoutDetectionConfig>,
    pub use_layout_for_markdown: bool,
    pub include_document_structure: bool,
    pub acceleration: Option<AccelerationConfig>,
    pub cache_namespace: Option<String>,
    pub cache_ttl_secs: Option<u64>,
    pub email: Option<EmailConfig>,
    pub concurrency: Option<String>,
    pub max_archive_depth: usize,
    pub tree_sitter: Option<TreeSitterConfig>,
    pub structured_extraction: Option<StructuredExtractionConfig>,
    pub cancel_token: Option<String>,
}

impl ExtractionConfig {
    pub fn new(opts: std::collections::HashMap<String, rustler::Term>) -> Self {
        Self {
            use_cache: opts.get("use_cache").and_then(|t| t.decode().ok()).unwrap_or(true),
            enable_quality_processing: opts
                .get("enable_quality_processing")
                .and_then(|t| t.decode().ok())
                .unwrap_or(true),
            ocr: opts.get("ocr").and_then(|t| t.decode().ok()),
            force_ocr: opts.get("force_ocr").and_then(|t| t.decode().ok()).unwrap_or(false),
            force_ocr_pages: opts.get("force_ocr_pages").and_then(|t| t.decode().ok()),
            disable_ocr: opts.get("disable_ocr").and_then(|t| t.decode().ok()).unwrap_or(false),
            chunking: opts.get("chunking").and_then(|t| t.decode().ok()),
            content_filter: opts.get("content_filter").and_then(|t| t.decode().ok()),
            images: opts.get("images").and_then(|t| t.decode().ok()),
            pdf_options: opts.get("pdf_options").and_then(|t| t.decode().ok()),
            token_reduction: opts.get("token_reduction").and_then(|t| t.decode().ok()),
            language_detection: opts.get("language_detection").and_then(|t| t.decode().ok()),
            pages: opts.get("pages").and_then(|t| t.decode().ok()),
            keywords: opts.get("keywords").and_then(|t| t.decode().ok()),
            postprocessor: opts.get("postprocessor").and_then(|t| t.decode().ok()),
            html_options: opts.get("html_options").and_then(|t| t.decode().ok()),
            html_output: opts.get("html_output").and_then(|t| t.decode().ok()),
            extraction_timeout_secs: opts.get("extraction_timeout_secs").and_then(|t| t.decode().ok()),
            max_concurrent_extractions: opts.get("max_concurrent_extractions").and_then(|t| t.decode().ok()),
            result_format: opts
                .get("result_format")
                .and_then(|t| t.decode().ok())
                .unwrap_or_default(),
            security_limits: opts.get("security_limits").and_then(|t| t.decode().ok()),
            max_embedded_file_bytes: opts.get("max_embedded_file_bytes").and_then(|t| t.decode().ok()),
            output_format: opts
                .get("output_format")
                .and_then(|t| t.decode().ok())
                .unwrap_or_default(),
            layout: opts.get("layout").and_then(|t| t.decode().ok()),
            use_layout_for_markdown: opts
                .get("use_layout_for_markdown")
                .and_then(|t| t.decode().ok())
                .unwrap_or(false),
            include_document_structure: opts
                .get("include_document_structure")
                .and_then(|t| t.decode().ok())
                .unwrap_or(false),
            acceleration: opts.get("acceleration").and_then(|t| t.decode().ok()),
            cache_namespace: opts.get("cache_namespace").and_then(|t| t.decode().ok()),
            cache_ttl_secs: opts.get("cache_ttl_secs").and_then(|t| t.decode().ok()),
            email: opts.get("email").and_then(|t| t.decode().ok()),
            concurrency: opts.get("concurrency").and_then(|t| t.decode().ok()),
            max_archive_depth: opts
                .get("max_archive_depth")
                .and_then(|t| t.decode().ok())
                .unwrap_or_default(),
            tree_sitter: opts.get("tree_sitter").and_then(|t| t.decode().ok()),
            structured_extraction: opts.get("structured_extraction").and_then(|t| t.decode().ok()),
            cancel_token: opts.get("cancel_token").and_then(|t| t.decode().ok()),
        }
    }
}

#[derive(Debug, Clone, Default, serde::Serialize, serde::Deserialize, rustler::NifMap)]
pub struct FileExtractionConfig {
    pub enable_quality_processing: Option<bool>,
    pub ocr: Option<OcrConfig>,
    pub force_ocr: Option<bool>,
    pub force_ocr_pages: Option<Vec<u32>>,
    pub disable_ocr: Option<bool>,
    pub chunking: Option<ChunkingConfig>,
    pub content_filter: Option<ContentFilterConfig>,
    pub images: Option<ImageExtractionConfig>,
    pub pdf_options: Option<PdfConfig>,
    pub token_reduction: Option<TokenReductionOptions>,
    pub language_detection: Option<LanguageDetectionConfig>,
    pub pages: Option<PageConfig>,
    pub keywords: Option<KeywordConfig>,
    pub postprocessor: Option<PostProcessorConfig>,
    pub html_options: Option<String>,
    pub result_format: Option<ResultFormat>,
    pub output_format: Option<OutputFormat>,
    pub include_document_structure: Option<bool>,
    pub layout: Option<LayoutDetectionConfig>,
    pub timeout_secs: Option<u64>,
    pub tree_sitter: Option<TreeSitterConfig>,
    pub structured_extraction: Option<StructuredExtractionConfig>,
}

impl FileExtractionConfig {
    pub fn new(opts: std::collections::HashMap<String, rustler::Term>) -> Self {
        Self {
            enable_quality_processing: opts.get("enable_quality_processing").and_then(|t| t.decode().ok()),
            ocr: opts.get("ocr").and_then(|t| t.decode().ok()),
            force_ocr: opts.get("force_ocr").and_then(|t| t.decode().ok()),
            force_ocr_pages: opts.get("force_ocr_pages").and_then(|t| t.decode().ok()),
            disable_ocr: opts.get("disable_ocr").and_then(|t| t.decode().ok()),
            chunking: opts.get("chunking").and_then(|t| t.decode().ok()),
            content_filter: opts.get("content_filter").and_then(|t| t.decode().ok()),
            images: opts.get("images").and_then(|t| t.decode().ok()),
            pdf_options: opts.get("pdf_options").and_then(|t| t.decode().ok()),
            token_reduction: opts.get("token_reduction").and_then(|t| t.decode().ok()),
            language_detection: opts.get("language_detection").and_then(|t| t.decode().ok()),
            pages: opts.get("pages").and_then(|t| t.decode().ok()),
            keywords: opts.get("keywords").and_then(|t| t.decode().ok()),
            postprocessor: opts.get("postprocessor").and_then(|t| t.decode().ok()),
            html_options: opts.get("html_options").and_then(|t| t.decode().ok()),
            result_format: opts.get("result_format").and_then(|t| t.decode().ok()),
            output_format: opts.get("output_format").and_then(|t| t.decode().ok()),
            include_document_structure: opts.get("include_document_structure").and_then(|t| t.decode().ok()),
            layout: opts.get("layout").and_then(|t| t.decode().ok()),
            timeout_secs: opts.get("timeout_secs").and_then(|t| t.decode().ok()),
            tree_sitter: opts.get("tree_sitter").and_then(|t| t.decode().ok()),
            structured_extraction: opts.get("structured_extraction").and_then(|t| t.decode().ok()),
        }
    }
}

#[derive(Debug, Clone, Default, serde::Serialize, serde::Deserialize, rustler::NifStruct)]
#[module = "Kreuzberg.BatchBytesItem"]
pub struct BatchBytesItem {
    pub content: Vec<u8>,
    pub mime_type: String,
    pub config: Option<FileExtractionConfig>,
}

#[derive(Debug, Clone, Default, serde::Serialize, serde::Deserialize, rustler::NifStruct)]
#[module = "Kreuzberg.BatchFileItem"]
pub struct BatchFileItem {
    pub path: String,
    pub config: Option<FileExtractionConfig>,
}

#[derive(Debug, Clone, Default, serde::Serialize, serde::Deserialize, rustler::NifMap)]
pub struct ImageExtractionConfig {
    pub extract_images: bool,
    pub target_dpi: i32,
    pub max_image_dimension: i32,
    pub inject_placeholders: bool,
    pub auto_adjust_dpi: bool,
    pub min_dpi: i32,
    pub max_dpi: i32,
    pub max_images_per_page: Option<u32>,
    pub classify: bool,
    pub include_page_rasters: bool,
    pub run_ocr_on_images: bool,
    pub ocr_text_only: bool,
    pub append_ocr_text: bool,
}

impl ImageExtractionConfig {
    pub fn new(opts: std::collections::HashMap<String, rustler::Term>) -> Self {
        Self {
            extract_images: opts.get("extract_images").and_then(|t| t.decode().ok()).unwrap_or(true),
            target_dpi: opts.get("target_dpi").and_then(|t| t.decode().ok()).unwrap_or(300),
            max_image_dimension: opts
                .get("max_image_dimension")
                .and_then(|t| t.decode().ok())
                .unwrap_or(4096),
            inject_placeholders: opts
                .get("inject_placeholders")
                .and_then(|t| t.decode().ok())
                .unwrap_or(true),
            auto_adjust_dpi: opts
                .get("auto_adjust_dpi")
                .and_then(|t| t.decode().ok())
                .unwrap_or(true),
            min_dpi: opts.get("min_dpi").and_then(|t| t.decode().ok()).unwrap_or(72),
            max_dpi: opts.get("max_dpi").and_then(|t| t.decode().ok()).unwrap_or(600),
            max_images_per_page: opts.get("max_images_per_page").and_then(|t| t.decode().ok()),
            classify: opts.get("classify").and_then(|t| t.decode().ok()).unwrap_or(true),
            include_page_rasters: opts
                .get("include_page_rasters")
                .and_then(|t| t.decode().ok())
                .unwrap_or(false),
            run_ocr_on_images: opts
                .get("run_ocr_on_images")
                .and_then(|t| t.decode().ok())
                .unwrap_or(true),
            ocr_text_only: opts.get("ocr_text_only").and_then(|t| t.decode().ok()).unwrap_or(false),
            append_ocr_text: opts
                .get("append_ocr_text")
                .and_then(|t| t.decode().ok())
                .unwrap_or(false),
        }
    }
}

#[derive(Debug, Clone, Default, serde::Serialize, serde::Deserialize, rustler::NifMap)]
pub struct TokenReductionOptions {
    pub mode: String,
    pub preserve_important_words: bool,
}

impl TokenReductionOptions {
    pub fn new(opts: std::collections::HashMap<String, rustler::Term>) -> Self {
        Self {
            mode: opts.get("mode").and_then(|t| t.decode().ok()).unwrap_or_default(),
            preserve_important_words: opts
                .get("preserve_important_words")
                .and_then(|t| t.decode().ok())
                .unwrap_or(true),
        }
    }
}

#[derive(Debug, Clone, Default, serde::Serialize, serde::Deserialize, rustler::NifMap)]
pub struct LanguageDetectionConfig {
    pub enabled: bool,
    pub min_confidence: f64,
    pub detect_multiple: bool,
}

impl LanguageDetectionConfig {
    pub fn new(opts: std::collections::HashMap<String, rustler::Term>) -> Self {
        Self {
            enabled: opts.get("enabled").and_then(|t| t.decode().ok()).unwrap_or(true),
            min_confidence: opts.get("min_confidence").and_then(|t| t.decode().ok()).unwrap_or(0.8),
            detect_multiple: opts
                .get("detect_multiple")
                .and_then(|t| t.decode().ok())
                .unwrap_or(false),
        }
    }
}

#[derive(Debug, Clone, Default, serde::Serialize, serde::Deserialize, rustler::NifMap)]
pub struct HtmlOutputConfig {
    pub css: Option<String>,
    pub css_file: Option<String>,
    pub theme: HtmlTheme,
    pub class_prefix: String,
    pub embed_css: bool,
}

impl HtmlOutputConfig {
    pub fn new(opts: std::collections::HashMap<String, rustler::Term>) -> Self {
        Self {
            css: opts.get("css").and_then(|t| t.decode().ok()),
            css_file: opts.get("css_file").and_then(|t| t.decode().ok()),
            theme: opts.get("theme").and_then(|t| t.decode().ok()).unwrap_or_default(),
            class_prefix: opts
                .get("class_prefix")
                .and_then(|t| t.decode().ok())
                .unwrap_or_default(),
            embed_css: opts.get("embed_css").and_then(|t| t.decode().ok()).unwrap_or(true),
        }
    }
}

#[derive(Debug, Clone, Default, serde::Serialize, serde::Deserialize, rustler::NifMap)]
pub struct LayoutDetectionConfig {
    pub confidence_threshold: Option<f32>,
    pub apply_heuristics: bool,
    pub table_model: TableModel,
    pub acceleration: Option<AccelerationConfig>,
}

impl LayoutDetectionConfig {
    pub fn new(opts: std::collections::HashMap<String, rustler::Term>) -> Self {
        Self {
            confidence_threshold: opts.get("confidence_threshold").and_then(|t| t.decode().ok()),
            apply_heuristics: opts
                .get("apply_heuristics")
                .and_then(|t| t.decode().ok())
                .unwrap_or(true),
            table_model: opts
                .get("table_model")
                .and_then(|t| t.decode().ok())
                .unwrap_or_default(),
            acceleration: opts.get("acceleration").and_then(|t| t.decode().ok()),
        }
    }
}

#[derive(Debug, Clone, Default, serde::Serialize, serde::Deserialize, rustler::NifMap)]
pub struct LlmConfig {
    pub model: String,
    pub api_key: Option<String>,
    pub base_url: Option<String>,
    pub timeout_secs: Option<u64>,
    pub max_retries: Option<u32>,
    pub temperature: Option<f64>,
    pub max_tokens: Option<u64>,
}

impl LlmConfig {
    pub fn new(opts: std::collections::HashMap<String, rustler::Term>) -> Self {
        Self {
            model: opts.get("model").and_then(|t| t.decode().ok()).unwrap_or_default(),
            api_key: opts.get("api_key").and_then(|t| t.decode().ok()),
            base_url: opts.get("base_url").and_then(|t| t.decode().ok()),
            timeout_secs: opts.get("timeout_secs").and_then(|t| t.decode().ok()),
            max_retries: opts.get("max_retries").and_then(|t| t.decode().ok()),
            temperature: opts.get("temperature").and_then(|t| t.decode().ok()),
            max_tokens: opts.get("max_tokens").and_then(|t| t.decode().ok()),
        }
    }
}

#[derive(Debug, Clone, Default, serde::Serialize, serde::Deserialize, rustler::NifStruct)]
#[module = "Kreuzberg.StructuredExtractionConfig"]
pub struct StructuredExtractionConfig {
    pub schema: String,
    pub schema_name: String,
    pub schema_description: Option<String>,
    pub strict: bool,
    pub prompt: Option<String>,
    pub llm: LlmConfig,
}

#[derive(Debug, Clone, Default, serde::Serialize, serde::Deserialize, rustler::NifMap)]
pub struct OcrQualityThresholds {
    pub min_total_non_whitespace: usize,
    pub min_non_whitespace_per_page: f64,
    pub min_meaningful_word_len: usize,
    pub min_meaningful_words: usize,
    pub min_alnum_ratio: f64,
    pub min_garbage_chars: usize,
    pub max_fragmented_word_ratio: f64,
    pub critical_fragmented_word_ratio: f64,
    pub min_avg_word_length: f64,
    pub min_words_for_avg_length_check: usize,
    pub min_consecutive_repeat_ratio: f64,
    pub min_words_for_repeat_check: usize,
    pub substantive_min_chars: usize,
    pub non_text_min_chars: usize,
    pub alnum_ws_ratio_threshold: f64,
    pub pipeline_min_quality: f64,
}

impl OcrQualityThresholds {
    pub fn new(opts: std::collections::HashMap<String, rustler::Term>) -> Self {
        Self {
            min_total_non_whitespace: opts
                .get("min_total_non_whitespace")
                .and_then(|t| t.decode().ok())
                .unwrap_or(64),
            min_non_whitespace_per_page: opts
                .get("min_non_whitespace_per_page")
                .and_then(|t| t.decode().ok())
                .unwrap_or(32.0),
            min_meaningful_word_len: opts
                .get("min_meaningful_word_len")
                .and_then(|t| t.decode().ok())
                .unwrap_or(4),
            min_meaningful_words: opts
                .get("min_meaningful_words")
                .and_then(|t| t.decode().ok())
                .unwrap_or(3),
            min_alnum_ratio: opts.get("min_alnum_ratio").and_then(|t| t.decode().ok()).unwrap_or(0.3),
            min_garbage_chars: opts.get("min_garbage_chars").and_then(|t| t.decode().ok()).unwrap_or(5),
            max_fragmented_word_ratio: opts
                .get("max_fragmented_word_ratio")
                .and_then(|t| t.decode().ok())
                .unwrap_or(0.6),
            critical_fragmented_word_ratio: opts
                .get("critical_fragmented_word_ratio")
                .and_then(|t| t.decode().ok())
                .unwrap_or(0.8),
            min_avg_word_length: opts
                .get("min_avg_word_length")
                .and_then(|t| t.decode().ok())
                .unwrap_or(2.0),
            min_words_for_avg_length_check: opts
                .get("min_words_for_avg_length_check")
                .and_then(|t| t.decode().ok())
                .unwrap_or(50),
            min_consecutive_repeat_ratio: opts
                .get("min_consecutive_repeat_ratio")
                .and_then(|t| t.decode().ok())
                .unwrap_or(0.08),
            min_words_for_repeat_check: opts
                .get("min_words_for_repeat_check")
                .and_then(|t| t.decode().ok())
                .unwrap_or(50),
            substantive_min_chars: opts
                .get("substantive_min_chars")
                .and_then(|t| t.decode().ok())
                .unwrap_or(100),
            non_text_min_chars: opts
                .get("non_text_min_chars")
                .and_then(|t| t.decode().ok())
                .unwrap_or(20),
            alnum_ws_ratio_threshold: opts
                .get("alnum_ws_ratio_threshold")
                .and_then(|t| t.decode().ok())
                .unwrap_or(0.4),
            pipeline_min_quality: opts
                .get("pipeline_min_quality")
                .and_then(|t| t.decode().ok())
                .unwrap_or(0.5),
        }
    }
}

#[derive(Debug, Clone, Default, serde::Serialize, serde::Deserialize, rustler::NifStruct)]
#[module = "Kreuzberg.OcrPipelineStage"]
pub struct OcrPipelineStage {
    pub backend: String,
    pub priority: u32,
    pub language: Option<String>,
    pub tesseract_config: Option<TesseractConfig>,
    pub paddle_ocr_config: Option<String>,
    pub vlm_config: Option<LlmConfig>,
    pub backend_options: Option<String>,
}

#[derive(Debug, Clone, Default, serde::Serialize, serde::Deserialize, rustler::NifStruct)]
#[module = "Kreuzberg.OcrPipelineConfig"]
pub struct OcrPipelineConfig {
    pub stages: Vec<OcrPipelineStage>,
    pub quality_thresholds: OcrQualityThresholds,
}

#[derive(Debug, Clone, Default, serde::Serialize, serde::Deserialize, rustler::NifMap)]
pub struct OcrConfig {
    pub enabled: bool,
    pub backend: String,
    pub language: String,
    pub tesseract_config: Option<TesseractConfig>,
    pub output_format: Option<OutputFormat>,
    pub paddle_ocr_config: Option<String>,
    pub backend_options: Option<String>,
    pub element_config: Option<OcrElementConfig>,
    pub quality_thresholds: Option<OcrQualityThresholds>,
    pub pipeline: Option<OcrPipelineConfig>,
    pub auto_rotate: bool,
    pub vlm_config: Option<LlmConfig>,
    pub vlm_prompt: Option<String>,
    pub acceleration: Option<AccelerationConfig>,
    pub tessdata_bytes: Option<HashMap<String, Vec<u8>>>,
}

impl OcrConfig {
    pub fn new(opts: std::collections::HashMap<String, rustler::Term>) -> Self {
        Self {
            enabled: opts.get("enabled").and_then(|t| t.decode().ok()).unwrap_or(true),
            backend: opts.get("backend").and_then(|t| t.decode().ok()).unwrap_or_default(),
            language: opts.get("language").and_then(|t| t.decode().ok()).unwrap_or_default(),
            tesseract_config: opts.get("tesseract_config").and_then(|t| t.decode().ok()),
            output_format: opts.get("output_format").and_then(|t| t.decode().ok()),
            paddle_ocr_config: opts.get("paddle_ocr_config").and_then(|t| t.decode().ok()),
            backend_options: opts.get("backend_options").and_then(|t| t.decode().ok()),
            element_config: opts.get("element_config").and_then(|t| t.decode().ok()),
            quality_thresholds: opts.get("quality_thresholds").and_then(|t| t.decode().ok()),
            pipeline: opts.get("pipeline").and_then(|t| t.decode().ok()),
            auto_rotate: opts.get("auto_rotate").and_then(|t| t.decode().ok()).unwrap_or(false),
            vlm_config: opts.get("vlm_config").and_then(|t| t.decode().ok()),
            vlm_prompt: opts.get("vlm_prompt").and_then(|t| t.decode().ok()),
            acceleration: opts.get("acceleration").and_then(|t| t.decode().ok()),
            tessdata_bytes: opts.get("tessdata_bytes").and_then(|t| t.decode().ok()),
        }
    }
}

#[derive(Debug, Clone, Default, serde::Serialize, serde::Deserialize, rustler::NifMap)]
pub struct PageConfig {
    pub extract_pages: bool,
    pub insert_page_markers: bool,
    pub marker_format: String,
}

impl PageConfig {
    pub fn new(opts: std::collections::HashMap<String, rustler::Term>) -> Self {
        Self {
            extract_pages: opts.get("extract_pages").and_then(|t| t.decode().ok()).unwrap_or(false),
            insert_page_markers: opts
                .get("insert_page_markers")
                .and_then(|t| t.decode().ok())
                .unwrap_or(false),
            marker_format: opts
                .get("marker_format")
                .and_then(|t| t.decode().ok())
                .unwrap_or_default(),
        }
    }
}

#[derive(Debug, Clone, Default, serde::Serialize, serde::Deserialize, rustler::NifMap)]
pub struct PdfConfig {
    pub extract_images: bool,
    pub extract_tables: bool,
    pub passwords: Option<Vec<String>>,
    pub extract_metadata: bool,
    pub hierarchy: Option<HierarchyConfig>,
    pub extract_annotations: bool,
    pub top_margin_fraction: Option<f32>,
    pub bottom_margin_fraction: Option<f32>,
    pub allow_single_column_tables: bool,
    pub ocr_inline_images: bool,
}

impl PdfConfig {
    pub fn new(opts: std::collections::HashMap<String, rustler::Term>) -> Self {
        Self {
            extract_images: opts
                .get("extract_images")
                .and_then(|t| t.decode().ok())
                .unwrap_or(false),
            extract_tables: opts.get("extract_tables").and_then(|t| t.decode().ok()).unwrap_or(true),
            passwords: opts.get("passwords").and_then(|t| t.decode().ok()),
            extract_metadata: opts
                .get("extract_metadata")
                .and_then(|t| t.decode().ok())
                .unwrap_or(true),
            hierarchy: opts.get("hierarchy").and_then(|t| t.decode().ok()),
            extract_annotations: opts
                .get("extract_annotations")
                .and_then(|t| t.decode().ok())
                .unwrap_or(false),
            top_margin_fraction: opts.get("top_margin_fraction").and_then(|t| t.decode().ok()),
            bottom_margin_fraction: opts.get("bottom_margin_fraction").and_then(|t| t.decode().ok()),
            allow_single_column_tables: opts
                .get("allow_single_column_tables")
                .and_then(|t| t.decode().ok())
                .unwrap_or(false),
            ocr_inline_images: opts
                .get("ocr_inline_images")
                .and_then(|t| t.decode().ok())
                .unwrap_or(false),
        }
    }
}

#[derive(Debug, Clone, Default, serde::Serialize, serde::Deserialize, rustler::NifMap)]
pub struct HierarchyConfig {
    pub enabled: bool,
    pub k_clusters: usize,
    pub include_bbox: bool,
    pub ocr_coverage_threshold: Option<f32>,
}

impl HierarchyConfig {
    pub fn new(opts: std::collections::HashMap<String, rustler::Term>) -> Self {
        Self {
            enabled: opts.get("enabled").and_then(|t| t.decode().ok()).unwrap_or(true),
            k_clusters: opts.get("k_clusters").and_then(|t| t.decode().ok()).unwrap_or(3),
            include_bbox: opts.get("include_bbox").and_then(|t| t.decode().ok()).unwrap_or(true),
            ocr_coverage_threshold: opts.get("ocr_coverage_threshold").and_then(|t| t.decode().ok()),
        }
    }
}

#[derive(Debug, Clone, Default, serde::Serialize, serde::Deserialize, rustler::NifMap)]
pub struct PostProcessorConfig {
    pub enabled: bool,
    pub enabled_processors: Option<Vec<String>>,
    pub disabled_processors: Option<Vec<String>>,
    pub enabled_set: Option<Vec<String>>,
    pub disabled_set: Option<Vec<String>>,
}

impl PostProcessorConfig {
    pub fn new(opts: std::collections::HashMap<String, rustler::Term>) -> Self {
        Self {
            enabled: opts.get("enabled").and_then(|t| t.decode().ok()).unwrap_or(true),
            enabled_processors: opts.get("enabled_processors").and_then(|t| t.decode().ok()),
            disabled_processors: opts.get("disabled_processors").and_then(|t| t.decode().ok()),
            enabled_set: opts.get("enabled_set").and_then(|t| t.decode().ok()),
            disabled_set: opts.get("disabled_set").and_then(|t| t.decode().ok()),
        }
    }
}

#[derive(Debug, Clone, Default, serde::Serialize, serde::Deserialize, rustler::NifMap)]
pub struct ChunkingConfig {
    pub max_characters: usize,
    pub overlap: usize,
    pub trim: bool,
    pub chunker_type: ChunkerType,
    pub embedding: Option<EmbeddingConfig>,
    pub preset: Option<String>,
    pub sizing: ChunkSizing,
    pub prepend_heading_context: bool,
    pub topic_threshold: Option<f32>,
}

impl ChunkingConfig {
    pub fn new(opts: std::collections::HashMap<String, rustler::Term>) -> Self {
        Self {
            max_characters: opts.get("max_characters").and_then(|t| t.decode().ok()).unwrap_or(1000),
            overlap: opts.get("overlap").and_then(|t| t.decode().ok()).unwrap_or(200),
            trim: opts.get("trim").and_then(|t| t.decode().ok()).unwrap_or(true),
            chunker_type: opts
                .get("chunker_type")
                .and_then(|t| t.decode().ok())
                .unwrap_or_default(),
            embedding: opts.get("embedding").and_then(|t| t.decode().ok()),
            preset: opts.get("preset").and_then(|t| t.decode().ok()),
            sizing: opts.get("sizing").and_then(|t| t.decode().ok()).unwrap_or_default(),
            prepend_heading_context: opts
                .get("prepend_heading_context")
                .and_then(|t| t.decode().ok())
                .unwrap_or(false),
            topic_threshold: opts.get("topic_threshold").and_then(|t| t.decode().ok()),
        }
    }
}

#[derive(Debug, Clone, Default, serde::Serialize, serde::Deserialize, rustler::NifMap)]
pub struct EmbeddingConfig {
    pub model: EmbeddingModelType,
    pub normalize: bool,
    pub batch_size: usize,
    pub show_download_progress: bool,
    pub cache_dir: Option<String>,
    pub acceleration: Option<AccelerationConfig>,
    pub max_embed_duration_secs: Option<u64>,
}

impl EmbeddingConfig {
    pub fn new(opts: std::collections::HashMap<String, rustler::Term>) -> Self {
        Self {
            model: opts.get("model").and_then(|t| t.decode().ok()).unwrap_or_default(),
            normalize: opts.get("normalize").and_then(|t| t.decode().ok()).unwrap_or(true),
            batch_size: opts.get("batch_size").and_then(|t| t.decode().ok()).unwrap_or(32),
            show_download_progress: opts
                .get("show_download_progress")
                .and_then(|t| t.decode().ok())
                .unwrap_or(false),
            cache_dir: opts.get("cache_dir").and_then(|t| t.decode().ok()),
            acceleration: opts.get("acceleration").and_then(|t| t.decode().ok()),
            max_embed_duration_secs: opts.get("max_embed_duration_secs").and_then(|t| t.decode().ok()),
        }
    }
}

#[derive(Debug, Clone, Default, serde::Serialize, serde::Deserialize, rustler::NifMap)]
pub struct TreeSitterConfig {
    pub enabled: bool,
    pub cache_dir: Option<String>,
    pub languages: Option<Vec<String>>,
    pub groups: Option<Vec<String>>,
    pub process: TreeSitterProcessConfig,
}

impl TreeSitterConfig {
    pub fn new(opts: std::collections::HashMap<String, rustler::Term>) -> Self {
        Self {
            enabled: opts.get("enabled").and_then(|t| t.decode().ok()).unwrap_or(true),
            cache_dir: opts.get("cache_dir").and_then(|t| t.decode().ok()),
            languages: opts.get("languages").and_then(|t| t.decode().ok()),
            groups: opts.get("groups").and_then(|t| t.decode().ok()),
            process: opts.get("process").and_then(|t| t.decode().ok()).unwrap_or_default(),
        }
    }
}

#[derive(Debug, Clone, Default, serde::Serialize, serde::Deserialize, rustler::NifMap)]
pub struct TreeSitterProcessConfig {
    pub structure: bool,
    pub imports: bool,
    pub exports: bool,
    pub comments: bool,
    pub docstrings: bool,
    pub symbols: bool,
    pub diagnostics: bool,
    pub chunk_max_size: Option<usize>,
    pub content_mode: CodeContentMode,
}

impl TreeSitterProcessConfig {
    pub fn new(opts: std::collections::HashMap<String, rustler::Term>) -> Self {
        Self {
            structure: opts.get("structure").and_then(|t| t.decode().ok()).unwrap_or(true),
            imports: opts.get("imports").and_then(|t| t.decode().ok()).unwrap_or(true),
            exports: opts.get("exports").and_then(|t| t.decode().ok()).unwrap_or(true),
            comments: opts.get("comments").and_then(|t| t.decode().ok()).unwrap_or(false),
            docstrings: opts.get("docstrings").and_then(|t| t.decode().ok()).unwrap_or(false),
            symbols: opts.get("symbols").and_then(|t| t.decode().ok()).unwrap_or(false),
            diagnostics: opts.get("diagnostics").and_then(|t| t.decode().ok()).unwrap_or(false),
            chunk_max_size: opts.get("chunk_max_size").and_then(|t| t.decode().ok()),
            content_mode: opts
                .get("content_mode")
                .and_then(|t| t.decode().ok())
                .unwrap_or_default(),
        }
    }
}

#[derive(Debug, Clone, Default, serde::Serialize, serde::Deserialize, rustler::NifMap)]
pub struct ServerConfig {
    pub host: String,
    pub port: u16,
    pub cors_origins: Vec<String>,
    pub max_request_body_bytes: usize,
    pub max_multipart_field_bytes: usize,
}

impl ServerConfig {
    pub fn new(opts: std::collections::HashMap<String, rustler::Term>) -> Self {
        Self {
            host: opts.get("host").and_then(|t| t.decode().ok()).unwrap_or_default(),
            port: opts.get("port").and_then(|t| t.decode().ok()).unwrap_or_default(),
            cors_origins: opts
                .get("cors_origins")
                .and_then(|t| t.decode().ok())
                .unwrap_or_default(),
            max_request_body_bytes: opts
                .get("max_request_body_bytes")
                .and_then(|t| t.decode().ok())
                .unwrap_or_default(),
            max_multipart_field_bytes: opts
                .get("max_multipart_field_bytes")
                .and_then(|t| t.decode().ok())
                .unwrap_or_default(),
        }
    }
}

#[derive(Debug, Clone, Default, serde::Serialize, serde::Deserialize, rustler::NifMap)]
pub struct DocxAppProperties {
    pub application: Option<String>,
    pub app_version: Option<String>,
    pub template: Option<String>,
    pub total_time: Option<i32>,
    pub pages: Option<i32>,
    pub words: Option<i32>,
    pub characters: Option<i32>,
    pub characters_with_spaces: Option<i32>,
    pub lines: Option<i32>,
    pub paragraphs: Option<i32>,
    pub company: Option<String>,
    pub doc_security: Option<i32>,
    pub scale_crop: Option<bool>,
    pub links_up_to_date: Option<bool>,
    pub shared_doc: Option<bool>,
    pub hyperlinks_changed: Option<bool>,
}

impl DocxAppProperties {
    pub fn new(opts: std::collections::HashMap<String, rustler::Term>) -> Self {
        Self {
            application: opts.get("application").and_then(|t| t.decode().ok()),
            app_version: opts.get("app_version").and_then(|t| t.decode().ok()),
            template: opts.get("template").and_then(|t| t.decode().ok()),
            total_time: opts.get("total_time").and_then(|t| t.decode().ok()),
            pages: opts.get("pages").and_then(|t| t.decode().ok()),
            words: opts.get("words").and_then(|t| t.decode().ok()),
            characters: opts.get("characters").and_then(|t| t.decode().ok()),
            characters_with_spaces: opts.get("characters_with_spaces").and_then(|t| t.decode().ok()),
            lines: opts.get("lines").and_then(|t| t.decode().ok()),
            paragraphs: opts.get("paragraphs").and_then(|t| t.decode().ok()),
            company: opts.get("company").and_then(|t| t.decode().ok()),
            doc_security: opts.get("doc_security").and_then(|t| t.decode().ok()),
            scale_crop: opts.get("scale_crop").and_then(|t| t.decode().ok()),
            links_up_to_date: opts.get("links_up_to_date").and_then(|t| t.decode().ok()),
            shared_doc: opts.get("shared_doc").and_then(|t| t.decode().ok()),
            hyperlinks_changed: opts.get("hyperlinks_changed").and_then(|t| t.decode().ok()),
        }
    }
}

#[derive(Debug, Clone, Default, serde::Serialize, serde::Deserialize, rustler::NifMap)]
pub struct CoreProperties {
    pub title: Option<String>,
    pub subject: Option<String>,
    pub creator: Option<String>,
    pub keywords: Option<String>,
    pub description: Option<String>,
    pub last_modified_by: Option<String>,
    pub revision: Option<String>,
    pub created: Option<String>,
    pub modified: Option<String>,
    pub category: Option<String>,
    pub content_status: Option<String>,
    pub language: Option<String>,
    pub identifier: Option<String>,
    pub version: Option<String>,
    pub last_printed: Option<String>,
}

impl CoreProperties {
    pub fn new(opts: std::collections::HashMap<String, rustler::Term>) -> Self {
        Self {
            title: opts.get("title").and_then(|t| t.decode().ok()),
            subject: opts.get("subject").and_then(|t| t.decode().ok()),
            creator: opts.get("creator").and_then(|t| t.decode().ok()),
            keywords: opts.get("keywords").and_then(|t| t.decode().ok()),
            description: opts.get("description").and_then(|t| t.decode().ok()),
            last_modified_by: opts.get("last_modified_by").and_then(|t| t.decode().ok()),
            revision: opts.get("revision").and_then(|t| t.decode().ok()),
            created: opts.get("created").and_then(|t| t.decode().ok()),
            modified: opts.get("modified").and_then(|t| t.decode().ok()),
            category: opts.get("category").and_then(|t| t.decode().ok()),
            content_status: opts.get("content_status").and_then(|t| t.decode().ok()),
            language: opts.get("language").and_then(|t| t.decode().ok()),
            identifier: opts.get("identifier").and_then(|t| t.decode().ok()),
            version: opts.get("version").and_then(|t| t.decode().ok()),
            last_printed: opts.get("last_printed").and_then(|t| t.decode().ok()),
        }
    }
}

#[derive(Debug, Clone, Default, serde::Serialize, serde::Deserialize, rustler::NifMap)]
pub struct SecurityLimits {
    pub max_archive_size: usize,
    pub max_compression_ratio: usize,
    pub max_files_in_archive: usize,
    pub max_nesting_depth: usize,
    pub max_entity_length: usize,
    pub max_content_size: usize,
    pub max_iterations: usize,
    pub max_xml_depth: usize,
    pub max_table_cells: usize,
}

impl SecurityLimits {
    pub fn new(opts: std::collections::HashMap<String, rustler::Term>) -> Self {
        Self {
            max_archive_size: opts
                .get("max_archive_size")
                .and_then(|t| t.decode().ok())
                .unwrap_or(524288000),
            max_compression_ratio: opts
                .get("max_compression_ratio")
                .and_then(|t| t.decode().ok())
                .unwrap_or(100),
            max_files_in_archive: opts
                .get("max_files_in_archive")
                .and_then(|t| t.decode().ok())
                .unwrap_or(10000),
            max_nesting_depth: opts
                .get("max_nesting_depth")
                .and_then(|t| t.decode().ok())
                .unwrap_or(1024),
            max_entity_length: opts
                .get("max_entity_length")
                .and_then(|t| t.decode().ok())
                .unwrap_or(1048576),
            max_content_size: opts
                .get("max_content_size")
                .and_then(|t| t.decode().ok())
                .unwrap_or(104857600),
            max_iterations: opts
                .get("max_iterations")
                .and_then(|t| t.decode().ok())
                .unwrap_or(10000000),
            max_xml_depth: opts.get("max_xml_depth").and_then(|t| t.decode().ok()).unwrap_or(1024),
            max_table_cells: opts
                .get("max_table_cells")
                .and_then(|t| t.decode().ok())
                .unwrap_or(100000),
        }
    }
}

#[derive(Debug, Clone, Default, serde::Serialize, serde::Deserialize, rustler::NifMap)]
pub struct TokenReductionConfig {
    pub level: ReductionLevel,
    pub language_hint: Option<String>,
    pub preserve_markdown: bool,
    pub preserve_code: bool,
    pub semantic_threshold: f32,
    pub enable_parallel: bool,
    pub use_simd: bool,
    pub custom_stopwords: Option<HashMap<String, Vec<String>>>,
    pub preserve_patterns: Vec<String>,
    pub target_reduction: Option<f32>,
    pub enable_semantic_clustering: bool,
}

impl TokenReductionConfig {
    pub fn new(opts: std::collections::HashMap<String, rustler::Term>) -> Self {
        Self {
            level: opts.get("level").and_then(|t| t.decode().ok()).unwrap_or_default(),
            language_hint: opts.get("language_hint").and_then(|t| t.decode().ok()),
            preserve_markdown: opts
                .get("preserve_markdown")
                .and_then(|t| t.decode().ok())
                .unwrap_or(false),
            preserve_code: opts.get("preserve_code").and_then(|t| t.decode().ok()).unwrap_or(true),
            semantic_threshold: opts
                .get("semantic_threshold")
                .and_then(|t| t.decode().ok())
                .unwrap_or(0.3),
            enable_parallel: opts
                .get("enable_parallel")
                .and_then(|t| t.decode().ok())
                .unwrap_or(true),
            use_simd: opts.get("use_simd").and_then(|t| t.decode().ok()).unwrap_or(true),
            custom_stopwords: opts.get("custom_stopwords").and_then(|t| t.decode().ok()),
            preserve_patterns: opts
                .get("preserve_patterns")
                .and_then(|t| t.decode().ok())
                .unwrap_or_default(),
            target_reduction: opts.get("target_reduction").and_then(|t| t.decode().ok()),
            enable_semantic_clustering: opts
                .get("enable_semantic_clustering")
                .and_then(|t| t.decode().ok())
                .unwrap_or(false),
        }
    }
}

#[derive(Debug, Clone, Default, serde::Serialize, serde::Deserialize, rustler::NifStruct)]
#[module = "Kreuzberg.PdfAnnotation"]
pub struct PdfAnnotation {
    pub annotation_type: PdfAnnotationType,
    pub content: Option<String>,
    pub page_number: u32,
    pub bounding_box: Option<BoundingBox>,
}

#[derive(Debug, Clone, Default, serde::Serialize, serde::Deserialize, rustler::NifStruct)]
#[module = "Kreuzberg.DjotContent"]
pub struct DjotContent {
    pub plain_text: String,
    pub blocks: Vec<FormattedBlock>,
    pub metadata: Metadata,
    pub tables: Vec<Table>,
    pub images: Vec<DjotImage>,
    pub links: Vec<DjotLink>,
    pub footnotes: Vec<Footnote>,
    pub attributes: Vec<String>,
}

#[derive(Debug, Clone, Default, serde::Serialize, serde::Deserialize, rustler::NifStruct)]
#[module = "Kreuzberg.FormattedBlock"]
pub struct FormattedBlock {
    pub block_type: BlockType,
    pub level: Option<usize>,
    pub inline_content: Vec<InlineElement>,
    pub attributes: Option<String>,
    pub language: Option<String>,
    pub code: Option<String>,
    pub children: Vec<FormattedBlock>,
}

#[derive(Debug, Clone, Default, serde::Serialize, serde::Deserialize, rustler::NifStruct)]
#[module = "Kreuzberg.InlineElement"]
pub struct InlineElement {
    pub element_type: InlineType,
    pub content: String,
    pub attributes: Option<String>,
    pub metadata: Option<HashMap<String, String>>,
}

#[derive(Debug, Clone, Default, serde::Serialize, serde::Deserialize, rustler::NifStruct)]
#[module = "Kreuzberg.DjotImage"]
pub struct DjotImage {
    pub src: String,
    pub alt: String,
    pub title: Option<String>,
    pub attributes: Option<String>,
}

#[derive(Debug, Clone, Default, serde::Serialize, serde::Deserialize, rustler::NifStruct)]
#[module = "Kreuzberg.DjotLink"]
pub struct DjotLink {
    pub url: String,
    pub text: String,
    pub title: Option<String>,
    pub attributes: Option<String>,
}

#[derive(Debug, Clone, Default, serde::Serialize, serde::Deserialize, rustler::NifStruct)]
#[module = "Kreuzberg.Footnote"]
pub struct Footnote {
    pub label: String,
    pub content: Vec<FormattedBlock>,
}

#[derive(Debug, Clone, Default, serde::Serialize, serde::Deserialize, rustler::NifMap)]
pub struct DocumentStructure {
    pub nodes: Vec<DocumentNode>,
    pub source_format: Option<String>,
    pub relationships: Vec<DocumentRelationship>,
    pub node_types: Vec<String>,
}

impl DocumentStructure {
    pub fn new(opts: std::collections::HashMap<String, rustler::Term>) -> Self {
        Self {
            nodes: opts.get("nodes").and_then(|t| t.decode().ok()).unwrap_or_default(),
            source_format: opts.get("source_format").and_then(|t| t.decode().ok()),
            relationships: opts
                .get("relationships")
                .and_then(|t| t.decode().ok())
                .unwrap_or_default(),
            node_types: opts.get("node_types").and_then(|t| t.decode().ok()).unwrap_or_default(),
        }
    }
}

#[derive(Debug, Clone, Default, serde::Serialize, serde::Deserialize, rustler::NifStruct)]
#[module = "Kreuzberg.DocumentRelationship"]
pub struct DocumentRelationship {
    pub source: u32,
    pub target: u32,
    pub kind: RelationshipKind,
}

#[derive(Debug, Clone, Default, serde::Serialize, serde::Deserialize, rustler::NifStruct)]
#[module = "Kreuzberg.DocumentNode"]
pub struct DocumentNode {
    pub id: String,
    pub content: NodeContent,
    pub parent: Option<u32>,
    pub children: Vec<u32>,
    pub content_layer: ContentLayer,
    pub page: Option<u32>,
    pub page_end: Option<u32>,
    pub bbox: Option<BoundingBox>,
    pub annotations: Vec<TextAnnotation>,
    pub attributes: Option<HashMap<String, String>>,
}

#[derive(Debug, Clone, Default, serde::Serialize, serde::Deserialize, rustler::NifMap)]
pub struct TableGrid {
    pub rows: u32,
    pub cols: u32,
    pub cells: Vec<GridCell>,
}

impl TableGrid {
    pub fn new(opts: std::collections::HashMap<String, rustler::Term>) -> Self {
        Self {
            rows: opts.get("rows").and_then(|t| t.decode().ok()).unwrap_or_default(),
            cols: opts.get("cols").and_then(|t| t.decode().ok()).unwrap_or_default(),
            cells: opts.get("cells").and_then(|t| t.decode().ok()).unwrap_or_default(),
        }
    }
}

#[derive(Debug, Clone, Default, serde::Serialize, serde::Deserialize, rustler::NifStruct)]
#[module = "Kreuzberg.GridCell"]
pub struct GridCell {
    pub content: String,
    pub row: u32,
    pub col: u32,
    pub row_span: u32,
    pub col_span: u32,
    pub is_header: bool,
    pub bbox: Option<BoundingBox>,
}

#[derive(Debug, Clone, Default, serde::Serialize, serde::Deserialize, rustler::NifStruct)]
#[module = "Kreuzberg.TextAnnotation"]
pub struct TextAnnotation {
    pub start: u32,
    pub end: u32,
    pub kind: AnnotationKind,
}

#[derive(Debug, Clone, Default, serde::Serialize, serde::Deserialize, rustler::NifMap)]
pub struct ExtractionResult {
    pub content: String,
    pub mime_type: String,
    pub metadata: Metadata,
    pub extraction_method: Option<ExtractionMethod>,
    pub tables: Vec<Table>,
    pub detected_languages: Option<Vec<String>>,
    pub chunks: Option<Vec<Chunk>>,
    pub images: Option<Vec<ExtractedImage>>,
    pub pages: Option<Vec<PageContent>>,
    pub elements: Option<Vec<Element>>,
    pub djot_content: Option<DjotContent>,
    pub ocr_elements: Option<Vec<OcrElement>>,
    pub document: Option<DocumentStructure>,
    pub extracted_keywords: Option<Vec<Keyword>>,
    pub quality_score: Option<f64>,
    pub processing_warnings: Vec<ProcessingWarning>,
    pub annotations: Option<Vec<PdfAnnotation>>,
    pub children: Option<Vec<ArchiveEntry>>,
    pub uris: Option<Vec<ExtractedUri>>,
    pub revisions: Option<Vec<DocumentRevision>>,
    pub structured_output: Option<String>,
    pub code_intelligence: Option<String>,
    pub llm_usage: Option<Vec<LlmUsage>>,
    pub formatted_content: Option<String>,
    pub ocr_internal_document: Option<String>,
}

impl ExtractionResult {
    pub fn new(opts: std::collections::HashMap<String, rustler::Term>) -> Self {
        Self {
            content: opts.get("content").and_then(|t| t.decode().ok()).unwrap_or_default(),
            mime_type: opts.get("mime_type").and_then(|t| t.decode().ok()).unwrap_or_default(),
            metadata: opts.get("metadata").and_then(|t| t.decode().ok()).unwrap_or_default(),
            extraction_method: opts.get("extraction_method").and_then(|t| t.decode().ok()),
            tables: opts.get("tables").and_then(|t| t.decode().ok()).unwrap_or_default(),
            detected_languages: opts.get("detected_languages").and_then(|t| t.decode().ok()),
            chunks: opts.get("chunks").and_then(|t| t.decode().ok()),
            images: opts.get("images").and_then(|t| t.decode().ok()),
            pages: opts.get("pages").and_then(|t| t.decode().ok()),
            elements: opts.get("elements").and_then(|t| t.decode().ok()),
            djot_content: opts.get("djot_content").and_then(|t| t.decode().ok()),
            ocr_elements: opts.get("ocr_elements").and_then(|t| t.decode().ok()),
            document: opts.get("document").and_then(|t| t.decode().ok()),
            extracted_keywords: opts.get("extracted_keywords").and_then(|t| t.decode().ok()),
            quality_score: opts.get("quality_score").and_then(|t| t.decode().ok()),
            processing_warnings: opts
                .get("processing_warnings")
                .and_then(|t| t.decode().ok())
                .unwrap_or_default(),
            annotations: opts.get("annotations").and_then(|t| t.decode().ok()),
            children: opts.get("children").and_then(|t| t.decode().ok()),
            uris: opts.get("uris").and_then(|t| t.decode().ok()),
            revisions: opts.get("revisions").and_then(|t| t.decode().ok()),
            structured_output: opts.get("structured_output").and_then(|t| t.decode().ok()),
            code_intelligence: opts.get("code_intelligence").and_then(|t| t.decode().ok()),
            llm_usage: opts.get("llm_usage").and_then(|t| t.decode().ok()),
            formatted_content: opts.get("formatted_content").and_then(|t| t.decode().ok()),
            ocr_internal_document: opts.get("ocr_internal_document").and_then(|t| t.decode().ok()),
        }
    }
}

#[derive(Debug, Clone, Default, serde::Serialize, serde::Deserialize, rustler::NifStruct)]
#[module = "Kreuzberg.ArchiveEntry"]
pub struct ArchiveEntry {
    pub path: String,
    pub mime_type: String,
    pub result: ExtractionResult,
}

#[derive(Debug, Clone, Default, serde::Serialize, serde::Deserialize, rustler::NifStruct)]
#[module = "Kreuzberg.ProcessingWarning"]
pub struct ProcessingWarning {
    pub source: String,
    pub message: String,
}

#[derive(Debug, Clone, Default, serde::Serialize, serde::Deserialize, rustler::NifMap)]
pub struct LlmUsage {
    pub model: String,
    pub source: String,
    pub input_tokens: Option<u64>,
    pub output_tokens: Option<u64>,
    pub total_tokens: Option<u64>,
    pub estimated_cost: Option<f64>,
    pub finish_reason: Option<String>,
}

impl LlmUsage {
    pub fn new(opts: std::collections::HashMap<String, rustler::Term>) -> Self {
        Self {
            model: opts.get("model").and_then(|t| t.decode().ok()).unwrap_or_default(),
            source: opts.get("source").and_then(|t| t.decode().ok()).unwrap_or_default(),
            input_tokens: opts.get("input_tokens").and_then(|t| t.decode().ok()),
            output_tokens: opts.get("output_tokens").and_then(|t| t.decode().ok()),
            total_tokens: opts.get("total_tokens").and_then(|t| t.decode().ok()),
            estimated_cost: opts.get("estimated_cost").and_then(|t| t.decode().ok()),
            finish_reason: opts.get("finish_reason").and_then(|t| t.decode().ok()),
        }
    }
}

#[derive(Debug, Clone, Default, serde::Serialize, serde::Deserialize, rustler::NifStruct)]
#[module = "Kreuzberg.Chunk"]
pub struct Chunk {
    pub content: String,
    pub chunk_type: ChunkType,
    pub embedding: Option<Vec<f32>>,
    pub metadata: ChunkMetadata,
}

#[derive(Debug, Clone, Default, serde::Serialize, serde::Deserialize, rustler::NifStruct)]
#[module = "Kreuzberg.HeadingContext"]
pub struct HeadingContext {
    pub headings: Vec<HeadingLevel>,
}

#[derive(Debug, Clone, Default, serde::Serialize, serde::Deserialize, rustler::NifStruct)]
#[module = "Kreuzberg.HeadingLevel"]
pub struct HeadingLevel {
    pub level: u8,
    pub text: String,
}

#[derive(Debug, Clone, Default, serde::Serialize, serde::Deserialize, rustler::NifStruct)]
#[module = "Kreuzberg.ChunkMetadata"]
pub struct ChunkMetadata {
    pub byte_start: usize,
    pub byte_end: usize,
    pub token_count: Option<usize>,
    pub chunk_index: usize,
    pub total_chunks: usize,
    pub first_page: Option<u32>,
    pub last_page: Option<u32>,
    pub heading_context: Option<HeadingContext>,
    pub image_indices: Vec<u32>,
}

#[derive(Debug, Clone, Default, serde::Serialize, serde::Deserialize, rustler::NifStruct)]
#[module = "Kreuzberg.ExtractedImage"]
pub struct ExtractedImage {
    pub data: Vec<u8>,
    pub format: String,
    pub image_index: u32,
    pub page_number: Option<u32>,
    pub width: Option<u32>,
    pub height: Option<u32>,
    pub colorspace: Option<String>,
    pub bits_per_component: Option<u32>,
    pub is_mask: bool,
    pub description: Option<String>,
    pub ocr_result: Option<ExtractionResult>,
    pub bounding_box: Option<BoundingBox>,
    pub source_path: Option<String>,
    pub image_kind: Option<ImageKind>,
    pub kind_confidence: Option<f32>,
    pub cluster_id: Option<u32>,
}

#[derive(Debug, Clone, Default, serde::Serialize, serde::Deserialize, rustler::NifMap)]
pub struct BoundingBox {
    pub x0: f64,
    pub y0: f64,
    pub x1: f64,
    pub y1: f64,
}

impl BoundingBox {
    pub fn new(opts: std::collections::HashMap<String, rustler::Term>) -> Self {
        Self {
            x0: opts.get("x0").and_then(|t| t.decode().ok()).unwrap_or_default(),
            y0: opts.get("y0").and_then(|t| t.decode().ok()).unwrap_or_default(),
            x1: opts.get("x1").and_then(|t| t.decode().ok()).unwrap_or_default(),
            y1: opts.get("y1").and_then(|t| t.decode().ok()).unwrap_or_default(),
        }
    }
}

#[derive(Debug, Clone, Default, serde::Serialize, serde::Deserialize, rustler::NifStruct)]
#[module = "Kreuzberg.ElementMetadata"]
pub struct ElementMetadata {
    pub page_number: Option<u32>,
    pub filename: Option<String>,
    pub coordinates: Option<BoundingBox>,
    pub element_index: Option<usize>,
    pub additional: HashMap<String, String>,
}

#[derive(Debug, Clone, Default, serde::Serialize, serde::Deserialize, rustler::NifStruct)]
#[module = "Kreuzberg.Element"]
pub struct Element {
    pub element_id: String,
    pub element_type: ElementType,
    pub text: String,
    pub metadata: ElementMetadata,
}

#[derive(Debug, Clone, Default, serde::Serialize, serde::Deserialize, rustler::NifStruct)]
#[module = "Kreuzberg.OcrExtractionResult"]
pub struct OcrExtractionResult {
    pub content: String,
    pub mime_type: String,
    pub metadata: HashMap<String, String>,
    pub tables: Vec<OcrTable>,
    pub ocr_elements: Option<Vec<OcrElement>>,
    pub internal_document: Option<String>,
}

#[derive(Debug, Clone, Default, serde::Serialize, serde::Deserialize, rustler::NifStruct)]
#[module = "Kreuzberg.OcrTable"]
pub struct OcrTable {
    pub cells: Vec<Vec<String>>,
    pub markdown: String,
    pub page_number: u32,
    pub bounding_box: Option<OcrTableBoundingBox>,
}

#[derive(Debug, Clone, Default, serde::Serialize, serde::Deserialize, rustler::NifStruct)]
#[module = "Kreuzberg.OcrTableBoundingBox"]
pub struct OcrTableBoundingBox {
    pub left: u32,
    pub top: u32,
    pub right: u32,
    pub bottom: u32,
}

#[derive(Debug, Clone, Default, serde::Serialize, serde::Deserialize, rustler::NifMap)]
pub struct ImagePreprocessingConfig {
    pub target_dpi: i32,
    pub auto_rotate: bool,
    pub deskew: bool,
    pub denoise: bool,
    pub contrast_enhance: bool,
    pub binarization_method: String,
    pub invert_colors: bool,
}

impl ImagePreprocessingConfig {
    pub fn new(opts: std::collections::HashMap<String, rustler::Term>) -> Self {
        Self {
            target_dpi: opts.get("target_dpi").and_then(|t| t.decode().ok()).unwrap_or(300),
            auto_rotate: opts.get("auto_rotate").and_then(|t| t.decode().ok()).unwrap_or(true),
            deskew: opts.get("deskew").and_then(|t| t.decode().ok()).unwrap_or(true),
            denoise: opts.get("denoise").and_then(|t| t.decode().ok()).unwrap_or(false),
            contrast_enhance: opts
                .get("contrast_enhance")
                .and_then(|t| t.decode().ok())
                .unwrap_or(false),
            binarization_method: opts
                .get("binarization_method")
                .and_then(|t| t.decode().ok())
                .unwrap_or_default(),
            invert_colors: opts.get("invert_colors").and_then(|t| t.decode().ok()).unwrap_or(false),
        }
    }
}

#[derive(Debug, Clone, Default, serde::Serialize, serde::Deserialize, rustler::NifMap)]
pub struct TesseractConfig {
    pub language: String,
    pub psm: i32,
    pub output_format: String,
    pub oem: i32,
    pub min_confidence: f64,
    pub preprocessing: Option<ImagePreprocessingConfig>,
    pub enable_table_detection: bool,
    pub table_min_confidence: f64,
    pub table_column_threshold: i32,
    pub table_row_threshold_ratio: f64,
    pub use_cache: bool,
    pub classify_use_pre_adapted_templates: bool,
    pub language_model_ngram_on: bool,
    pub tessedit_dont_blkrej_good_wds: bool,
    pub tessedit_dont_rowrej_good_wds: bool,
    pub tessedit_enable_dict_correction: bool,
    pub tessedit_char_whitelist: String,
    pub tessedit_char_blacklist: String,
    pub tessedit_use_primary_params_model: bool,
    pub textord_space_size_is_variable: bool,
    pub thresholding_method: bool,
}

impl TesseractConfig {
    pub fn new(opts: std::collections::HashMap<String, rustler::Term>) -> Self {
        Self {
            language: opts.get("language").and_then(|t| t.decode().ok()).unwrap_or_default(),
            psm: opts.get("psm").and_then(|t| t.decode().ok()).unwrap_or(3),
            output_format: opts
                .get("output_format")
                .and_then(|t| t.decode().ok())
                .unwrap_or_default(),
            oem: opts.get("oem").and_then(|t| t.decode().ok()).unwrap_or(3),
            min_confidence: opts.get("min_confidence").and_then(|t| t.decode().ok()).unwrap_or(0.0),
            preprocessing: opts.get("preprocessing").and_then(|t| t.decode().ok()),
            enable_table_detection: opts
                .get("enable_table_detection")
                .and_then(|t| t.decode().ok())
                .unwrap_or(true),
            table_min_confidence: opts
                .get("table_min_confidence")
                .and_then(|t| t.decode().ok())
                .unwrap_or(0.0),
            table_column_threshold: opts
                .get("table_column_threshold")
                .and_then(|t| t.decode().ok())
                .unwrap_or(50),
            table_row_threshold_ratio: opts
                .get("table_row_threshold_ratio")
                .and_then(|t| t.decode().ok())
                .unwrap_or(0.5),
            use_cache: opts.get("use_cache").and_then(|t| t.decode().ok()).unwrap_or(true),
            classify_use_pre_adapted_templates: opts
                .get("classify_use_pre_adapted_templates")
                .and_then(|t| t.decode().ok())
                .unwrap_or(true),
            language_model_ngram_on: opts
                .get("language_model_ngram_on")
                .and_then(|t| t.decode().ok())
                .unwrap_or(false),
            tessedit_dont_blkrej_good_wds: opts
                .get("tessedit_dont_blkrej_good_wds")
                .and_then(|t| t.decode().ok())
                .unwrap_or(true),
            tessedit_dont_rowrej_good_wds: opts
                .get("tessedit_dont_rowrej_good_wds")
                .and_then(|t| t.decode().ok())
                .unwrap_or(true),
            tessedit_enable_dict_correction: opts
                .get("tessedit_enable_dict_correction")
                .and_then(|t| t.decode().ok())
                .unwrap_or(true),
            tessedit_char_whitelist: opts
                .get("tessedit_char_whitelist")
                .and_then(|t| t.decode().ok())
                .unwrap_or_default(),
            tessedit_char_blacklist: opts
                .get("tessedit_char_blacklist")
                .and_then(|t| t.decode().ok())
                .unwrap_or_default(),
            tessedit_use_primary_params_model: opts
                .get("tessedit_use_primary_params_model")
                .and_then(|t| t.decode().ok())
                .unwrap_or(true),
            textord_space_size_is_variable: opts
                .get("textord_space_size_is_variable")
                .and_then(|t| t.decode().ok())
                .unwrap_or(true),
            thresholding_method: opts
                .get("thresholding_method")
                .and_then(|t| t.decode().ok())
                .unwrap_or(false),
        }
    }
}

#[derive(Debug, Clone, Default, serde::Serialize, serde::Deserialize, rustler::NifStruct)]
#[module = "Kreuzberg.ImagePreprocessingMetadata"]
pub struct ImagePreprocessingMetadata {
    pub original_dimensions: Vec<usize>,
    pub original_dpi: Vec<f64>,
    pub target_dpi: i32,
    pub scale_factor: f64,
    pub auto_adjusted: bool,
    pub final_dpi: i32,
    pub new_dimensions: Option<Vec<usize>>,
    pub resample_method: String,
    pub dimension_clamped: bool,
    pub calculated_dpi: Option<i32>,
    pub skipped_resize: bool,
    pub resize_error: Option<String>,
}

#[derive(Debug, Clone, Default, serde::Serialize, serde::Deserialize, rustler::NifMap)]
pub struct Metadata {
    pub title: Option<String>,
    pub subject: Option<String>,
    pub authors: Option<Vec<String>>,
    pub keywords: Option<Vec<String>>,
    pub language: Option<String>,
    pub created_at: Option<String>,
    pub modified_at: Option<String>,
    pub created_by: Option<String>,
    pub modified_by: Option<String>,
    pub pages: Option<PageStructure>,
    pub format: Option<FormatMetadata>,
    pub image_preprocessing: Option<ImagePreprocessingMetadata>,
    pub json_schema: Option<String>,
    pub error: Option<ErrorMetadata>,
    pub extraction_duration_ms: Option<u64>,
    pub category: Option<String>,
    pub tags: Option<Vec<String>>,
    pub document_version: Option<String>,
    pub abstract_text: Option<String>,
    pub output_format: Option<String>,
    pub ocr_used: bool,
    pub additional: HashMap<String, String>,
}

impl Metadata {
    pub fn new(opts: std::collections::HashMap<String, rustler::Term>) -> Self {
        Self {
            title: opts.get("title").and_then(|t| t.decode().ok()),
            subject: opts.get("subject").and_then(|t| t.decode().ok()),
            authors: opts.get("authors").and_then(|t| t.decode().ok()),
            keywords: opts.get("keywords").and_then(|t| t.decode().ok()),
            language: opts.get("language").and_then(|t| t.decode().ok()),
            created_at: opts.get("created_at").and_then(|t| t.decode().ok()),
            modified_at: opts.get("modified_at").and_then(|t| t.decode().ok()),
            created_by: opts.get("created_by").and_then(|t| t.decode().ok()),
            modified_by: opts.get("modified_by").and_then(|t| t.decode().ok()),
            pages: opts.get("pages").and_then(|t| t.decode().ok()),
            format: opts.get("format").and_then(|t| t.decode().ok()),
            image_preprocessing: opts.get("image_preprocessing").and_then(|t| t.decode().ok()),
            json_schema: opts.get("json_schema").and_then(|t| t.decode().ok()),
            error: opts.get("error").and_then(|t| t.decode().ok()),
            extraction_duration_ms: opts.get("extraction_duration_ms").and_then(|t| t.decode().ok()),
            category: opts.get("category").and_then(|t| t.decode().ok()),
            tags: opts.get("tags").and_then(|t| t.decode().ok()),
            document_version: opts.get("document_version").and_then(|t| t.decode().ok()),
            abstract_text: opts.get("abstract_text").and_then(|t| t.decode().ok()),
            output_format: opts.get("output_format").and_then(|t| t.decode().ok()),
            ocr_used: opts.get("ocr_used").and_then(|t| t.decode().ok()).unwrap_or_default(),
            additional: opts.get("additional").and_then(|t| t.decode().ok()).unwrap_or_default(),
        }
    }
}

#[derive(Debug, Clone, Default, serde::Serialize, serde::Deserialize, rustler::NifMap)]
pub struct ExcelMetadata {
    pub sheet_count: Option<u32>,
    pub sheet_names: Option<Vec<String>>,
}

impl ExcelMetadata {
    pub fn new(opts: std::collections::HashMap<String, rustler::Term>) -> Self {
        Self {
            sheet_count: opts.get("sheet_count").and_then(|t| t.decode().ok()),
            sheet_names: opts.get("sheet_names").and_then(|t| t.decode().ok()),
        }
    }
}

#[derive(Debug, Clone, Default, serde::Serialize, serde::Deserialize, rustler::NifMap)]
pub struct EmailMetadata {
    pub from_email: Option<String>,
    pub from_name: Option<String>,
    pub to_emails: Vec<String>,
    pub cc_emails: Vec<String>,
    pub bcc_emails: Vec<String>,
    pub message_id: Option<String>,
    pub attachments: Vec<String>,
}

impl EmailMetadata {
    pub fn new(opts: std::collections::HashMap<String, rustler::Term>) -> Self {
        Self {
            from_email: opts.get("from_email").and_then(|t| t.decode().ok()),
            from_name: opts.get("from_name").and_then(|t| t.decode().ok()),
            to_emails: opts.get("to_emails").and_then(|t| t.decode().ok()).unwrap_or_default(),
            cc_emails: opts.get("cc_emails").and_then(|t| t.decode().ok()).unwrap_or_default(),
            bcc_emails: opts.get("bcc_emails").and_then(|t| t.decode().ok()).unwrap_or_default(),
            message_id: opts.get("message_id").and_then(|t| t.decode().ok()),
            attachments: opts
                .get("attachments")
                .and_then(|t| t.decode().ok())
                .unwrap_or_default(),
        }
    }
}

#[derive(Debug, Clone, Default, serde::Serialize, serde::Deserialize, rustler::NifMap)]
pub struct ArchiveMetadata {
    pub format: String,
    pub file_count: u32,
    pub file_list: Vec<String>,
    pub total_size: u64,
    pub compressed_size: Option<u64>,
}

impl ArchiveMetadata {
    pub fn new(opts: std::collections::HashMap<String, rustler::Term>) -> Self {
        Self {
            format: opts.get("format").and_then(|t| t.decode().ok()).unwrap_or_default(),
            file_count: opts.get("file_count").and_then(|t| t.decode().ok()).unwrap_or_default(),
            file_list: opts.get("file_list").and_then(|t| t.decode().ok()).unwrap_or_default(),
            total_size: opts.get("total_size").and_then(|t| t.decode().ok()).unwrap_or_default(),
            compressed_size: opts.get("compressed_size").and_then(|t| t.decode().ok()),
        }
    }
}

#[derive(Debug, Clone, Default, serde::Serialize, serde::Deserialize, rustler::NifMap)]
pub struct ImageMetadata {
    pub width: u32,
    pub height: u32,
    pub format: String,
    pub exif: HashMap<String, String>,
}

impl ImageMetadata {
    pub fn new(opts: std::collections::HashMap<String, rustler::Term>) -> Self {
        Self {
            width: opts.get("width").and_then(|t| t.decode().ok()).unwrap_or_default(),
            height: opts.get("height").and_then(|t| t.decode().ok()).unwrap_or_default(),
            format: opts.get("format").and_then(|t| t.decode().ok()).unwrap_or_default(),
            exif: opts.get("exif").and_then(|t| t.decode().ok()).unwrap_or_default(),
        }
    }
}

#[derive(Debug, Clone, Default, serde::Serialize, serde::Deserialize, rustler::NifMap)]
pub struct XmlMetadata {
    pub element_count: u32,
    pub unique_elements: Vec<String>,
}

impl XmlMetadata {
    pub fn new(opts: std::collections::HashMap<String, rustler::Term>) -> Self {
        Self {
            element_count: opts
                .get("element_count")
                .and_then(|t| t.decode().ok())
                .unwrap_or_default(),
            unique_elements: opts
                .get("unique_elements")
                .and_then(|t| t.decode().ok())
                .unwrap_or_default(),
        }
    }
}

#[derive(Debug, Clone, Default, serde::Serialize, serde::Deserialize, rustler::NifMap)]
pub struct TextMetadata {
    pub line_count: u32,
    pub word_count: u32,
    pub character_count: u32,
    pub headers: Option<Vec<String>>,
    pub links: Option<Vec<Vec<String>>>,
    pub code_blocks: Option<Vec<Vec<String>>>,
}

impl TextMetadata {
    pub fn new(opts: std::collections::HashMap<String, rustler::Term>) -> Self {
        Self {
            line_count: opts.get("line_count").and_then(|t| t.decode().ok()).unwrap_or_default(),
            word_count: opts.get("word_count").and_then(|t| t.decode().ok()).unwrap_or_default(),
            character_count: opts
                .get("character_count")
                .and_then(|t| t.decode().ok())
                .unwrap_or_default(),
            headers: opts.get("headers").and_then(|t| t.decode().ok()),
            links: opts.get("links").and_then(|t| t.decode().ok()),
            code_blocks: opts.get("code_blocks").and_then(|t| t.decode().ok()),
        }
    }
}

#[derive(Debug, Clone, Default, serde::Serialize, serde::Deserialize, rustler::NifStruct)]
#[module = "Kreuzberg.HeaderMetadata"]
pub struct HeaderMetadata {
    pub level: u8,
    pub text: String,
    pub id: Option<String>,
    pub depth: u32,
    pub html_offset: u32,
}

#[derive(Debug, Clone, Default, serde::Serialize, serde::Deserialize, rustler::NifStruct)]
#[module = "Kreuzberg.LinkMetadata"]
pub struct LinkMetadata {
    pub href: String,
    pub text: String,
    pub title: Option<String>,
    pub link_type: LinkType,
    pub rel: Vec<String>,
    pub attributes: Vec<Vec<String>>,
}

#[derive(Debug, Clone, Default, serde::Serialize, serde::Deserialize, rustler::NifStruct)]
#[module = "Kreuzberg.ImageMetadataType"]
pub struct ImageMetadataType {
    pub src: String,
    pub alt: Option<String>,
    pub title: Option<String>,
    pub dimensions: Option<Vec<u32>>,
    pub image_type: ImageType,
    pub attributes: Vec<Vec<String>>,
}

#[derive(Debug, Clone, Default, serde::Serialize, serde::Deserialize, rustler::NifStruct)]
#[module = "Kreuzberg.StructuredData"]
pub struct StructuredData {
    pub data_type: StructuredDataType,
    pub raw_json: String,
    pub schema_type: Option<String>,
}

#[derive(Debug, Clone, Default, serde::Serialize, serde::Deserialize, rustler::NifMap)]
pub struct HtmlMetadata {
    pub title: Option<String>,
    pub description: Option<String>,
    pub keywords: Vec<String>,
    pub author: Option<String>,
    pub canonical_url: Option<String>,
    pub base_href: Option<String>,
    pub language: Option<String>,
    pub text_direction: Option<TextDirection>,
    pub open_graph: HashMap<String, String>,
    pub twitter_card: HashMap<String, String>,
    pub meta_tags: HashMap<String, String>,
    pub headers: Vec<HeaderMetadata>,
    pub links: Vec<LinkMetadata>,
    pub images: Vec<ImageMetadataType>,
    pub structured_data: Vec<StructuredData>,
}

impl HtmlMetadata {
    pub fn new(opts: std::collections::HashMap<String, rustler::Term>) -> Self {
        Self {
            title: opts.get("title").and_then(|t| t.decode().ok()),
            description: opts.get("description").and_then(|t| t.decode().ok()),
            keywords: opts.get("keywords").and_then(|t| t.decode().ok()).unwrap_or_default(),
            author: opts.get("author").and_then(|t| t.decode().ok()),
            canonical_url: opts.get("canonical_url").and_then(|t| t.decode().ok()),
            base_href: opts.get("base_href").and_then(|t| t.decode().ok()),
            language: opts.get("language").and_then(|t| t.decode().ok()),
            text_direction: opts.get("text_direction").and_then(|t| t.decode().ok()),
            open_graph: opts.get("open_graph").and_then(|t| t.decode().ok()).unwrap_or_default(),
            twitter_card: opts
                .get("twitter_card")
                .and_then(|t| t.decode().ok())
                .unwrap_or_default(),
            meta_tags: opts.get("meta_tags").and_then(|t| t.decode().ok()).unwrap_or_default(),
            headers: opts.get("headers").and_then(|t| t.decode().ok()).unwrap_or_default(),
            links: opts.get("links").and_then(|t| t.decode().ok()).unwrap_or_default(),
            images: opts.get("images").and_then(|t| t.decode().ok()).unwrap_or_default(),
            structured_data: opts
                .get("structured_data")
                .and_then(|t| t.decode().ok())
                .unwrap_or_default(),
        }
    }
}

#[derive(Debug, Clone, Default, serde::Serialize, serde::Deserialize, rustler::NifMap)]
pub struct OcrMetadata {
    pub language: String,
    pub psm: i32,
    pub output_format: String,
    pub table_count: u32,
    pub table_rows: Option<u32>,
    pub table_cols: Option<u32>,
}

impl OcrMetadata {
    pub fn new(opts: std::collections::HashMap<String, rustler::Term>) -> Self {
        Self {
            language: opts.get("language").and_then(|t| t.decode().ok()).unwrap_or_default(),
            psm: opts.get("psm").and_then(|t| t.decode().ok()).unwrap_or_default(),
            output_format: opts
                .get("output_format")
                .and_then(|t| t.decode().ok())
                .unwrap_or_default(),
            table_count: opts
                .get("table_count")
                .and_then(|t| t.decode().ok())
                .unwrap_or_default(),
            table_rows: opts.get("table_rows").and_then(|t| t.decode().ok()),
            table_cols: opts.get("table_cols").and_then(|t| t.decode().ok()),
        }
    }
}

#[derive(Debug, Clone, Default, serde::Serialize, serde::Deserialize, rustler::NifStruct)]
#[module = "Kreuzberg.ErrorMetadata"]
pub struct ErrorMetadata {
    pub error_type: String,
    pub message: String,
}

#[derive(Debug, Clone, Default, serde::Serialize, serde::Deserialize, rustler::NifMap)]
pub struct PptxMetadata {
    pub slide_count: u32,
    pub slide_names: Vec<String>,
    pub image_count: Option<u32>,
    pub table_count: Option<u32>,
}

impl PptxMetadata {
    pub fn new(opts: std::collections::HashMap<String, rustler::Term>) -> Self {
        Self {
            slide_count: opts
                .get("slide_count")
                .and_then(|t| t.decode().ok())
                .unwrap_or_default(),
            slide_names: opts
                .get("slide_names")
                .and_then(|t| t.decode().ok())
                .unwrap_or_default(),
            image_count: opts.get("image_count").and_then(|t| t.decode().ok()),
            table_count: opts.get("table_count").and_then(|t| t.decode().ok()),
        }
    }
}

#[derive(Debug, Clone, Default, serde::Serialize, serde::Deserialize, rustler::NifMap)]
pub struct DocxMetadata {
    pub core_properties: Option<CoreProperties>,
    pub app_properties: Option<DocxAppProperties>,
    pub custom_properties: Option<HashMap<String, String>>,
}

impl DocxMetadata {
    pub fn new(opts: std::collections::HashMap<String, rustler::Term>) -> Self {
        Self {
            core_properties: opts.get("core_properties").and_then(|t| t.decode().ok()),
            app_properties: opts.get("app_properties").and_then(|t| t.decode().ok()),
            custom_properties: opts.get("custom_properties").and_then(|t| t.decode().ok()),
        }
    }
}

#[derive(Debug, Clone, Default, serde::Serialize, serde::Deserialize, rustler::NifMap)]
pub struct CsvMetadata {
    pub row_count: u32,
    pub column_count: u32,
    pub delimiter: Option<String>,
    pub has_header: bool,
    pub column_types: Option<Vec<String>>,
}

impl CsvMetadata {
    pub fn new(opts: std::collections::HashMap<String, rustler::Term>) -> Self {
        Self {
            row_count: opts.get("row_count").and_then(|t| t.decode().ok()).unwrap_or_default(),
            column_count: opts
                .get("column_count")
                .and_then(|t| t.decode().ok())
                .unwrap_or_default(),
            delimiter: opts.get("delimiter").and_then(|t| t.decode().ok()),
            has_header: opts.get("has_header").and_then(|t| t.decode().ok()).unwrap_or_default(),
            column_types: opts.get("column_types").and_then(|t| t.decode().ok()),
        }
    }
}

#[derive(Debug, Clone, Default, serde::Serialize, serde::Deserialize, rustler::NifMap)]
pub struct BibtexMetadata {
    pub entry_count: usize,
    pub citation_keys: Vec<String>,
    pub authors: Vec<String>,
    pub year_range: Option<YearRange>,
    pub entry_types: Option<HashMap<String, usize>>,
}

impl BibtexMetadata {
    pub fn new(opts: std::collections::HashMap<String, rustler::Term>) -> Self {
        Self {
            entry_count: opts
                .get("entry_count")
                .and_then(|t| t.decode().ok())
                .unwrap_or_default(),
            citation_keys: opts
                .get("citation_keys")
                .and_then(|t| t.decode().ok())
                .unwrap_or_default(),
            authors: opts.get("authors").and_then(|t| t.decode().ok()).unwrap_or_default(),
            year_range: opts.get("year_range").and_then(|t| t.decode().ok()),
            entry_types: opts.get("entry_types").and_then(|t| t.decode().ok()),
        }
    }
}

#[derive(Debug, Clone, Default, serde::Serialize, serde::Deserialize, rustler::NifMap)]
pub struct CitationMetadata {
    pub citation_count: usize,
    pub format: Option<String>,
    pub authors: Vec<String>,
    pub year_range: Option<YearRange>,
    pub dois: Vec<String>,
    pub keywords: Vec<String>,
}

impl CitationMetadata {
    pub fn new(opts: std::collections::HashMap<String, rustler::Term>) -> Self {
        Self {
            citation_count: opts
                .get("citation_count")
                .and_then(|t| t.decode().ok())
                .unwrap_or_default(),
            format: opts.get("format").and_then(|t| t.decode().ok()),
            authors: opts.get("authors").and_then(|t| t.decode().ok()).unwrap_or_default(),
            year_range: opts.get("year_range").and_then(|t| t.decode().ok()),
            dois: opts.get("dois").and_then(|t| t.decode().ok()).unwrap_or_default(),
            keywords: opts.get("keywords").and_then(|t| t.decode().ok()).unwrap_or_default(),
        }
    }
}

#[derive(Debug, Clone, Default, serde::Serialize, serde::Deserialize, rustler::NifStruct)]
#[module = "Kreuzberg.YearRange"]
pub struct YearRange {
    pub min: Option<u32>,
    pub max: Option<u32>,
    pub years: Vec<u32>,
}

#[derive(Debug, Clone, Default, serde::Serialize, serde::Deserialize, rustler::NifMap)]
pub struct FictionBookMetadata {
    pub genres: Vec<String>,
    pub sequences: Vec<String>,
    pub annotation: Option<String>,
}

impl FictionBookMetadata {
    pub fn new(opts: std::collections::HashMap<String, rustler::Term>) -> Self {
        Self {
            genres: opts.get("genres").and_then(|t| t.decode().ok()).unwrap_or_default(),
            sequences: opts.get("sequences").and_then(|t| t.decode().ok()).unwrap_or_default(),
            annotation: opts.get("annotation").and_then(|t| t.decode().ok()),
        }
    }
}

#[derive(Debug, Clone, Default, serde::Serialize, serde::Deserialize, rustler::NifMap)]
pub struct DbfMetadata {
    pub record_count: usize,
    pub field_count: usize,
    pub fields: Vec<DbfFieldInfo>,
}

impl DbfMetadata {
    pub fn new(opts: std::collections::HashMap<String, rustler::Term>) -> Self {
        Self {
            record_count: opts
                .get("record_count")
                .and_then(|t| t.decode().ok())
                .unwrap_or_default(),
            field_count: opts
                .get("field_count")
                .and_then(|t| t.decode().ok())
                .unwrap_or_default(),
            fields: opts.get("fields").and_then(|t| t.decode().ok()).unwrap_or_default(),
        }
    }
}

#[derive(Debug, Clone, Default, serde::Serialize, serde::Deserialize, rustler::NifStruct)]
#[module = "Kreuzberg.DbfFieldInfo"]
pub struct DbfFieldInfo {
    pub name: String,
    pub field_type: String,
}

#[derive(Debug, Clone, Default, serde::Serialize, serde::Deserialize, rustler::NifMap)]
pub struct JatsMetadata {
    pub copyright: Option<String>,
    pub license: Option<String>,
    pub history_dates: HashMap<String, String>,
    pub contributor_roles: Vec<ContributorRole>,
}

impl JatsMetadata {
    pub fn new(opts: std::collections::HashMap<String, rustler::Term>) -> Self {
        Self {
            copyright: opts.get("copyright").and_then(|t| t.decode().ok()),
            license: opts.get("license").and_then(|t| t.decode().ok()),
            history_dates: opts
                .get("history_dates")
                .and_then(|t| t.decode().ok())
                .unwrap_or_default(),
            contributor_roles: opts
                .get("contributor_roles")
                .and_then(|t| t.decode().ok())
                .unwrap_or_default(),
        }
    }
}

#[derive(Debug, Clone, Default, serde::Serialize, serde::Deserialize, rustler::NifStruct)]
#[module = "Kreuzberg.ContributorRole"]
pub struct ContributorRole {
    pub name: String,
    pub role: Option<String>,
}

#[derive(Debug, Clone, Default, serde::Serialize, serde::Deserialize, rustler::NifMap)]
pub struct EpubMetadata {
    pub coverage: Option<String>,
    pub dc_format: Option<String>,
    pub relation: Option<String>,
    pub source: Option<String>,
    pub dc_type: Option<String>,
    pub cover_image: Option<String>,
}

impl EpubMetadata {
    pub fn new(opts: std::collections::HashMap<String, rustler::Term>) -> Self {
        Self {
            coverage: opts.get("coverage").and_then(|t| t.decode().ok()),
            dc_format: opts.get("dc_format").and_then(|t| t.decode().ok()),
            relation: opts.get("relation").and_then(|t| t.decode().ok()),
            source: opts.get("source").and_then(|t| t.decode().ok()),
            dc_type: opts.get("dc_type").and_then(|t| t.decode().ok()),
            cover_image: opts.get("cover_image").and_then(|t| t.decode().ok()),
        }
    }
}

#[derive(Debug, Clone, Default, serde::Serialize, serde::Deserialize, rustler::NifMap)]
pub struct PstMetadata {
    pub message_count: usize,
}

impl PstMetadata {
    pub fn new(opts: std::collections::HashMap<String, rustler::Term>) -> Self {
        Self {
            message_count: opts
                .get("message_count")
                .and_then(|t| t.decode().ok())
                .unwrap_or_default(),
        }
    }
}

#[derive(Debug, Clone, Default, serde::Serialize, serde::Deserialize, rustler::NifMap)]
pub struct OcrConfidence {
    pub detection: Option<f64>,
    pub recognition: f64,
}

impl OcrConfidence {
    pub fn new(opts: std::collections::HashMap<String, rustler::Term>) -> Self {
        Self {
            detection: opts.get("detection").and_then(|t| t.decode().ok()),
            recognition: opts
                .get("recognition")
                .and_then(|t| t.decode().ok())
                .unwrap_or_default(),
        }
    }
}

#[derive(Debug, Clone, Default, serde::Serialize, serde::Deserialize, rustler::NifStruct)]
#[module = "Kreuzberg.OcrRotation"]
pub struct OcrRotation {
    pub angle_degrees: f64,
    pub confidence: Option<f64>,
}

#[derive(Debug, Clone, Default, serde::Serialize, serde::Deserialize, rustler::NifMap)]
pub struct OcrElement {
    pub text: String,
    pub geometry: OcrBoundingGeometry,
    pub confidence: OcrConfidence,
    pub level: OcrElementLevel,
    pub rotation: Option<OcrRotation>,
    pub page_number: u32,
    pub parent_id: Option<String>,
    pub backend_metadata: HashMap<String, String>,
}

impl OcrElement {
    pub fn new(opts: std::collections::HashMap<String, rustler::Term>) -> Self {
        Self {
            text: opts.get("text").and_then(|t| t.decode().ok()).unwrap_or_default(),
            geometry: opts.get("geometry").and_then(|t| t.decode().ok()).unwrap_or_default(),
            confidence: opts.get("confidence").and_then(|t| t.decode().ok()).unwrap_or_default(),
            level: opts.get("level").and_then(|t| t.decode().ok()).unwrap_or_default(),
            rotation: opts.get("rotation").and_then(|t| t.decode().ok()),
            page_number: opts
                .get("page_number")
                .and_then(|t| t.decode().ok())
                .unwrap_or_default(),
            parent_id: opts.get("parent_id").and_then(|t| t.decode().ok()),
            backend_metadata: opts
                .get("backend_metadata")
                .and_then(|t| t.decode().ok())
                .unwrap_or_default(),
        }
    }
}

#[derive(Debug, Clone, Default, serde::Serialize, serde::Deserialize, rustler::NifMap)]
pub struct OcrElementConfig {
    pub include_elements: bool,
    pub min_level: OcrElementLevel,
    pub min_confidence: f64,
    pub build_hierarchy: bool,
}

impl OcrElementConfig {
    pub fn new(opts: std::collections::HashMap<String, rustler::Term>) -> Self {
        Self {
            include_elements: opts
                .get("include_elements")
                .and_then(|t| t.decode().ok())
                .unwrap_or_default(),
            min_level: opts.get("min_level").and_then(|t| t.decode().ok()).unwrap_or_default(),
            min_confidence: opts
                .get("min_confidence")
                .and_then(|t| t.decode().ok())
                .unwrap_or_default(),
            build_hierarchy: opts
                .get("build_hierarchy")
                .and_then(|t| t.decode().ok())
                .unwrap_or_default(),
        }
    }
}

#[derive(Debug, Clone, Default, serde::Serialize, serde::Deserialize, rustler::NifStruct)]
#[module = "Kreuzberg.PageStructure"]
pub struct PageStructure {
    pub total_count: u32,
    pub unit_type: PageUnitType,
    pub boundaries: Option<Vec<PageBoundary>>,
    pub pages: Option<Vec<PageInfo>>,
}

#[derive(Debug, Clone, Default, serde::Serialize, serde::Deserialize, rustler::NifStruct)]
#[module = "Kreuzberg.PageBoundary"]
pub struct PageBoundary {
    pub byte_start: usize,
    pub byte_end: usize,
    pub page_number: u32,
}

#[derive(Debug, Clone, Default, serde::Serialize, serde::Deserialize, rustler::NifStruct)]
#[module = "Kreuzberg.PageInfo"]
pub struct PageInfo {
    pub number: u32,
    pub title: Option<String>,
    pub dimensions: Option<Vec<f64>>,
    pub image_count: Option<u32>,
    pub table_count: Option<u32>,
    pub hidden: Option<bool>,
    pub is_blank: Option<bool>,
    pub has_vector_graphics: bool,
}

#[derive(Debug, Clone, Default, serde::Serialize, serde::Deserialize, rustler::NifStruct)]
#[module = "Kreuzberg.PageContent"]
pub struct PageContent {
    pub page_number: u32,
    pub content: String,
    pub tables: Vec<Table>,
    pub image_indices: Vec<u32>,
    pub hierarchy: Option<PageHierarchy>,
    pub is_blank: Option<bool>,
    pub layout_regions: Option<Vec<LayoutRegion>>,
    pub speaker_notes: Option<String>,
    pub section_name: Option<String>,
    pub sheet_name: Option<String>,
}

#[derive(Debug, Clone, Default, serde::Serialize, serde::Deserialize, rustler::NifMap)]
pub struct LayoutRegion {
    pub class_name: String,
    pub confidence: f64,
    pub bounding_box: BoundingBox,
    pub area_fraction: f64,
}

impl LayoutRegion {
    pub fn new(opts: std::collections::HashMap<String, rustler::Term>) -> Self {
        Self {
            class_name: opts.get("class_name").and_then(|t| t.decode().ok()).unwrap_or_default(),
            confidence: opts.get("confidence").and_then(|t| t.decode().ok()).unwrap_or_default(),
            bounding_box: opts
                .get("bounding_box")
                .and_then(|t| t.decode().ok())
                .unwrap_or_default(),
            area_fraction: opts
                .get("area_fraction")
                .and_then(|t| t.decode().ok())
                .unwrap_or_default(),
        }
    }
}

#[derive(Debug, Clone, Default, serde::Serialize, serde::Deserialize, rustler::NifStruct)]
#[module = "Kreuzberg.PageHierarchy"]
pub struct PageHierarchy {
    pub block_count: u32,
    pub blocks: Vec<HierarchicalBlock>,
}

#[derive(Debug, Clone, Default, serde::Serialize, serde::Deserialize, rustler::NifStruct)]
#[module = "Kreuzberg.HierarchicalBlock"]
pub struct HierarchicalBlock {
    pub text: String,
    pub font_size: f32,
    pub level: String,
    pub bbox: Option<Vec<f32>>,
}

#[derive(Debug, Clone, Default, serde::Serialize, serde::Deserialize, rustler::NifStruct)]
#[module = "Kreuzberg.CellChange"]
pub struct CellChange {
    pub row: usize,
    pub col: usize,
    pub from: String,
    pub to: String,
}

#[derive(Debug, Clone, Default, serde::Serialize, serde::Deserialize, rustler::NifStruct)]
#[module = "Kreuzberg.DocumentRevision"]
pub struct DocumentRevision {
    pub revision_id: String,
    pub author: Option<String>,
    pub timestamp: Option<String>,
    pub kind: RevisionKind,
    pub anchor: Option<RevisionAnchor>,
    pub delta: RevisionDelta,
}

#[derive(Debug, Clone, Default, serde::Serialize, serde::Deserialize, rustler::NifMap)]
pub struct RevisionDelta {
    pub content: Vec<DiffLine>,
    pub table_changes: Vec<CellChange>,
}

impl RevisionDelta {
    pub fn new(opts: std::collections::HashMap<String, rustler::Term>) -> Self {
        Self {
            content: opts.get("content").and_then(|t| t.decode().ok()).unwrap_or_default(),
            table_changes: opts
                .get("table_changes")
                .and_then(|t| t.decode().ok())
                .unwrap_or_default(),
        }
    }
}

#[derive(Debug, Clone, Default, serde::Serialize, serde::Deserialize, rustler::NifMap)]
pub struct Table {
    pub cells: Vec<Vec<String>>,
    pub markdown: String,
    pub page_number: u32,
    pub bounding_box: Option<BoundingBox>,
}

impl Table {
    pub fn new(opts: std::collections::HashMap<String, rustler::Term>) -> Self {
        Self {
            cells: opts.get("cells").and_then(|t| t.decode().ok()).unwrap_or_default(),
            markdown: opts.get("markdown").and_then(|t| t.decode().ok()).unwrap_or_default(),
            page_number: opts
                .get("page_number")
                .and_then(|t| t.decode().ok())
                .unwrap_or_default(),
            bounding_box: opts.get("bounding_box").and_then(|t| t.decode().ok()),
        }
    }
}

#[derive(Debug, Clone, Default, serde::Serialize, serde::Deserialize, rustler::NifStruct)]
#[module = "Kreuzberg.ExtractedUri"]
pub struct ExtractedUri {
    pub url: String,
    pub label: Option<String>,
    pub page: Option<u32>,
    pub kind: UriKind,
}

#[derive(Debug, Clone, Default, serde::Serialize, serde::Deserialize, rustler::NifMap)]
pub struct DiffOptions {
    pub include_metadata: bool,
    pub include_embedded: bool,
    pub max_content_chars: Option<usize>,
}

impl DiffOptions {
    pub fn new(opts: std::collections::HashMap<String, rustler::Term>) -> Self {
        Self {
            include_metadata: opts
                .get("include_metadata")
                .and_then(|t| t.decode().ok())
                .unwrap_or(true),
            include_embedded: opts
                .get("include_embedded")
                .and_then(|t| t.decode().ok())
                .unwrap_or(true),
            max_content_chars: opts.get("max_content_chars").and_then(|t| t.decode().ok()),
        }
    }
}

#[derive(Debug, Clone, Default, serde::Serialize, serde::Deserialize, rustler::NifStruct)]
#[module = "Kreuzberg.ExtractionDiff"]
pub struct ExtractionDiff {
    pub content_diff: Vec<DiffHunk>,
    pub tables_added: Vec<Table>,
    pub tables_removed: Vec<Table>,
    pub tables_changed: Vec<TableDiff>,
    pub metadata_changed: String,
    pub embedded_changes: EmbeddedChanges,
}

#[derive(Debug, Clone, Default, serde::Serialize, serde::Deserialize, rustler::NifStruct)]
#[module = "Kreuzberg.DiffHunk"]
pub struct DiffHunk {
    pub from_line: usize,
    pub from_count: usize,
    pub to_line: usize,
    pub to_count: usize,
    pub lines: Vec<DiffLine>,
}

#[derive(Debug, Clone, Default, serde::Serialize, serde::Deserialize, rustler::NifStruct)]
#[module = "Kreuzberg.TableDiff"]
pub struct TableDiff {
    pub from_index: usize,
    pub to_index: usize,
    pub cell_changes: Vec<CellChange>,
}

#[derive(Debug, Clone, Default, serde::Serialize, serde::Deserialize, rustler::NifStruct)]
#[module = "Kreuzberg.EmbeddedChanges"]
pub struct EmbeddedChanges {
    pub added: Vec<ArchiveEntry>,
    pub removed: Vec<ArchiveEntry>,
    pub changed: Vec<EmbeddedDiff>,
}

#[derive(Debug, Clone, Default, serde::Serialize, serde::Deserialize, rustler::NifStruct)]
#[module = "Kreuzberg.EmbeddedDiff"]
pub struct EmbeddedDiff {
    pub path: String,
    pub diff: ExtractionDiff,
}

#[derive(Debug, Clone, Default, serde::Serialize, serde::Deserialize, rustler::NifStruct)]
#[module = "Kreuzberg.EmbeddingPreset"]
pub struct EmbeddingPreset {
    pub name: String,
    pub chunk_size: usize,
    pub overlap: usize,
    pub model_repo: String,
    pub pooling: String,
    pub model_file: String,
    pub dimensions: usize,
    pub description: String,
}

#[derive(Debug, Clone, Default, serde::Serialize, serde::Deserialize, rustler::NifMap)]
pub struct YakeParams {
    pub window_size: usize,
}

impl YakeParams {
    pub fn new(opts: std::collections::HashMap<String, rustler::Term>) -> Self {
        Self {
            window_size: opts.get("window_size").and_then(|t| t.decode().ok()).unwrap_or(2),
        }
    }
}

#[derive(Debug, Clone, Default, serde::Serialize, serde::Deserialize, rustler::NifMap)]
pub struct RakeParams {
    pub min_word_length: usize,
    pub max_words_per_phrase: usize,
}

impl RakeParams {
    pub fn new(opts: std::collections::HashMap<String, rustler::Term>) -> Self {
        Self {
            min_word_length: opts.get("min_word_length").and_then(|t| t.decode().ok()).unwrap_or(1),
            max_words_per_phrase: opts
                .get("max_words_per_phrase")
                .and_then(|t| t.decode().ok())
                .unwrap_or(3),
        }
    }
}

#[derive(Debug, Clone, Default, serde::Serialize, serde::Deserialize, rustler::NifMap)]
pub struct KeywordConfig {
    pub algorithm: KeywordAlgorithm,
    pub max_keywords: usize,
    pub min_score: f32,
    pub ngram_range: Vec<usize>,
    pub language: Option<String>,
    pub yake_params: Option<YakeParams>,
    pub rake_params: Option<RakeParams>,
}

impl KeywordConfig {
    pub fn new(opts: std::collections::HashMap<String, rustler::Term>) -> Self {
        Self {
            algorithm: opts.get("algorithm").and_then(|t| t.decode().ok()).unwrap_or_default(),
            max_keywords: opts.get("max_keywords").and_then(|t| t.decode().ok()).unwrap_or(10),
            min_score: opts.get("min_score").and_then(|t| t.decode().ok()).unwrap_or(0.0),
            ngram_range: opts
                .get("ngram_range")
                .and_then(|t| t.decode().ok())
                .unwrap_or_default(),
            language: opts.get("language").and_then(|t| t.decode().ok()),
            yake_params: opts.get("yake_params").and_then(|t| t.decode().ok()),
            rake_params: opts.get("rake_params").and_then(|t| t.decode().ok()),
        }
    }
}

#[derive(Debug, Clone, Default, serde::Serialize, serde::Deserialize, rustler::NifStruct)]
#[module = "Kreuzberg.Keyword"]
pub struct Keyword {
    pub text: String,
    pub score: f32,
    pub algorithm: KeywordAlgorithm,
    pub positions: Option<Vec<usize>>,
}

#[derive(Debug, Clone, Default, serde::Serialize, serde::Deserialize, rustler::NifMap)]
pub struct PaddleOcrConfig {
    pub language: String,
    pub cache_dir: Option<String>,
    pub use_angle_cls: bool,
    pub enable_table_detection: bool,
    pub det_db_thresh: f32,
    pub det_db_box_thresh: f32,
    pub det_db_unclip_ratio: f32,
    pub det_limit_side_len: u32,
    pub rec_batch_num: u32,
    pub padding: u32,
    pub drop_score: f32,
    pub model_tier: String,
}

impl PaddleOcrConfig {
    pub fn new(opts: std::collections::HashMap<String, rustler::Term>) -> Self {
        Self {
            language: opts.get("language").and_then(|t| t.decode().ok()).unwrap_or_default(),
            cache_dir: opts.get("cache_dir").and_then(|t| t.decode().ok()),
            use_angle_cls: opts
                .get("use_angle_cls")
                .and_then(|t| t.decode().ok())
                .unwrap_or_default(),
            enable_table_detection: opts
                .get("enable_table_detection")
                .and_then(|t| t.decode().ok())
                .unwrap_or_default(),
            det_db_thresh: opts
                .get("det_db_thresh")
                .and_then(|t| t.decode().ok())
                .unwrap_or_default(),
            det_db_box_thresh: opts
                .get("det_db_box_thresh")
                .and_then(|t| t.decode().ok())
                .unwrap_or_default(),
            det_db_unclip_ratio: opts
                .get("det_db_unclip_ratio")
                .and_then(|t| t.decode().ok())
                .unwrap_or_default(),
            det_limit_side_len: opts
                .get("det_limit_side_len")
                .and_then(|t| t.decode().ok())
                .unwrap_or_default(),
            rec_batch_num: opts
                .get("rec_batch_num")
                .and_then(|t| t.decode().ok())
                .unwrap_or_default(),
            padding: opts.get("padding").and_then(|t| t.decode().ok()).unwrap_or_default(),
            drop_score: opts.get("drop_score").and_then(|t| t.decode().ok()).unwrap_or_default(),
            model_tier: opts.get("model_tier").and_then(|t| t.decode().ok()).unwrap_or_default(),
        }
    }
}

#[derive(Debug, Clone, Default, serde::Serialize, serde::Deserialize, rustler::NifMap)]
pub struct PdfMetadata {
    pub pdf_version: Option<String>,
    pub producer: Option<String>,
    pub is_encrypted: Option<bool>,
    pub width: Option<i64>,
    pub height: Option<i64>,
    pub page_count: Option<u32>,
}

impl PdfMetadata {
    pub fn new(opts: std::collections::HashMap<String, rustler::Term>) -> Self {
        Self {
            pdf_version: opts.get("pdf_version").and_then(|t| t.decode().ok()),
            producer: opts.get("producer").and_then(|t| t.decode().ok()),
            is_encrypted: opts.get("is_encrypted").and_then(|t| t.decode().ok()),
            width: opts.get("width").and_then(|t| t.decode().ok()),
            height: opts.get("height").and_then(|t| t.decode().ok()),
            page_count: opts.get("page_count").and_then(|t| t.decode().ok()),
        }
    }
}

#[derive(Debug, Clone, Copy, serde::Serialize, serde::Deserialize, rustler::NifUnitEnum)]
pub enum ExecutionProviderType {
    Auto,
    Cpu,
    CoreMl,
    Cuda,
    TensorRt,
}

#[allow(clippy::derivable_impls)]
impl Default for ExecutionProviderType {
    fn default() -> Self {
        Self::Auto
    }
}

#[derive(Debug, Clone, serde::Serialize, serde::Deserialize, rustler::NifStruct)]
#[module = "Kreuzberg.OutputFormat"]
pub struct OutputFormat {
    pub format_type: String,
    pub custom: Option<String>,
}

#[allow(clippy::derivable_impls)]
impl Default for OutputFormat {
    fn default() -> Self {
        Self {
            format_type: String::new(),
            custom: None,
        }
    }
}

#[derive(Debug, Clone, Copy, serde::Serialize, serde::Deserialize, rustler::NifUnitEnum)]
pub enum HtmlTheme {
    Default,
    GitHub,
    Dark,
    Light,
    Unstyled,
}

#[allow(clippy::derivable_impls)]
impl Default for HtmlTheme {
    fn default() -> Self {
        Self::Unstyled
    }
}

#[derive(Debug, Clone, Copy, serde::Serialize, serde::Deserialize, rustler::NifUnitEnum)]
pub enum TableModel {
    Tatr,
    SlanetWired,
    SlanetWireless,
    SlanetPlus,
    SlanetAuto,
    Disabled,
}

#[allow(clippy::derivable_impls)]
impl Default for TableModel {
    fn default() -> Self {
        Self::Tatr
    }
}

#[derive(Debug, Clone, Copy, serde::Serialize, serde::Deserialize, rustler::NifUnitEnum)]
pub enum ChunkerType {
    Text,
    Markdown,
    Yaml,
    Semantic,
}

#[allow(clippy::derivable_impls)]
impl Default for ChunkerType {
    fn default() -> Self {
        Self::Text
    }
}

#[derive(Debug, Clone, serde::Serialize, serde::Deserialize, rustler::NifTaggedEnum)]
#[serde(tag = "type", rename_all = "snake_case")]
pub enum ChunkSizing {
    Characters,
    Tokenizer {
        model: String,
        #[serde(default)]
        cache_dir: Option<String>,
    },
}

#[allow(clippy::derivable_impls)]
impl Default for ChunkSizing {
    fn default() -> Self {
        Self::Characters
    }
}

#[derive(Debug, Clone, serde::Serialize, serde::Deserialize, rustler::NifTaggedEnum)]
#[serde(tag = "type", rename_all = "snake_case")]
pub enum EmbeddingModelType {
    Preset { name: String },
    Custom { model_id: String, dimensions: usize },
    Llm { llm: LlmConfig },
    Plugin { name: String },
}

#[allow(clippy::derivable_impls)]
impl Default for EmbeddingModelType {
    fn default() -> Self {
        Self::Preset {
            name: Default::default(),
        }
    }
}

#[derive(Debug, Clone, Copy, serde::Serialize, serde::Deserialize, rustler::NifUnitEnum)]
pub enum CodeContentMode {
    Chunks,
    Raw,
    Structure,
}

#[allow(clippy::derivable_impls)]
impl Default for CodeContentMode {
    fn default() -> Self {
        Self::Chunks
    }
}

#[derive(Debug, Clone, Copy, serde::Serialize, serde::Deserialize, rustler::NifUnitEnum)]
pub enum ListType {
    Bullet,
    Numbered,
    Lettered,
    Indented,
}

#[allow(clippy::derivable_impls)]
impl Default for ListType {
    fn default() -> Self {
        Self::Bullet
    }
}

#[derive(Debug, Clone, Copy, serde::Serialize, serde::Deserialize, rustler::NifUnitEnum)]
pub enum OcrBackendType {
    Tesseract,
    EasyOCR,
    PaddleOCR,
    Custom,
}

#[allow(clippy::derivable_impls)]
impl Default for OcrBackendType {
    fn default() -> Self {
        Self::Tesseract
    }
}

#[derive(Debug, Clone, Copy, serde::Serialize, serde::Deserialize, rustler::NifUnitEnum)]
pub enum ProcessingStage {
    Early,
    Middle,
    Late,
}

#[allow(clippy::derivable_impls)]
impl Default for ProcessingStage {
    fn default() -> Self {
        Self::Early
    }
}

#[derive(Debug, Clone, Copy, serde::Serialize, serde::Deserialize, rustler::NifUnitEnum)]
pub enum ReductionLevel {
    Off,
    Light,
    Moderate,
    Aggressive,
    Maximum,
}

#[allow(clippy::derivable_impls)]
impl Default for ReductionLevel {
    fn default() -> Self {
        Self::Off
    }
}

#[derive(Debug, Clone, Copy, serde::Serialize, serde::Deserialize, rustler::NifUnitEnum)]
pub enum PdfAnnotationType {
    Text,
    Highlight,
    Link,
    Stamp,
    Underline,
    StrikeOut,
    Other,
}

#[allow(clippy::derivable_impls)]
impl Default for PdfAnnotationType {
    fn default() -> Self {
        Self::Text
    }
}

#[derive(Debug, Clone, Copy, serde::Serialize, serde::Deserialize, rustler::NifUnitEnum)]
pub enum BlockType {
    Paragraph,
    Heading,
    Blockquote,
    CodeBlock,
    ListItem,
    OrderedList,
    BulletList,
    TaskList,
    DefinitionList,
    DefinitionTerm,
    DefinitionDescription,
    Div,
    Section,
    ThematicBreak,
    RawBlock,
    MathDisplay,
}

#[allow(clippy::derivable_impls)]
impl Default for BlockType {
    fn default() -> Self {
        Self::Paragraph
    }
}

#[derive(Debug, Clone, Copy, serde::Serialize, serde::Deserialize, rustler::NifUnitEnum)]
pub enum InlineType {
    Text,
    Strong,
    Emphasis,
    Highlight,
    Subscript,
    Superscript,
    Insert,
    Delete,
    Code,
    Link,
    Image,
    Span,
    Math,
    RawInline,
    FootnoteRef,
    Symbol,
}

#[allow(clippy::derivable_impls)]
impl Default for InlineType {
    fn default() -> Self {
        Self::Text
    }
}

#[derive(Debug, Clone, Copy, serde::Serialize, serde::Deserialize, rustler::NifUnitEnum)]
pub enum RelationshipKind {
    FootnoteReference,
    CitationReference,
    InternalLink,
    Caption,
    Label,
    TocEntry,
    CrossReference,
}

#[allow(clippy::derivable_impls)]
impl Default for RelationshipKind {
    fn default() -> Self {
        Self::FootnoteReference
    }
}

#[derive(Debug, Clone, Copy, serde::Serialize, serde::Deserialize, rustler::NifUnitEnum)]
pub enum ContentLayer {
    Body,
    Header,
    Footer,
    Footnote,
}

#[allow(clippy::derivable_impls)]
impl Default for ContentLayer {
    fn default() -> Self {
        Self::Body
    }
}

#[derive(Debug, Clone, serde::Serialize, serde::Deserialize, rustler::NifTaggedEnum)]
#[serde(tag = "node_type", rename_all = "snake_case")]
pub enum NodeContent {
    Title {
        text: String,
    },
    Heading {
        level: u8,
        text: String,
    },
    Paragraph {
        text: String,
    },
    List {
        ordered: bool,
    },
    ListItem {
        text: String,
    },
    Table {
        grid: TableGrid,
    },
    Image {
        #[serde(default)]
        description: Option<String>,
        #[serde(default)]
        image_index: Option<u32>,
        #[serde(default)]
        src: Option<String>,
    },
    Code {
        text: String,
        #[serde(default)]
        language: Option<String>,
    },
    Quote,
    Formula {
        text: String,
    },
    Footnote {
        text: String,
    },
    Group {
        #[serde(default)]
        label: Option<String>,
        #[serde(default)]
        heading_level: Option<u8>,
        #[serde(default)]
        heading_text: Option<String>,
    },
    PageBreak,
    Slide {
        number: u32,
        #[serde(default)]
        title: Option<String>,
    },
    DefinitionList,
    DefinitionItem {
        term: String,
        definition: String,
    },
    Citation {
        key: String,
        text: String,
    },
    Admonition {
        kind: String,
        #[serde(default)]
        title: Option<String>,
    },
    RawBlock {
        format: String,
        content: String,
    },
    MetadataBlock {
        entries: Vec<Vec<String>>,
    },
}

#[allow(clippy::derivable_impls)]
impl Default for NodeContent {
    fn default() -> Self {
        Self::Title {
            text: Default::default(),
        }
    }
}

#[derive(Debug, Clone, serde::Serialize, serde::Deserialize, rustler::NifTaggedEnum)]
#[serde(tag = "annotation_type", rename_all = "snake_case")]
pub enum AnnotationKind {
    Bold,
    Italic,
    Underline,
    Strikethrough,
    Code,
    Subscript,
    Superscript,
    Link {
        url: String,
        #[serde(default)]
        title: Option<String>,
    },
    Highlight,
    Color {
        value: String,
    },
    FontSize {
        value: String,
    },
    Custom {
        name: String,
        #[serde(default)]
        value: Option<String>,
    },
}

#[allow(clippy::derivable_impls)]
impl Default for AnnotationKind {
    fn default() -> Self {
        Self::Bold
    }
}

#[derive(Debug, Clone, Copy, serde::Serialize, serde::Deserialize, rustler::NifUnitEnum)]
pub enum ExtractionMethod {
    Native,
    Ocr,
    Mixed,
}

#[allow(clippy::derivable_impls)]
impl Default for ExtractionMethod {
    fn default() -> Self {
        Self::Native
    }
}

#[derive(Debug, Clone, Copy, serde::Serialize, serde::Deserialize, rustler::NifUnitEnum)]
pub enum ChunkType {
    Heading,
    PartyList,
    Definitions,
    OperativeClause,
    SignatureBlock,
    Schedule,
    TableLike,
    Formula,
    CodeBlock,
    Image,
    OrgChart,
    Diagram,
    Unknown,
}

#[allow(clippy::derivable_impls)]
impl Default for ChunkType {
    fn default() -> Self {
        Self::Unknown
    }
}

#[derive(Debug, Clone, Copy, serde::Serialize, serde::Deserialize, rustler::NifUnitEnum)]
pub enum ImageKind {
    Photograph,
    Diagram,
    Chart,
    Drawing,
    TextBlock,
    Decoration,
    Logo,
    Icon,
    TileFragment,
    Mask,
    PageRaster,
    Unknown,
}

#[allow(clippy::derivable_impls)]
impl Default for ImageKind {
    fn default() -> Self {
        Self::Photograph
    }
}

#[derive(Debug, Clone, Copy, serde::Serialize, serde::Deserialize, rustler::NifUnitEnum)]
pub enum ResultFormat {
    Unified,
    ElementBased,
}

#[allow(clippy::derivable_impls)]
impl Default for ResultFormat {
    fn default() -> Self {
        Self::Unified
    }
}

#[derive(Debug, Clone, Copy, serde::Serialize, serde::Deserialize, rustler::NifUnitEnum)]
pub enum ElementType {
    Title,
    NarrativeText,
    Heading,
    ListItem,
    Table,
    Image,
    PageBreak,
    CodeBlock,
    BlockQuote,
    Footer,
    Header,
}

#[allow(clippy::derivable_impls)]
impl Default for ElementType {
    fn default() -> Self {
        Self::Title
    }
}

#[derive(Debug, Clone, serde::Serialize, serde::Deserialize, rustler::NifStruct)]
#[module = "Kreuzberg.FormatMetadata"]
pub struct FormatMetadata {
    pub format_type: String,
    pub pdf: Option<PdfMetadata>,
    pub docx: Option<DocxMetadata>,
    pub excel: Option<ExcelMetadata>,
    pub email: Option<EmailMetadata>,
    pub pptx: Option<PptxMetadata>,
    pub archive: Option<ArchiveMetadata>,
    pub image: Option<ImageMetadata>,
    pub xml: Option<XmlMetadata>,
    pub text: Option<TextMetadata>,
    pub html: Option<HtmlMetadata>,
    pub ocr: Option<OcrMetadata>,
    pub csv: Option<CsvMetadata>,
    pub bibtex: Option<BibtexMetadata>,
    pub citation: Option<CitationMetadata>,
    pub fiction_book: Option<FictionBookMetadata>,
    pub dbf: Option<DbfMetadata>,
    pub jats: Option<JatsMetadata>,
    pub epub: Option<EpubMetadata>,
    pub pst: Option<PstMetadata>,
    pub code: Option<String>,
}

#[allow(clippy::derivable_impls)]
impl Default for FormatMetadata {
    fn default() -> Self {
        Self {
            format_type: String::new(),
            pdf: None,
            docx: None,
            excel: None,
            email: None,
            pptx: None,
            archive: None,
            image: None,
            xml: None,
            text: None,
            html: None,
            ocr: None,
            csv: None,
            bibtex: None,
            citation: None,
            fiction_book: None,
            dbf: None,
            jats: None,
            epub: None,
            pst: None,
            code: None,
        }
    }
}

#[derive(Debug, Clone, Copy, serde::Serialize, serde::Deserialize, rustler::NifUnitEnum)]
pub enum TextDirection {
    LeftToRight,
    RightToLeft,
    Auto,
}

#[allow(clippy::derivable_impls)]
impl Default for TextDirection {
    fn default() -> Self {
        Self::LeftToRight
    }
}

#[derive(Debug, Clone, Copy, serde::Serialize, serde::Deserialize, rustler::NifUnitEnum)]
pub enum LinkType {
    Anchor,
    Internal,
    External,
    Email,
    Phone,
    Other,
}

#[allow(clippy::derivable_impls)]
impl Default for LinkType {
    fn default() -> Self {
        Self::Anchor
    }
}

#[derive(Debug, Clone, Copy, serde::Serialize, serde::Deserialize, rustler::NifUnitEnum)]
pub enum ImageType {
    DataUri,
    InlineSvg,
    External,
    Relative,
}

#[allow(clippy::derivable_impls)]
impl Default for ImageType {
    fn default() -> Self {
        Self::DataUri
    }
}

#[derive(Debug, Clone, Copy, serde::Serialize, serde::Deserialize, rustler::NifUnitEnum)]
pub enum StructuredDataType {
    JsonLd,
    Microdata,
    RDFa,
}

#[allow(clippy::derivable_impls)]
impl Default for StructuredDataType {
    fn default() -> Self {
        Self::JsonLd
    }
}

#[derive(Debug, Clone, serde::Serialize, serde::Deserialize, rustler::NifTaggedEnum)]
#[serde(tag = "type", rename_all = "snake_case")]
pub enum OcrBoundingGeometry {
    Rectangle {
        left: u32,
        top: u32,
        width: u32,
        height: u32,
    },
    Quadrilateral {
        points: String,
    },
}

#[allow(clippy::derivable_impls)]
impl Default for OcrBoundingGeometry {
    fn default() -> Self {
        Self::Rectangle {
            left: Default::default(),
            top: Default::default(),
            width: Default::default(),
            height: Default::default(),
        }
    }
}

#[derive(Debug, Clone, Copy, serde::Serialize, serde::Deserialize, rustler::NifUnitEnum)]
pub enum OcrElementLevel {
    Word,
    Line,
    Block,
    Page,
}

#[allow(clippy::derivable_impls)]
impl Default for OcrElementLevel {
    fn default() -> Self {
        Self::Line
    }
}

#[derive(Debug, Clone, Copy, serde::Serialize, serde::Deserialize, rustler::NifUnitEnum)]
pub enum PageUnitType {
    Page,
    Slide,
    Sheet,
}

#[allow(clippy::derivable_impls)]
impl Default for PageUnitType {
    fn default() -> Self {
        Self::Page
    }
}

#[derive(Debug, Clone, serde::Serialize, serde::Deserialize, rustler::NifStruct)]
#[module = "Kreuzberg.DiffLine"]
pub struct DiffLine {
    pub kind: String,
    pub context: Option<String>,
    pub added: Option<String>,
    pub removed: Option<String>,
}

#[allow(clippy::derivable_impls)]
impl Default for DiffLine {
    fn default() -> Self {
        Self {
            kind: String::new(),
            context: None,
            added: None,
            removed: None,
        }
    }
}

#[derive(Debug, Clone, Copy, serde::Serialize, serde::Deserialize, rustler::NifUnitEnum)]
pub enum RevisionKind {
    Insertion,
    Deletion,
    FormatChange,
    Comment,
}

#[allow(clippy::derivable_impls)]
impl Default for RevisionKind {
    fn default() -> Self {
        Self::Insertion
    }
}

#[derive(Debug, Clone, serde::Serialize, serde::Deserialize, rustler::NifTaggedEnum)]
#[serde(tag = "type", rename_all = "snake_case")]
pub enum RevisionAnchor {
    Paragraph {
        index: usize,
    },
    TableCell {
        row: usize,
        col: usize,
        table_index: usize,
    },
    Page {
        index: usize,
    },
    Slide {
        index: usize,
    },
    Sheet {
        index: usize,
        #[serde(default)]
        name: Option<String>,
    },
}

#[allow(clippy::derivable_impls)]
impl Default for RevisionAnchor {
    fn default() -> Self {
        Self::Paragraph {
            index: Default::default(),
        }
    }
}

#[derive(Debug, Clone, Copy, serde::Serialize, serde::Deserialize, rustler::NifUnitEnum)]
pub enum UriKind {
    Hyperlink,
    Image,
    Anchor,
    Citation,
    Reference,
    Email,
}

#[allow(clippy::derivable_impls)]
impl Default for UriKind {
    fn default() -> Self {
        Self::Hyperlink
    }
}

#[derive(Debug, Clone, Copy, serde::Serialize, serde::Deserialize, rustler::NifUnitEnum)]
pub enum KeywordAlgorithm {
    Yake,
    Rake,
}

#[allow(clippy::derivable_impls)]
impl Default for KeywordAlgorithm {
    fn default() -> Self {
        Self::Yake
    }
}

#[derive(Debug, Clone, Copy, serde::Serialize, serde::Deserialize, rustler::NifUnitEnum)]
pub enum PSMMode {
    OsdOnly,
    AutoOsd,
    AutoOnly,
    Auto,
    SingleColumn,
    SingleBlockVertical,
    SingleBlock,
    SingleLine,
    SingleWord,
    CircleWord,
    SingleChar,
}

#[allow(clippy::derivable_impls)]
impl Default for PSMMode {
    fn default() -> Self {
        Self::OsdOnly
    }
}

#[derive(Debug, Clone, Copy, serde::Serialize, serde::Deserialize, rustler::NifUnitEnum)]
pub enum PaddleLanguage {
    English,
    Chinese,
    Japanese,
    Korean,
    German,
    French,
    Latin,
    Cyrillic,
    TraditionalChinese,
    Thai,
    Greek,
    EastSlavic,
    Arabic,
    Devanagari,
    Tamil,
    Telugu,
}

#[allow(clippy::derivable_impls)]
impl Default for PaddleLanguage {
    fn default() -> Self {
        Self::English
    }
}

#[derive(Debug, Clone, Copy, serde::Serialize, serde::Deserialize, rustler::NifUnitEnum)]
pub enum LayoutClass {
    Caption,
    Footnote,
    Formula,
    ListItem,
    PageFooter,
    PageHeader,
    Picture,
    SectionHeader,
    Table,
    Text,
    Title,
    DocumentIndex,
    Code,
    CheckboxSelected,
    CheckboxUnselected,
    Form,
    KeyValueRegion,
}

#[allow(clippy::derivable_impls)]
impl Default for LayoutClass {
    fn default() -> Self {
        Self::Caption
    }
}

static TRAIT_REPLY_COUNTER: AtomicU64 = AtomicU64::new(1);

static TRAIT_REPLY_CHANNELS: std::sync::LazyLock<
    Mutex<HashMap<u64, tokio::sync::oneshot::Sender<std::result::Result<String, String>>>>,
> = std::sync::LazyLock::new(|| Mutex::new(HashMap::new()));

/// Extract content from a byte array.
///
/// This is the main entry point for in-memory extraction. It performs the following steps:
/// 1. Validate MIME type
/// 2. Handle legacy format conversion if needed
/// 3. Select appropriate extractor from registry
/// 4. Extract content
/// 5. Run post-processing pipeline
///
/// # Arguments
///
/// * `content` - The byte array to extract
/// * `mime_type` - MIME type of the content
/// * `config` - Extraction configuration
///
/// # Returns
///
/// An `ExtractionResult` containing the extracted content and metadata.
///
/// # Errors
///
/// Returns `KreuzbergError::Validation` if MIME type is invalid.
/// Returns `KreuzbergError::UnsupportedFormat` if MIME type is not supported.
///
/// # Example
///
/// ```rust,no_run
/// use kreuzberg::core::extractor::extract_bytes;
/// use kreuzberg::core::config::ExtractionConfig;
///
/// let config = ExtractionConfig::default();
/// let bytes = b"Hello, world!";
/// let result = extract_bytes(bytes, "text/plain", &config).await?;
/// println!("Content: {}", result.content);
/// ```
#[rustler::nif(schedule = "DirtyCpu")]
pub fn extract_bytes_async(
    content: rustler::Binary,
    mime_type: String,
    config: Option<String>,
) -> Result<ExtractionResult, String> {
    let content: Vec<u8> = content.as_slice().to_vec();
    let config_core: Option<kreuzberg::ExtractionConfig> = config
        .map(|s| serde_json::from_str::<kreuzberg::ExtractionConfig>(&s))
        .transpose()
        .map_err(|e| e.to_string())?;
    let result = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
        std::thread::Builder::new()
            .stack_size(32 * 1024 * 1024)
            .spawn(move || {
                let rt = tokio::runtime::Runtime::new().map_err(|e| e.to_string())?;
                let result = rt
                    .block_on(async {
                        kreuzberg::extract_bytes(
                            &content,
                            &mime_type,
                            config_core.as_ref().unwrap_or(&Default::default()),
                        )
                        .await
                    })
                    .map_err(|e| e.to_string())?;
                Ok(result.into())
            })
            .map_err(|e| e.to_string())?
            .join()
            .map_err(|_| "thread panicked".to_string())
    }));
    match result {
        Ok(inner_result) => inner_result?,
        Err(_) => Err("thread panic during async operation".to_string()),
    }
}

/// Extract content from a file.
///
/// This is the main entry point for file-based extraction. It performs the following steps:
/// 1. Check cache for existing result (if caching enabled)
/// 2. Detect or validate MIME type
/// 3. Select appropriate extractor from registry
/// 4. Extract content
/// 5. Run post-processing pipeline
/// 6. Store result in cache (if caching enabled)
///
/// # Arguments
///
/// * `path` - Path to the file to extract
/// * `mime_type` - Optional MIME type override. If None, will be auto-detected
/// * `config` - Extraction configuration
///
/// # Returns
///
/// An `ExtractionResult` containing the extracted content and metadata.
///
/// # Errors
///
/// Returns `KreuzbergError::Io` if the file doesn't exist (NotFound) or for other file I/O errors.
/// Returns `KreuzbergError::UnsupportedFormat` if MIME type is not supported.
///
/// # Example
///
/// ```rust,no_run
/// use kreuzberg::core::extractor::extract_file;
/// use kreuzberg::core::config::ExtractionConfig;
///
/// let config = ExtractionConfig::default();
/// let result = extract_file("document.pdf", None, &config).await?;
/// println!("Content: {}", result.content);
/// ```
#[rustler::nif(schedule = "DirtyCpu")]
pub fn extract_file_async(
    path: String,
    mime_type: Option<String>,
    config: Option<String>,
) -> Result<ExtractionResult, String> {
    let config_core: Option<kreuzberg::ExtractionConfig> = config
        .map(|s| serde_json::from_str::<kreuzberg::ExtractionConfig>(&s))
        .transpose()
        .map_err(|e| e.to_string())?;
    let result = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
        std::thread::Builder::new()
            .stack_size(32 * 1024 * 1024)
            .spawn(move || {
                let rt = tokio::runtime::Runtime::new().map_err(|e| e.to_string())?;
                let result = rt
                    .block_on(async {
                        kreuzberg::extract_file(
                            std::path::PathBuf::from(path),
                            mime_type.as_deref(),
                            config_core.as_ref().unwrap_or(&Default::default()),
                        )
                        .await
                    })
                    .map_err(|e| e.to_string())?;
                Ok(result.into())
            })
            .map_err(|e| e.to_string())?
            .join()
            .map_err(|_| "thread panicked".to_string())
    }));
    match result {
        Ok(inner_result) => inner_result?,
        Err(_) => Err("thread panic during async operation".to_string()),
    }
}

/// Synchronous wrapper for `extract_file`.
///
/// This is a convenience function that blocks the current thread until extraction completes.
/// For async code, use `extract_file` directly.
///
/// Uses the global Tokio runtime for 100x+ performance improvement over creating
/// a new runtime per call. Always uses the global runtime to avoid nested runtime issues.
///
/// This function is only available with the `tokio-runtime` feature. For WASM targets,
/// use a truly synchronous extraction approach instead.
///
/// # Example
///
/// ```rust,no_run
/// use kreuzberg::core::extractor::extract_file_sync;
/// use kreuzberg::core::config::ExtractionConfig;
///
/// let config = ExtractionConfig::default();
/// let result = extract_file_sync("document.pdf", None, &config)?;
/// println!("Content: {}", result.content);
/// ```
#[rustler::nif]
pub fn extract_file_sync(
    path: String,
    mime_type: Option<String>,
    config: Option<String>,
) -> Result<ExtractionResult, String> {
    let config_core: Option<kreuzberg::ExtractionConfig> = config
        .map(|s| serde_json::from_str::<kreuzberg::ExtractionConfig>(&s))
        .transpose()
        .map_err(|e| e.to_string())?;
    let result = kreuzberg::extract_file_sync(
        std::path::PathBuf::from(path),
        mime_type.as_deref(),
        config_core.as_ref().unwrap_or(&Default::default()),
    )
    .map_err(|e| e.to_string())?;
    Ok(result.into())
}

/// Synchronous wrapper for `extract_bytes`.
///
/// Uses the global Tokio runtime for 100x+ performance improvement over creating
/// a new runtime per call.
///
/// With the `tokio-runtime` feature, this blocks the current thread using the global
/// Tokio runtime. Without it (WASM), this calls a truly synchronous implementation.
///
/// # Example
///
/// ```rust,no_run
/// use kreuzberg::core::extractor::extract_bytes_sync;
/// use kreuzberg::core::config::ExtractionConfig;
///
/// let config = ExtractionConfig::default();
/// let bytes = b"Hello, world!";
/// let result = extract_bytes_sync(bytes, "text/plain", &config)?;
/// println!("Content: {}", result.content);
/// ```
#[rustler::nif]
pub fn extract_bytes_sync(
    content: rustler::Binary,
    mime_type: String,
    config: Option<String>,
) -> Result<ExtractionResult, String> {
    let config_core: Option<kreuzberg::ExtractionConfig> = config
        .map(|s| serde_json::from_str::<kreuzberg::ExtractionConfig>(&s))
        .transpose()
        .map_err(|e| e.to_string())?;
    let result = kreuzberg::extract_bytes_sync(
        content.as_slice(),
        &mime_type,
        config_core.as_ref().unwrap_or(&Default::default()),
    )
    .map_err(|e| e.to_string())?;
    Ok(result.into())
}

/// Detect MIME type from raw file bytes.
///
/// Uses magic byte signatures to detect file type from content.
/// Falls back to `infer` crate for comprehensive detection.
///
/// For ZIP-based files, inspects contents to distinguish Office Open XML
/// formats (DOCX, XLSX, PPTX) from plain ZIP archives.
///
/// # Arguments
///
/// * `content` - Raw file bytes
///
/// # Returns
///
/// The detected MIME type string.
///
/// # Errors
///
/// Returns `KreuzbergError::UnsupportedFormat` if MIME type cannot be determined.
#[rustler::nif]
pub fn detect_mime_type_from_bytes(content: rustler::Binary) -> Result<String, String> {
    let result = kreuzberg::detect_mime_type_from_bytes(content.as_slice()).map_err(|e| e.to_string())?;
    Ok(result)
}

/// Get file extensions for a given MIME type.
///
/// Returns all known file extensions that map to the specified MIME type.
///
/// # Arguments
///
/// * `mime_type` - The MIME type to look up
///
/// # Returns
///
/// A vector of file extensions (without leading dot) for the MIME type.
///
/// # Example
///
/// ```
/// use kreuzberg::core::mime::get_extensions_for_mime;
///
/// let extensions = get_extensions_for_mime("application/pdf").unwrap();
/// assert_eq!(extensions, vec!["pdf"]);
///
/// let doc_extensions = get_extensions_for_mime("application/vnd.openxmlformats-officedocument.wordprocessingml.document").unwrap();
/// assert!(doc_extensions.contains(&"docx".to_string()));
/// ```
#[rustler::nif]
pub fn get_extensions_for_mime(mime_type: String) -> Result<Vec<String>, String> {
    let result = kreuzberg::get_extensions_for_mime(&mime_type).map_err(|e| e.to_string())?;
    Ok(result)
}

/// List the names of all registered embedding backends.
///
/// Used by `kreuzberg-cli`, the api/mcp endpoints, and generated language
/// bindings.
#[rustler::nif]
pub fn list_embedding_backends() -> Result<Vec<String>, String> {
    let result = kreuzberg::list_embedding_backends().map_err(|e| e.to_string())?;
    Ok(result)
}

/// List names of all registered document extractors.
#[rustler::nif]
pub fn list_document_extractors() -> Result<Vec<String>, String> {
    let result = kreuzberg::list_document_extractors().map_err(|e| e.to_string())?;
    Ok(result)
}

/// List all registered OCR backends.
///
/// Returns the names of all OCR backends currently registered in the global registry.
///
/// # Returns
///
/// A vector of OCR backend names.
///
/// # Example
///
/// ```rust
/// use kreuzberg::plugins::list_ocr_backends;
///
/// let backends = list_ocr_backends()?;
/// for name in backends {
///     println!("Registered OCR backend: {}", name);
/// }
/// ```
#[rustler::nif]
pub fn list_ocr_backends() -> Result<Vec<String>, String> {
    let result = kreuzberg::list_ocr_backends().map_err(|e| e.to_string())?;
    Ok(result)
}

/// List all registered post-processor names.
///
/// Returns a vector of all post-processor names currently registered in the
/// global registry.
///
/// # Returns
///
/// - `Ok(Vec<String>)` - Vector of post-processor names
/// - `Err(...)` if the registry lock is poisoned
///
/// # Example
///
/// ```rust
/// use kreuzberg::plugins::list_post_processors;
///
/// let processors = list_post_processors()?;
/// for name in processors {
///     println!("Registered post-processor: {}", name);
/// }
/// ```
#[rustler::nif]
pub fn list_post_processors() -> Result<Vec<String>, String> {
    let result = kreuzberg::list_post_processors().map_err(|e| e.to_string())?;
    Ok(result)
}

/// List names of all registered renderers.
///
/// # Errors
///
/// Returns an error if the registry lock is poisoned.
#[rustler::nif]
pub fn list_renderers() -> Result<Vec<String>, String> {
    let result = kreuzberg::list_renderers().map_err(|e| e.to_string())?;
    Ok(result)
}

/// List names of all registered validators.
#[rustler::nif]
pub fn list_validators() -> Result<Vec<String>, String> {
    let result = kreuzberg::list_validators().map_err(|e| e.to_string())?;
    Ok(result)
}

/// Compare two extraction results and return a structured diff.
///
/// The comparison is purely structural — no I/O, no side effects. All fields
/// of [`ExtractionDiff`] are populated according to the provided [`DiffOptions`].
///
/// # Arguments
///
/// * `a` — the "before" extraction result
/// * `b` — the "after" extraction result
/// * `opts` — controls which sections are compared and optional truncation
///
/// # Example
///
/// ```rust,no_run
/// use kreuzberg::{ExtractionResult, diff::{compare, DiffOptions}};
///
/// let mut a = ExtractionResult::default();
/// let mut b = ExtractionResult::default();
/// a.content = "Hello world".to_string();
/// b.content = "Hello Rust".to_string();
///
/// let diff = compare(&a, &b, &DiffOptions::default());
/// assert_eq!(diff.content_diff.len(), 1);
/// ```
#[rustler::nif]
pub fn compare(a: Option<String>, b: Option<String>, opts: Option<String>) -> ExtractionDiff {
    let a_core: Option<kreuzberg::ExtractionResult> =
        a.and_then(|s| serde_json::from_str::<kreuzberg::ExtractionResult>(&s).ok());
    let b_core: Option<kreuzberg::ExtractionResult> =
        b.and_then(|s| serde_json::from_str::<kreuzberg::ExtractionResult>(&s).ok());
    let opts_core: Option<kreuzberg::DiffOptions> =
        opts.and_then(|s| serde_json::from_str::<kreuzberg::DiffOptions>(&s).ok());
    kreuzberg::compare(
        a_core.as_ref().unwrap_or(&Default::default()),
        b_core.as_ref().unwrap_or(&Default::default()),
        opts_core.as_ref().unwrap_or(&Default::default()),
    )
    .into()
}

/// Generate embeddings asynchronously for a list of text strings.
///
/// This is the async counterpart to [`embed_texts`]. It offloads the blocking
/// ONNX inference work to a dedicated blocking thread pool via Tokio's
/// `spawn_blocking`, keeping the async executor free.
///
/// Returns one embedding vector per input text in the same order.
///
/// # Arguments
///
/// * `texts` - Vec of strings to embed (owned, sent to blocking thread)
/// * `config` - Embedding configuration specifying model, batch size, and normalization
///
/// # Errors
///
/// - `KreuzbergError::MissingDependency` if ONNX Runtime is not installed
/// - `KreuzbergError::Embedding` if the preset name is unknown, model download fails,
///   or the blocking inference task panics
///
/// # Example
///
/// ```rust,ignore
/// use kreuzberg::{embed_texts_async, EmbeddingConfig};
///
/// let embeddings = embed_texts_async(
///     vec!["Hello!".to_string()],
///     &EmbeddingConfig::default(),
/// ).await?;
/// ```
#[rustler::nif(schedule = "DirtyCpu")]
pub fn embed_texts_async(texts: Vec<String>, config: Option<String>) -> Result<Vec<Vec<f32>>, String> {
    let config_core: Option<kreuzberg::EmbeddingConfig> = config
        .map(|s| serde_json::from_str::<kreuzberg::EmbeddingConfig>(&s))
        .transpose()
        .map_err(|e| e.to_string())?;
    let result = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
        std::thread::Builder::new()
            .stack_size(32 * 1024 * 1024)
            .spawn(move || {
                let rt = tokio::runtime::Runtime::new().map_err(|e| e.to_string())?;
                let result = rt
                    .block_on(async {
                        kreuzberg::embed_texts_async(texts, config_core.as_ref().unwrap_or(&Default::default())).await
                    })
                    .map_err(|e| e.to_string())?;
                Ok(result)
            })
            .map_err(|e| e.to_string())?
            .join()
            .map_err(|_| "thread panicked".to_string())
    }));
    match result {
        Ok(inner_result) => inner_result?,
        Err(_) => Err("thread panic during async operation".to_string()),
    }
}

/// Render a single PDF page to PNG bytes.
///
/// Returns raw PNG-encoded bytes for the specified page at the given DPI.
/// Uses pdf_oxide with tiny-skia for pure-Rust rendering.
///
/// # Arguments
///
/// * `pdf_bytes` - Raw PDF file bytes
/// * `page_index` - Zero-based page index
/// * `dpi` - Resolution in dots per inch (default: 150)
/// * `password` - Optional password for encrypted PDFs
///
/// # Errors
///
/// Returns `KreuzbergError::Parsing` if the PDF cannot be opened, authenticated,
/// or rendered, or if `page_index` is out of range.
#[rustler::nif]
pub fn render_pdf_page_to_png(
    pdf_bytes: rustler::Binary,
    page_index: usize,
    dpi: Option<i32>,
    password: Option<String>,
) -> Result<Vec<u8>, String> {
    let result = kreuzberg::render_pdf_page_to_png(pdf_bytes.as_slice(), page_index, dpi, password.as_deref())
        .map_err(|e| e.to_string())?;
    Ok(result.to_vec())
}

/// Detect the MIME type of a file at the given path.
///
/// Uses the file extension and optionally the file content to determine the MIME type.
/// Set `check_exists` to `true` to verify the file exists before detection.
#[rustler::nif]
pub fn detect_mime_type(path: String, check_exists: bool) -> Result<String, String> {
    let result = kreuzberg::detect_mime_type(path, check_exists).map_err(|e| e.to_string())?;
    Ok(result)
}

/// Embed a list of texts using the configured embedding model.
///
/// Returns a 2D vector where each inner vector is the embedding for the corresponding text.
#[rustler::nif]
pub fn embed_texts(texts: Vec<String>, config: Option<String>) -> Result<Vec<Vec<f32>>, String> {
    let config_core: Option<kreuzberg::EmbeddingConfig> = config
        .map(|s| serde_json::from_str::<kreuzberg::EmbeddingConfig>(&s))
        .transpose()
        .map_err(|e| e.to_string())?;
    let result = kreuzberg::embed_texts(texts, config_core.as_ref().unwrap_or(&Default::default()))
        .map_err(|e| e.to_string())?;
    Ok(result)
}

/// Get an embedding preset by name.
///
/// Returns `None` if no preset with the given name exists. Returns an owned
/// clone so the value is safe to pass across FFI boundaries.
#[rustler::nif]
pub fn get_embedding_preset(name: String) -> Option<EmbeddingPreset> {
    kreuzberg::get_embedding_preset(&name).map(|v| v.into())
}

/// List the names of all available embedding presets.
///
/// Returns owned `String`s so the values are safe to pass across FFI boundaries.
#[rustler::nif]
pub fn list_embedding_presets() -> Vec<String> {
    kreuzberg::list_embedding_presets()
}

/// Wrapper that bridges a foreign Rustler object to the `OcrBackend` trait.
pub struct RustlerOcrBackendBridge {
    inner: rustler::LocalPid,
    cached_name: String,
}

impl std::fmt::Debug for RustlerOcrBackendBridge {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        write!(f, "RustlerOcrBackendBridge")
    }
}

impl RustlerOcrBackendBridge {
    /// Create a new bridge wrapping an Elixir GenServer PID.
    ///
    /// The PID is copied (LocalPid is Copy + Send + Sync) and used to send
    /// messages to the backing GenServer. The plugin_name is cached for fast
    /// Plugin::name() lookups.
    pub fn new(pid: rustler::LocalPid, plugin_name: String) -> Self {
        Self {
            inner: pid,
            cached_name: plugin_name,
        }
    }
}

impl kreuzberg::plugins::Plugin for RustlerOcrBackendBridge {
    fn name(&self) -> &str {
        &self.cached_name
    }

    fn version(&self) -> String {
        let reply_id = TRAIT_REPLY_COUNTER.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
        let (tx, rx) = tokio::sync::oneshot::channel::<Result<String, String>>();
        TRAIT_REPLY_CHANNELS.lock().unwrap().insert(reply_id, tx);

        let pid = self.inner;

        let args_json = {
            let args = serde_json::Map::new();
            serde_json::Value::Object(args).to_string()
        };

        let method = "version";

        // Check if we're already in a tokio runtime context. If not, create one.
        let _rt_guard = if tokio::runtime::Handle::try_current().is_err() {
            Some(tokio::runtime::Runtime::new().unwrap())
        } else {
            None
        };

        if let Some(ref rt) = _rt_guard {
            rt.spawn_blocking(move || {
                let mut env = rustler::OwnedEnv::new();
                let _ = env.send_and_clear(&pid, |env| {
                    (
                        rustler::types::atom::Atom::from_str(env, "trait_call").unwrap(),
                        method,
                        args_json.as_str(),
                        reply_id,
                    )
                        .encode(env)
                });
            });
        } else {
            tokio::task::spawn_blocking(move || {
                let mut env = rustler::OwnedEnv::new();
                let _ = env.send_and_clear(&pid, |env| {
                    (
                        rustler::types::atom::Atom::from_str(env, "trait_call").unwrap(),
                        method,
                        args_json.as_str(),
                        reply_id,
                    )
                        .encode(env)
                });
            });
        }

        match rx.blocking_recv() {
            Ok(Ok(json)) => serde_json::from_str(&json).unwrap_or_default(),
            _ => Default::default(),
        }
    }

    fn initialize(&self) -> std::result::Result<(), kreuzberg::KreuzbergError> {
        let reply_id = TRAIT_REPLY_COUNTER.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
        let (tx, rx) = tokio::sync::oneshot::channel::<Result<String, String>>();
        TRAIT_REPLY_CHANNELS.lock().unwrap().insert(reply_id, tx);

        let pid = self.inner;

        let args_json = {
            let args = serde_json::Map::new();
            serde_json::Value::Object(args).to_string()
        };

        let method = "initialize";

        // Check if we're already in a tokio runtime context. If not, create one.
        let _rt_guard = if tokio::runtime::Handle::try_current().is_err() {
            Some(tokio::runtime::Runtime::new().unwrap())
        } else {
            None
        };

        if let Some(ref rt) = _rt_guard {
            rt.spawn_blocking(move || {
                let mut env = rustler::OwnedEnv::new();
                let _ = env.send_and_clear(&pid, |env| {
                    (
                        rustler::types::atom::Atom::from_str(env, "trait_call").unwrap(),
                        method,
                        args_json.as_str(),
                        reply_id,
                    )
                        .encode(env)
                });
            });
        } else {
            tokio::task::spawn_blocking(move || {
                let mut env = rustler::OwnedEnv::new();
                let _ = env.send_and_clear(&pid, |env| {
                    (
                        rustler::types::atom::Atom::from_str(env, "trait_call").unwrap(),
                        method,
                        args_json.as_str(),
                        reply_id,
                    )
                        .encode(env)
                });
            });
        }

        match rx.blocking_recv() {
            Ok(Ok(json)) => serde_json::from_str(&json)
                .map_err(|_e| kreuzberg::KreuzbergError::Other(format!("Failed to deserialize response: {}", _e))),
            Ok(Err(msg)) => Err(kreuzberg::KreuzbergError::Other(msg)),
            Err(_) => Err(kreuzberg::KreuzbergError::Other(
                "Channel closed before reply received".to_string(),
            )),
        }
    }

    fn shutdown(&self) -> std::result::Result<(), kreuzberg::KreuzbergError> {
        let reply_id = TRAIT_REPLY_COUNTER.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
        let (tx, rx) = tokio::sync::oneshot::channel::<Result<String, String>>();
        TRAIT_REPLY_CHANNELS.lock().unwrap().insert(reply_id, tx);

        let pid = self.inner;

        let args_json = {
            let args = serde_json::Map::new();
            serde_json::Value::Object(args).to_string()
        };

        let method = "shutdown";

        // Check if we're already in a tokio runtime context. If not, create one.
        let _rt_guard = if tokio::runtime::Handle::try_current().is_err() {
            Some(tokio::runtime::Runtime::new().unwrap())
        } else {
            None
        };

        if let Some(ref rt) = _rt_guard {
            rt.spawn_blocking(move || {
                let mut env = rustler::OwnedEnv::new();
                let _ = env.send_and_clear(&pid, |env| {
                    (
                        rustler::types::atom::Atom::from_str(env, "trait_call").unwrap(),
                        method,
                        args_json.as_str(),
                        reply_id,
                    )
                        .encode(env)
                });
            });
        } else {
            tokio::task::spawn_blocking(move || {
                let mut env = rustler::OwnedEnv::new();
                let _ = env.send_and_clear(&pid, |env| {
                    (
                        rustler::types::atom::Atom::from_str(env, "trait_call").unwrap(),
                        method,
                        args_json.as_str(),
                        reply_id,
                    )
                        .encode(env)
                });
            });
        }

        match rx.blocking_recv() {
            Ok(Ok(json)) => serde_json::from_str(&json)
                .map_err(|_e| kreuzberg::KreuzbergError::Other(format!("Failed to deserialize response: {}", _e))),
            Ok(Err(msg)) => Err(kreuzberg::KreuzbergError::Other(msg)),
            Err(_) => Err(kreuzberg::KreuzbergError::Other(
                "Channel closed before reply received".to_string(),
            )),
        }
    }
}

#[async_trait::async_trait]
impl kreuzberg::OcrBackend for RustlerOcrBackendBridge {
    async fn process_image(
        &self,
        image_bytes: &[u8],
        config: &kreuzberg::OcrConfig,
    ) -> std::result::Result<kreuzberg::ExtractionResult, kreuzberg::KreuzbergError> {
        let image_bytes = image_bytes.to_owned();
        let config = config.to_owned();
        let reply_id = TRAIT_REPLY_COUNTER.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
        let (tx, rx) = tokio::sync::oneshot::channel::<Result<String, String>>();
        TRAIT_REPLY_CHANNELS.lock().unwrap().insert(reply_id, tx);

        let pid = self.inner;

        let args_json = {
            let mut args = serde_json::Map::new();
            args.insert(
                "image_bytes".to_string(),
                serde_json::Value::String(format!("{:?}", image_bytes)),
            );
            args.insert("config".to_string(), serde_json::Value::String(format!("{:?}", config)));
            serde_json::Value::Object(args).to_string()
        };

        let method = "process_image";

        let _ = tokio::task::spawn_blocking(move || {
            let mut env = rustler::OwnedEnv::new();
            let _ = env.send_and_clear(&pid, |env| {
                (
                    rustler::types::atom::Atom::from_str(env, "trait_call").unwrap(),
                    method,
                    args_json.as_str(),
                    reply_id,
                )
                    .encode(env)
            });
        })
        .await;

        match rx.await {
            Ok(Ok(json)) => serde_json::from_str(&json)
                .map_err(|_e| kreuzberg::KreuzbergError::Other(format!("Failed to deserialize response: {}", _e))),
            Ok(Err(msg)) => Err(kreuzberg::KreuzbergError::Other(msg)),
            Err(_) => Err(kreuzberg::KreuzbergError::Other(
                "Channel closed before reply received".to_string(),
            )),
        }
    }

    fn supports_language(&self, lang: &str) -> bool {
        let lang = lang.to_owned();
        let reply_id = TRAIT_REPLY_COUNTER.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
        let (tx, rx) = tokio::sync::oneshot::channel::<Result<String, String>>();
        TRAIT_REPLY_CHANNELS.lock().unwrap().insert(reply_id, tx);

        let pid = self.inner;

        let args_json = {
            let mut args = serde_json::Map::new();
            args.insert("lang".to_string(), serde_json::Value::String(lang.to_string()));
            serde_json::Value::Object(args).to_string()
        };

        let method = "supports_language";

        // Check if we're already in a tokio runtime context. If not, create one.
        let _rt_guard = if tokio::runtime::Handle::try_current().is_err() {
            Some(tokio::runtime::Runtime::new().unwrap())
        } else {
            None
        };

        if let Some(ref rt) = _rt_guard {
            rt.spawn_blocking(move || {
                let mut env = rustler::OwnedEnv::new();
                let _ = env.send_and_clear(&pid, |env| {
                    (
                        rustler::types::atom::Atom::from_str(env, "trait_call").unwrap(),
                        method,
                        args_json.as_str(),
                        reply_id,
                    )
                        .encode(env)
                });
            });
        } else {
            tokio::task::spawn_blocking(move || {
                let mut env = rustler::OwnedEnv::new();
                let _ = env.send_and_clear(&pid, |env| {
                    (
                        rustler::types::atom::Atom::from_str(env, "trait_call").unwrap(),
                        method,
                        args_json.as_str(),
                        reply_id,
                    )
                        .encode(env)
                });
            });
        }

        match rx.blocking_recv() {
            Ok(Ok(json)) => serde_json::from_str(&json).unwrap_or_default(),
            _ => Default::default(),
        }
    }

    fn backend_type(&self) -> kreuzberg::OcrBackendType {
        let reply_id = TRAIT_REPLY_COUNTER.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
        let (tx, rx) = tokio::sync::oneshot::channel::<Result<String, String>>();
        TRAIT_REPLY_CHANNELS.lock().unwrap().insert(reply_id, tx);

        let pid = self.inner;

        let args_json = {
            let args = serde_json::Map::new();
            serde_json::Value::Object(args).to_string()
        };

        let method = "backend_type";

        // Check if we're already in a tokio runtime context. If not, create one.
        let _rt_guard = if tokio::runtime::Handle::try_current().is_err() {
            Some(tokio::runtime::Runtime::new().unwrap())
        } else {
            None
        };

        if let Some(ref rt) = _rt_guard {
            rt.spawn_blocking(move || {
                let mut env = rustler::OwnedEnv::new();
                let _ = env.send_and_clear(&pid, |env| {
                    (
                        rustler::types::atom::Atom::from_str(env, "trait_call").unwrap(),
                        method,
                        args_json.as_str(),
                        reply_id,
                    )
                        .encode(env)
                });
            });
        } else {
            tokio::task::spawn_blocking(move || {
                let mut env = rustler::OwnedEnv::new();
                let _ = env.send_and_clear(&pid, |env| {
                    (
                        rustler::types::atom::Atom::from_str(env, "trait_call").unwrap(),
                        method,
                        args_json.as_str(),
                        reply_id,
                    )
                        .encode(env)
                });
            });
        }

        match rx.blocking_recv() {
            Ok(Ok(json)) => serde_json::from_str(&json).unwrap_or_default(),
            _ => Default::default(),
        }
    }
}

#[rustler::nif]
pub fn register_ocr_backend(
    env: rustler::Env<'_>,
    genserver_pid: rustler::LocalPid,
    plugin_name: String,
) -> rustler::Atom {
    let bridge = RustlerOcrBackendBridge::new(genserver_pid, plugin_name);
    let arc: Arc<dyn kreuzberg::OcrBackend> = Arc::new(bridge);

    let registry = kreuzberg::plugins::registry::get_ocr_backend_registry();
    match registry.write().register(arc) {
        Ok(_) => rustler::types::atom::Atom::from_str(env, "ok").unwrap(),
        Err(_) => rustler::types::atom::Atom::from_str(env, "error").unwrap(),
    }
}

#[rustler::nif]
pub fn unregister_ocr_backend(env: rustler::Env<'_>, name: String) -> rustler::Atom {
    match kreuzberg::plugins::ocr_backend::unregister_ocr_backend(&name) {
        Ok(_) => rustler::types::atom::Atom::from_str(env, "ok").unwrap(),
        Err(_) => rustler::types::atom::Atom::from_str(env, "error").unwrap(),
    }
}

#[rustler::nif]
pub fn clear_ocr_backends(env: rustler::Env<'_>) -> rustler::Atom {
    match kreuzberg::plugins::ocr_backend::clear_ocr_backends() {
        Ok(_) => rustler::types::atom::Atom::from_str(env, "ok").unwrap(),
        Err(_) => rustler::types::atom::Atom::from_str(env, "error").unwrap(),
    }
}

/// Wrapper that bridges a foreign Rustler object to the `PostProcessor` trait.
pub struct RustlerPostProcessorBridge {
    inner: rustler::LocalPid,
    cached_name: String,
}

impl std::fmt::Debug for RustlerPostProcessorBridge {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        write!(f, "RustlerPostProcessorBridge")
    }
}

impl RustlerPostProcessorBridge {
    /// Create a new bridge wrapping an Elixir GenServer PID.
    ///
    /// The PID is copied (LocalPid is Copy + Send + Sync) and used to send
    /// messages to the backing GenServer. The plugin_name is cached for fast
    /// Plugin::name() lookups.
    pub fn new(pid: rustler::LocalPid, plugin_name: String) -> Self {
        Self {
            inner: pid,
            cached_name: plugin_name,
        }
    }
}

impl kreuzberg::plugins::Plugin for RustlerPostProcessorBridge {
    fn name(&self) -> &str {
        &self.cached_name
    }

    fn version(&self) -> String {
        let reply_id = TRAIT_REPLY_COUNTER.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
        let (tx, rx) = tokio::sync::oneshot::channel::<Result<String, String>>();
        TRAIT_REPLY_CHANNELS.lock().unwrap().insert(reply_id, tx);

        let pid = self.inner;

        let args_json = {
            let args = serde_json::Map::new();
            serde_json::Value::Object(args).to_string()
        };

        let method = "version";

        // Check if we're already in a tokio runtime context. If not, create one.
        let _rt_guard = if tokio::runtime::Handle::try_current().is_err() {
            Some(tokio::runtime::Runtime::new().unwrap())
        } else {
            None
        };

        if let Some(ref rt) = _rt_guard {
            rt.spawn_blocking(move || {
                let mut env = rustler::OwnedEnv::new();
                let _ = env.send_and_clear(&pid, |env| {
                    (
                        rustler::types::atom::Atom::from_str(env, "trait_call").unwrap(),
                        method,
                        args_json.as_str(),
                        reply_id,
                    )
                        .encode(env)
                });
            });
        } else {
            tokio::task::spawn_blocking(move || {
                let mut env = rustler::OwnedEnv::new();
                let _ = env.send_and_clear(&pid, |env| {
                    (
                        rustler::types::atom::Atom::from_str(env, "trait_call").unwrap(),
                        method,
                        args_json.as_str(),
                        reply_id,
                    )
                        .encode(env)
                });
            });
        }

        match rx.blocking_recv() {
            Ok(Ok(json)) => serde_json::from_str(&json).unwrap_or_default(),
            _ => Default::default(),
        }
    }

    fn initialize(&self) -> std::result::Result<(), kreuzberg::KreuzbergError> {
        let reply_id = TRAIT_REPLY_COUNTER.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
        let (tx, rx) = tokio::sync::oneshot::channel::<Result<String, String>>();
        TRAIT_REPLY_CHANNELS.lock().unwrap().insert(reply_id, tx);

        let pid = self.inner;

        let args_json = {
            let args = serde_json::Map::new();
            serde_json::Value::Object(args).to_string()
        };

        let method = "initialize";

        // Check if we're already in a tokio runtime context. If not, create one.
        let _rt_guard = if tokio::runtime::Handle::try_current().is_err() {
            Some(tokio::runtime::Runtime::new().unwrap())
        } else {
            None
        };

        if let Some(ref rt) = _rt_guard {
            rt.spawn_blocking(move || {
                let mut env = rustler::OwnedEnv::new();
                let _ = env.send_and_clear(&pid, |env| {
                    (
                        rustler::types::atom::Atom::from_str(env, "trait_call").unwrap(),
                        method,
                        args_json.as_str(),
                        reply_id,
                    )
                        .encode(env)
                });
            });
        } else {
            tokio::task::spawn_blocking(move || {
                let mut env = rustler::OwnedEnv::new();
                let _ = env.send_and_clear(&pid, |env| {
                    (
                        rustler::types::atom::Atom::from_str(env, "trait_call").unwrap(),
                        method,
                        args_json.as_str(),
                        reply_id,
                    )
                        .encode(env)
                });
            });
        }

        match rx.blocking_recv() {
            Ok(Ok(json)) => serde_json::from_str(&json)
                .map_err(|_e| kreuzberg::KreuzbergError::Other(format!("Failed to deserialize response: {}", _e))),
            Ok(Err(msg)) => Err(kreuzberg::KreuzbergError::Other(msg)),
            Err(_) => Err(kreuzberg::KreuzbergError::Other(
                "Channel closed before reply received".to_string(),
            )),
        }
    }

    fn shutdown(&self) -> std::result::Result<(), kreuzberg::KreuzbergError> {
        let reply_id = TRAIT_REPLY_COUNTER.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
        let (tx, rx) = tokio::sync::oneshot::channel::<Result<String, String>>();
        TRAIT_REPLY_CHANNELS.lock().unwrap().insert(reply_id, tx);

        let pid = self.inner;

        let args_json = {
            let args = serde_json::Map::new();
            serde_json::Value::Object(args).to_string()
        };

        let method = "shutdown";

        // Check if we're already in a tokio runtime context. If not, create one.
        let _rt_guard = if tokio::runtime::Handle::try_current().is_err() {
            Some(tokio::runtime::Runtime::new().unwrap())
        } else {
            None
        };

        if let Some(ref rt) = _rt_guard {
            rt.spawn_blocking(move || {
                let mut env = rustler::OwnedEnv::new();
                let _ = env.send_and_clear(&pid, |env| {
                    (
                        rustler::types::atom::Atom::from_str(env, "trait_call").unwrap(),
                        method,
                        args_json.as_str(),
                        reply_id,
                    )
                        .encode(env)
                });
            });
        } else {
            tokio::task::spawn_blocking(move || {
                let mut env = rustler::OwnedEnv::new();
                let _ = env.send_and_clear(&pid, |env| {
                    (
                        rustler::types::atom::Atom::from_str(env, "trait_call").unwrap(),
                        method,
                        args_json.as_str(),
                        reply_id,
                    )
                        .encode(env)
                });
            });
        }

        match rx.blocking_recv() {
            Ok(Ok(json)) => serde_json::from_str(&json)
                .map_err(|_e| kreuzberg::KreuzbergError::Other(format!("Failed to deserialize response: {}", _e))),
            Ok(Err(msg)) => Err(kreuzberg::KreuzbergError::Other(msg)),
            Err(_) => Err(kreuzberg::KreuzbergError::Other(
                "Channel closed before reply received".to_string(),
            )),
        }
    }
}

#[async_trait::async_trait]
impl kreuzberg::PostProcessor for RustlerPostProcessorBridge {
    async fn process(
        &self,
        result: &mut kreuzberg::ExtractionResult,
        config: &kreuzberg::ExtractionConfig,
    ) -> std::result::Result<(), kreuzberg::KreuzbergError> {
        let result = result.to_owned();
        let config = config.to_owned();
        let reply_id = TRAIT_REPLY_COUNTER.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
        let (tx, rx) = tokio::sync::oneshot::channel::<Result<String, String>>();
        TRAIT_REPLY_CHANNELS.lock().unwrap().insert(reply_id, tx);

        let pid = self.inner;

        let args_json = {
            let mut args = serde_json::Map::new();
            args.insert("result".to_string(), serde_json::Value::String(format!("{:?}", result)));
            args.insert("config".to_string(), serde_json::Value::String(format!("{:?}", config)));
            serde_json::Value::Object(args).to_string()
        };

        let method = "process";

        let _ = tokio::task::spawn_blocking(move || {
            let mut env = rustler::OwnedEnv::new();
            let _ = env.send_and_clear(&pid, |env| {
                (
                    rustler::types::atom::Atom::from_str(env, "trait_call").unwrap(),
                    method,
                    args_json.as_str(),
                    reply_id,
                )
                    .encode(env)
            });
        })
        .await;

        match rx.await {
            Ok(Ok(json)) => serde_json::from_str(&json)
                .map_err(|_e| kreuzberg::KreuzbergError::Other(format!("Failed to deserialize response: {}", _e))),
            Ok(Err(msg)) => Err(kreuzberg::KreuzbergError::Other(msg)),
            Err(_) => Err(kreuzberg::KreuzbergError::Other(
                "Channel closed before reply received".to_string(),
            )),
        }
    }

    fn processing_stage(&self) -> kreuzberg::ProcessingStage {
        let reply_id = TRAIT_REPLY_COUNTER.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
        let (tx, rx) = tokio::sync::oneshot::channel::<Result<String, String>>();
        TRAIT_REPLY_CHANNELS.lock().unwrap().insert(reply_id, tx);

        let pid = self.inner;

        let args_json = {
            let args = serde_json::Map::new();
            serde_json::Value::Object(args).to_string()
        };

        let method = "processing_stage";

        // Check if we're already in a tokio runtime context. If not, create one.
        let _rt_guard = if tokio::runtime::Handle::try_current().is_err() {
            Some(tokio::runtime::Runtime::new().unwrap())
        } else {
            None
        };

        if let Some(ref rt) = _rt_guard {
            rt.spawn_blocking(move || {
                let mut env = rustler::OwnedEnv::new();
                let _ = env.send_and_clear(&pid, |env| {
                    (
                        rustler::types::atom::Atom::from_str(env, "trait_call").unwrap(),
                        method,
                        args_json.as_str(),
                        reply_id,
                    )
                        .encode(env)
                });
            });
        } else {
            tokio::task::spawn_blocking(move || {
                let mut env = rustler::OwnedEnv::new();
                let _ = env.send_and_clear(&pid, |env| {
                    (
                        rustler::types::atom::Atom::from_str(env, "trait_call").unwrap(),
                        method,
                        args_json.as_str(),
                        reply_id,
                    )
                        .encode(env)
                });
            });
        }

        match rx.blocking_recv() {
            Ok(Ok(json)) => serde_json::from_str(&json).unwrap_or_default(),
            _ => Default::default(),
        }
    }
}

#[rustler::nif]
pub fn register_post_processor(
    env: rustler::Env<'_>,
    genserver_pid: rustler::LocalPid,
    plugin_name: String,
) -> rustler::Atom {
    let bridge = RustlerPostProcessorBridge::new(genserver_pid, plugin_name);
    let arc: Arc<dyn kreuzberg::PostProcessor> = Arc::new(bridge);

    let registry = kreuzberg::plugins::registry::get_post_processor_registry();
    match registry.write().register(arc) {
        Ok(_) => rustler::types::atom::Atom::from_str(env, "ok").unwrap(),
        Err(_) => rustler::types::atom::Atom::from_str(env, "error").unwrap(),
    }
}

#[rustler::nif]
pub fn unregister_post_processor(env: rustler::Env<'_>, name: String) -> rustler::Atom {
    match kreuzberg::plugins::post_processor::unregister_post_processor(&name) {
        Ok(_) => rustler::types::atom::Atom::from_str(env, "ok").unwrap(),
        Err(_) => rustler::types::atom::Atom::from_str(env, "error").unwrap(),
    }
}

#[rustler::nif]
pub fn clear_post_processors(env: rustler::Env<'_>) -> rustler::Atom {
    match kreuzberg::plugins::post_processor::clear_post_processors() {
        Ok(_) => rustler::types::atom::Atom::from_str(env, "ok").unwrap(),
        Err(_) => rustler::types::atom::Atom::from_str(env, "error").unwrap(),
    }
}

/// Wrapper that bridges a foreign Rustler object to the `Validator` trait.
pub struct RustlerValidatorBridge {
    inner: rustler::LocalPid,
    cached_name: String,
}

impl std::fmt::Debug for RustlerValidatorBridge {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        write!(f, "RustlerValidatorBridge")
    }
}

impl RustlerValidatorBridge {
    /// Create a new bridge wrapping an Elixir GenServer PID.
    ///
    /// The PID is copied (LocalPid is Copy + Send + Sync) and used to send
    /// messages to the backing GenServer. The plugin_name is cached for fast
    /// Plugin::name() lookups.
    pub fn new(pid: rustler::LocalPid, plugin_name: String) -> Self {
        Self {
            inner: pid,
            cached_name: plugin_name,
        }
    }
}

impl kreuzberg::plugins::Plugin for RustlerValidatorBridge {
    fn name(&self) -> &str {
        &self.cached_name
    }

    fn version(&self) -> String {
        let reply_id = TRAIT_REPLY_COUNTER.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
        let (tx, rx) = tokio::sync::oneshot::channel::<Result<String, String>>();
        TRAIT_REPLY_CHANNELS.lock().unwrap().insert(reply_id, tx);

        let pid = self.inner;

        let args_json = {
            let args = serde_json::Map::new();
            serde_json::Value::Object(args).to_string()
        };

        let method = "version";

        // Check if we're already in a tokio runtime context. If not, create one.
        let _rt_guard = if tokio::runtime::Handle::try_current().is_err() {
            Some(tokio::runtime::Runtime::new().unwrap())
        } else {
            None
        };

        if let Some(ref rt) = _rt_guard {
            rt.spawn_blocking(move || {
                let mut env = rustler::OwnedEnv::new();
                let _ = env.send_and_clear(&pid, |env| {
                    (
                        rustler::types::atom::Atom::from_str(env, "trait_call").unwrap(),
                        method,
                        args_json.as_str(),
                        reply_id,
                    )
                        .encode(env)
                });
            });
        } else {
            tokio::task::spawn_blocking(move || {
                let mut env = rustler::OwnedEnv::new();
                let _ = env.send_and_clear(&pid, |env| {
                    (
                        rustler::types::atom::Atom::from_str(env, "trait_call").unwrap(),
                        method,
                        args_json.as_str(),
                        reply_id,
                    )
                        .encode(env)
                });
            });
        }

        match rx.blocking_recv() {
            Ok(Ok(json)) => serde_json::from_str(&json).unwrap_or_default(),
            _ => Default::default(),
        }
    }

    fn initialize(&self) -> std::result::Result<(), kreuzberg::KreuzbergError> {
        let reply_id = TRAIT_REPLY_COUNTER.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
        let (tx, rx) = tokio::sync::oneshot::channel::<Result<String, String>>();
        TRAIT_REPLY_CHANNELS.lock().unwrap().insert(reply_id, tx);

        let pid = self.inner;

        let args_json = {
            let args = serde_json::Map::new();
            serde_json::Value::Object(args).to_string()
        };

        let method = "initialize";

        // Check if we're already in a tokio runtime context. If not, create one.
        let _rt_guard = if tokio::runtime::Handle::try_current().is_err() {
            Some(tokio::runtime::Runtime::new().unwrap())
        } else {
            None
        };

        if let Some(ref rt) = _rt_guard {
            rt.spawn_blocking(move || {
                let mut env = rustler::OwnedEnv::new();
                let _ = env.send_and_clear(&pid, |env| {
                    (
                        rustler::types::atom::Atom::from_str(env, "trait_call").unwrap(),
                        method,
                        args_json.as_str(),
                        reply_id,
                    )
                        .encode(env)
                });
            });
        } else {
            tokio::task::spawn_blocking(move || {
                let mut env = rustler::OwnedEnv::new();
                let _ = env.send_and_clear(&pid, |env| {
                    (
                        rustler::types::atom::Atom::from_str(env, "trait_call").unwrap(),
                        method,
                        args_json.as_str(),
                        reply_id,
                    )
                        .encode(env)
                });
            });
        }

        match rx.blocking_recv() {
            Ok(Ok(json)) => serde_json::from_str(&json)
                .map_err(|_e| kreuzberg::KreuzbergError::Other(format!("Failed to deserialize response: {}", _e))),
            Ok(Err(msg)) => Err(kreuzberg::KreuzbergError::Other(msg)),
            Err(_) => Err(kreuzberg::KreuzbergError::Other(
                "Channel closed before reply received".to_string(),
            )),
        }
    }

    fn shutdown(&self) -> std::result::Result<(), kreuzberg::KreuzbergError> {
        let reply_id = TRAIT_REPLY_COUNTER.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
        let (tx, rx) = tokio::sync::oneshot::channel::<Result<String, String>>();
        TRAIT_REPLY_CHANNELS.lock().unwrap().insert(reply_id, tx);

        let pid = self.inner;

        let args_json = {
            let args = serde_json::Map::new();
            serde_json::Value::Object(args).to_string()
        };

        let method = "shutdown";

        // Check if we're already in a tokio runtime context. If not, create one.
        let _rt_guard = if tokio::runtime::Handle::try_current().is_err() {
            Some(tokio::runtime::Runtime::new().unwrap())
        } else {
            None
        };

        if let Some(ref rt) = _rt_guard {
            rt.spawn_blocking(move || {
                let mut env = rustler::OwnedEnv::new();
                let _ = env.send_and_clear(&pid, |env| {
                    (
                        rustler::types::atom::Atom::from_str(env, "trait_call").unwrap(),
                        method,
                        args_json.as_str(),
                        reply_id,
                    )
                        .encode(env)
                });
            });
        } else {
            tokio::task::spawn_blocking(move || {
                let mut env = rustler::OwnedEnv::new();
                let _ = env.send_and_clear(&pid, |env| {
                    (
                        rustler::types::atom::Atom::from_str(env, "trait_call").unwrap(),
                        method,
                        args_json.as_str(),
                        reply_id,
                    )
                        .encode(env)
                });
            });
        }

        match rx.blocking_recv() {
            Ok(Ok(json)) => serde_json::from_str(&json)
                .map_err(|_e| kreuzberg::KreuzbergError::Other(format!("Failed to deserialize response: {}", _e))),
            Ok(Err(msg)) => Err(kreuzberg::KreuzbergError::Other(msg)),
            Err(_) => Err(kreuzberg::KreuzbergError::Other(
                "Channel closed before reply received".to_string(),
            )),
        }
    }
}

#[async_trait::async_trait]
impl kreuzberg::Validator for RustlerValidatorBridge {
    async fn validate(
        &self,
        result: &kreuzberg::ExtractionResult,
        config: &kreuzberg::ExtractionConfig,
    ) -> std::result::Result<(), kreuzberg::KreuzbergError> {
        let result = result.to_owned();
        let config = config.to_owned();
        let reply_id = TRAIT_REPLY_COUNTER.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
        let (tx, rx) = tokio::sync::oneshot::channel::<Result<String, String>>();
        TRAIT_REPLY_CHANNELS.lock().unwrap().insert(reply_id, tx);

        let pid = self.inner;

        let args_json = {
            let mut args = serde_json::Map::new();
            args.insert("result".to_string(), serde_json::Value::String(format!("{:?}", result)));
            args.insert("config".to_string(), serde_json::Value::String(format!("{:?}", config)));
            serde_json::Value::Object(args).to_string()
        };

        let method = "validate";

        let _ = tokio::task::spawn_blocking(move || {
            let mut env = rustler::OwnedEnv::new();
            let _ = env.send_and_clear(&pid, |env| {
                (
                    rustler::types::atom::Atom::from_str(env, "trait_call").unwrap(),
                    method,
                    args_json.as_str(),
                    reply_id,
                )
                    .encode(env)
            });
        })
        .await;

        match rx.await {
            Ok(Ok(json)) => serde_json::from_str(&json)
                .map_err(|_e| kreuzberg::KreuzbergError::Other(format!("Failed to deserialize response: {}", _e))),
            Ok(Err(msg)) => Err(kreuzberg::KreuzbergError::Other(msg)),
            Err(_) => Err(kreuzberg::KreuzbergError::Other(
                "Channel closed before reply received".to_string(),
            )),
        }
    }
}

#[rustler::nif]
pub fn register_validator(
    env: rustler::Env<'_>,
    genserver_pid: rustler::LocalPid,
    plugin_name: String,
) -> rustler::Atom {
    let bridge = RustlerValidatorBridge::new(genserver_pid, plugin_name);
    let arc: Arc<dyn kreuzberg::Validator> = Arc::new(bridge);

    let registry = kreuzberg::plugins::registry::get_validator_registry();
    match registry.write().register(arc) {
        Ok(_) => rustler::types::atom::Atom::from_str(env, "ok").unwrap(),
        Err(_) => rustler::types::atom::Atom::from_str(env, "error").unwrap(),
    }
}

#[rustler::nif]
pub fn unregister_validator(env: rustler::Env<'_>, name: String) -> rustler::Atom {
    match kreuzberg::plugins::validator::unregister_validator(&name) {
        Ok(_) => rustler::types::atom::Atom::from_str(env, "ok").unwrap(),
        Err(_) => rustler::types::atom::Atom::from_str(env, "error").unwrap(),
    }
}

#[rustler::nif]
pub fn clear_validators(env: rustler::Env<'_>) -> rustler::Atom {
    match kreuzberg::plugins::validator::clear_validators() {
        Ok(_) => rustler::types::atom::Atom::from_str(env, "ok").unwrap(),
        Err(_) => rustler::types::atom::Atom::from_str(env, "error").unwrap(),
    }
}

/// Wrapper that bridges a foreign Rustler object to the `EmbeddingBackend` trait.
pub struct RustlerEmbeddingBackendBridge {
    inner: rustler::LocalPid,
    cached_name: String,
}

impl std::fmt::Debug for RustlerEmbeddingBackendBridge {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        write!(f, "RustlerEmbeddingBackendBridge")
    }
}

impl RustlerEmbeddingBackendBridge {
    /// Create a new bridge wrapping an Elixir GenServer PID.
    ///
    /// The PID is copied (LocalPid is Copy + Send + Sync) and used to send
    /// messages to the backing GenServer. The plugin_name is cached for fast
    /// Plugin::name() lookups.
    pub fn new(pid: rustler::LocalPid, plugin_name: String) -> Self {
        Self {
            inner: pid,
            cached_name: plugin_name,
        }
    }
}

impl kreuzberg::plugins::Plugin for RustlerEmbeddingBackendBridge {
    fn name(&self) -> &str {
        &self.cached_name
    }

    fn version(&self) -> String {
        let reply_id = TRAIT_REPLY_COUNTER.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
        let (tx, rx) = tokio::sync::oneshot::channel::<Result<String, String>>();
        TRAIT_REPLY_CHANNELS.lock().unwrap().insert(reply_id, tx);

        let pid = self.inner;

        let args_json = {
            let args = serde_json::Map::new();
            serde_json::Value::Object(args).to_string()
        };

        let method = "version";

        // Check if we're already in a tokio runtime context. If not, create one.
        let _rt_guard = if tokio::runtime::Handle::try_current().is_err() {
            Some(tokio::runtime::Runtime::new().unwrap())
        } else {
            None
        };

        if let Some(ref rt) = _rt_guard {
            rt.spawn_blocking(move || {
                let mut env = rustler::OwnedEnv::new();
                let _ = env.send_and_clear(&pid, |env| {
                    (
                        rustler::types::atom::Atom::from_str(env, "trait_call").unwrap(),
                        method,
                        args_json.as_str(),
                        reply_id,
                    )
                        .encode(env)
                });
            });
        } else {
            tokio::task::spawn_blocking(move || {
                let mut env = rustler::OwnedEnv::new();
                let _ = env.send_and_clear(&pid, |env| {
                    (
                        rustler::types::atom::Atom::from_str(env, "trait_call").unwrap(),
                        method,
                        args_json.as_str(),
                        reply_id,
                    )
                        .encode(env)
                });
            });
        }

        match rx.blocking_recv() {
            Ok(Ok(json)) => serde_json::from_str(&json).unwrap_or_default(),
            _ => Default::default(),
        }
    }

    fn initialize(&self) -> std::result::Result<(), kreuzberg::KreuzbergError> {
        let reply_id = TRAIT_REPLY_COUNTER.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
        let (tx, rx) = tokio::sync::oneshot::channel::<Result<String, String>>();
        TRAIT_REPLY_CHANNELS.lock().unwrap().insert(reply_id, tx);

        let pid = self.inner;

        let args_json = {
            let args = serde_json::Map::new();
            serde_json::Value::Object(args).to_string()
        };

        let method = "initialize";

        // Check if we're already in a tokio runtime context. If not, create one.
        let _rt_guard = if tokio::runtime::Handle::try_current().is_err() {
            Some(tokio::runtime::Runtime::new().unwrap())
        } else {
            None
        };

        if let Some(ref rt) = _rt_guard {
            rt.spawn_blocking(move || {
                let mut env = rustler::OwnedEnv::new();
                let _ = env.send_and_clear(&pid, |env| {
                    (
                        rustler::types::atom::Atom::from_str(env, "trait_call").unwrap(),
                        method,
                        args_json.as_str(),
                        reply_id,
                    )
                        .encode(env)
                });
            });
        } else {
            tokio::task::spawn_blocking(move || {
                let mut env = rustler::OwnedEnv::new();
                let _ = env.send_and_clear(&pid, |env| {
                    (
                        rustler::types::atom::Atom::from_str(env, "trait_call").unwrap(),
                        method,
                        args_json.as_str(),
                        reply_id,
                    )
                        .encode(env)
                });
            });
        }

        match rx.blocking_recv() {
            Ok(Ok(json)) => serde_json::from_str(&json)
                .map_err(|_e| kreuzberg::KreuzbergError::Other(format!("Failed to deserialize response: {}", _e))),
            Ok(Err(msg)) => Err(kreuzberg::KreuzbergError::Other(msg)),
            Err(_) => Err(kreuzberg::KreuzbergError::Other(
                "Channel closed before reply received".to_string(),
            )),
        }
    }

    fn shutdown(&self) -> std::result::Result<(), kreuzberg::KreuzbergError> {
        let reply_id = TRAIT_REPLY_COUNTER.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
        let (tx, rx) = tokio::sync::oneshot::channel::<Result<String, String>>();
        TRAIT_REPLY_CHANNELS.lock().unwrap().insert(reply_id, tx);

        let pid = self.inner;

        let args_json = {
            let args = serde_json::Map::new();
            serde_json::Value::Object(args).to_string()
        };

        let method = "shutdown";

        // Check if we're already in a tokio runtime context. If not, create one.
        let _rt_guard = if tokio::runtime::Handle::try_current().is_err() {
            Some(tokio::runtime::Runtime::new().unwrap())
        } else {
            None
        };

        if let Some(ref rt) = _rt_guard {
            rt.spawn_blocking(move || {
                let mut env = rustler::OwnedEnv::new();
                let _ = env.send_and_clear(&pid, |env| {
                    (
                        rustler::types::atom::Atom::from_str(env, "trait_call").unwrap(),
                        method,
                        args_json.as_str(),
                        reply_id,
                    )
                        .encode(env)
                });
            });
        } else {
            tokio::task::spawn_blocking(move || {
                let mut env = rustler::OwnedEnv::new();
                let _ = env.send_and_clear(&pid, |env| {
                    (
                        rustler::types::atom::Atom::from_str(env, "trait_call").unwrap(),
                        method,
                        args_json.as_str(),
                        reply_id,
                    )
                        .encode(env)
                });
            });
        }

        match rx.blocking_recv() {
            Ok(Ok(json)) => serde_json::from_str(&json)
                .map_err(|_e| kreuzberg::KreuzbergError::Other(format!("Failed to deserialize response: {}", _e))),
            Ok(Err(msg)) => Err(kreuzberg::KreuzbergError::Other(msg)),
            Err(_) => Err(kreuzberg::KreuzbergError::Other(
                "Channel closed before reply received".to_string(),
            )),
        }
    }
}

#[async_trait::async_trait]
impl kreuzberg::EmbeddingBackend for RustlerEmbeddingBackendBridge {
    fn dimensions(&self) -> usize {
        let reply_id = TRAIT_REPLY_COUNTER.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
        let (tx, rx) = tokio::sync::oneshot::channel::<Result<String, String>>();
        TRAIT_REPLY_CHANNELS.lock().unwrap().insert(reply_id, tx);

        let pid = self.inner;

        let args_json = {
            let args = serde_json::Map::new();
            serde_json::Value::Object(args).to_string()
        };

        let method = "dimensions";

        // Check if we're already in a tokio runtime context. If not, create one.
        let _rt_guard = if tokio::runtime::Handle::try_current().is_err() {
            Some(tokio::runtime::Runtime::new().unwrap())
        } else {
            None
        };

        if let Some(ref rt) = _rt_guard {
            rt.spawn_blocking(move || {
                let mut env = rustler::OwnedEnv::new();
                let _ = env.send_and_clear(&pid, |env| {
                    (
                        rustler::types::atom::Atom::from_str(env, "trait_call").unwrap(),
                        method,
                        args_json.as_str(),
                        reply_id,
                    )
                        .encode(env)
                });
            });
        } else {
            tokio::task::spawn_blocking(move || {
                let mut env = rustler::OwnedEnv::new();
                let _ = env.send_and_clear(&pid, |env| {
                    (
                        rustler::types::atom::Atom::from_str(env, "trait_call").unwrap(),
                        method,
                        args_json.as_str(),
                        reply_id,
                    )
                        .encode(env)
                });
            });
        }

        match rx.blocking_recv() {
            Ok(Ok(json)) => serde_json::from_str(&json).unwrap_or_default(),
            _ => Default::default(),
        }
    }

    async fn embed(&self, texts: Vec<String>) -> std::result::Result<Vec<Vec<f32>>, kreuzberg::KreuzbergError> {
        let reply_id = TRAIT_REPLY_COUNTER.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
        let (tx, rx) = tokio::sync::oneshot::channel::<Result<String, String>>();
        TRAIT_REPLY_CHANNELS.lock().unwrap().insert(reply_id, tx);

        let pid = self.inner;

        let args_json = {
            let mut args = serde_json::Map::new();
            args.insert("texts".to_string(), serde_json::Value::String(format!("{:?}", texts)));
            serde_json::Value::Object(args).to_string()
        };

        let method = "embed";

        let _ = tokio::task::spawn_blocking(move || {
            let mut env = rustler::OwnedEnv::new();
            let _ = env.send_and_clear(&pid, |env| {
                (
                    rustler::types::atom::Atom::from_str(env, "trait_call").unwrap(),
                    method,
                    args_json.as_str(),
                    reply_id,
                )
                    .encode(env)
            });
        })
        .await;

        match rx.await {
            Ok(Ok(json)) => serde_json::from_str(&json)
                .map_err(|_e| kreuzberg::KreuzbergError::Other(format!("Failed to deserialize response: {}", _e))),
            Ok(Err(msg)) => Err(kreuzberg::KreuzbergError::Other(msg)),
            Err(_) => Err(kreuzberg::KreuzbergError::Other(
                "Channel closed before reply received".to_string(),
            )),
        }
    }
}

#[rustler::nif]
pub fn register_embedding_backend(
    env: rustler::Env<'_>,
    genserver_pid: rustler::LocalPid,
    plugin_name: String,
) -> rustler::Atom {
    let bridge = RustlerEmbeddingBackendBridge::new(genserver_pid, plugin_name);
    let arc: Arc<dyn kreuzberg::EmbeddingBackend> = Arc::new(bridge);

    let registry = kreuzberg::plugins::registry::get_embedding_backend_registry();
    match registry.write().register(arc) {
        Ok(_) => rustler::types::atom::Atom::from_str(env, "ok").unwrap(),
        Err(_) => rustler::types::atom::Atom::from_str(env, "error").unwrap(),
    }
}

#[rustler::nif]
pub fn unregister_embedding_backend(env: rustler::Env<'_>, name: String) -> rustler::Atom {
    match kreuzberg::plugins::embedding_backend::unregister_embedding_backend(&name) {
        Ok(_) => rustler::types::atom::Atom::from_str(env, "ok").unwrap(),
        Err(_) => rustler::types::atom::Atom::from_str(env, "error").unwrap(),
    }
}

#[rustler::nif]
pub fn clear_embedding_backends(env: rustler::Env<'_>) -> rustler::Atom {
    match kreuzberg::plugins::embedding_backend::clear_embedding_backends() {
        Ok(_) => rustler::types::atom::Atom::from_str(env, "ok").unwrap(),
        Err(_) => rustler::types::atom::Atom::from_str(env, "error").unwrap(),
    }
}

/// Wrapper that bridges a foreign Rustler object to the `DocumentExtractor` trait.
pub struct RustlerDocumentExtractorBridge {
    inner: rustler::LocalPid,
    cached_name: String,
}

impl std::fmt::Debug for RustlerDocumentExtractorBridge {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        write!(f, "RustlerDocumentExtractorBridge")
    }
}

impl RustlerDocumentExtractorBridge {
    /// Create a new bridge wrapping an Elixir GenServer PID.
    ///
    /// The PID is copied (LocalPid is Copy + Send + Sync) and used to send
    /// messages to the backing GenServer. The plugin_name is cached for fast
    /// Plugin::name() lookups.
    pub fn new(pid: rustler::LocalPid, plugin_name: String) -> Self {
        Self {
            inner: pid,
            cached_name: plugin_name,
        }
    }
}

impl kreuzberg::plugins::Plugin for RustlerDocumentExtractorBridge {
    fn name(&self) -> &str {
        &self.cached_name
    }

    fn version(&self) -> String {
        let reply_id = TRAIT_REPLY_COUNTER.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
        let (tx, rx) = tokio::sync::oneshot::channel::<Result<String, String>>();
        TRAIT_REPLY_CHANNELS.lock().unwrap().insert(reply_id, tx);

        let pid = self.inner;

        let args_json = {
            let args = serde_json::Map::new();
            serde_json::Value::Object(args).to_string()
        };

        let method = "version";

        // Check if we're already in a tokio runtime context. If not, create one.
        let _rt_guard = if tokio::runtime::Handle::try_current().is_err() {
            Some(tokio::runtime::Runtime::new().unwrap())
        } else {
            None
        };

        if let Some(ref rt) = _rt_guard {
            rt.spawn_blocking(move || {
                let mut env = rustler::OwnedEnv::new();
                let _ = env.send_and_clear(&pid, |env| {
                    (
                        rustler::types::atom::Atom::from_str(env, "trait_call").unwrap(),
                        method,
                        args_json.as_str(),
                        reply_id,
                    )
                        .encode(env)
                });
            });
        } else {
            tokio::task::spawn_blocking(move || {
                let mut env = rustler::OwnedEnv::new();
                let _ = env.send_and_clear(&pid, |env| {
                    (
                        rustler::types::atom::Atom::from_str(env, "trait_call").unwrap(),
                        method,
                        args_json.as_str(),
                        reply_id,
                    )
                        .encode(env)
                });
            });
        }

        match rx.blocking_recv() {
            Ok(Ok(json)) => serde_json::from_str(&json).unwrap_or_default(),
            _ => Default::default(),
        }
    }

    fn initialize(&self) -> std::result::Result<(), kreuzberg::KreuzbergError> {
        let reply_id = TRAIT_REPLY_COUNTER.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
        let (tx, rx) = tokio::sync::oneshot::channel::<Result<String, String>>();
        TRAIT_REPLY_CHANNELS.lock().unwrap().insert(reply_id, tx);

        let pid = self.inner;

        let args_json = {
            let args = serde_json::Map::new();
            serde_json::Value::Object(args).to_string()
        };

        let method = "initialize";

        // Check if we're already in a tokio runtime context. If not, create one.
        let _rt_guard = if tokio::runtime::Handle::try_current().is_err() {
            Some(tokio::runtime::Runtime::new().unwrap())
        } else {
            None
        };

        if let Some(ref rt) = _rt_guard {
            rt.spawn_blocking(move || {
                let mut env = rustler::OwnedEnv::new();
                let _ = env.send_and_clear(&pid, |env| {
                    (
                        rustler::types::atom::Atom::from_str(env, "trait_call").unwrap(),
                        method,
                        args_json.as_str(),
                        reply_id,
                    )
                        .encode(env)
                });
            });
        } else {
            tokio::task::spawn_blocking(move || {
                let mut env = rustler::OwnedEnv::new();
                let _ = env.send_and_clear(&pid, |env| {
                    (
                        rustler::types::atom::Atom::from_str(env, "trait_call").unwrap(),
                        method,
                        args_json.as_str(),
                        reply_id,
                    )
                        .encode(env)
                });
            });
        }

        match rx.blocking_recv() {
            Ok(Ok(json)) => serde_json::from_str(&json)
                .map_err(|_e| kreuzberg::KreuzbergError::Other(format!("Failed to deserialize response: {}", _e))),
            Ok(Err(msg)) => Err(kreuzberg::KreuzbergError::Other(msg)),
            Err(_) => Err(kreuzberg::KreuzbergError::Other(
                "Channel closed before reply received".to_string(),
            )),
        }
    }

    fn shutdown(&self) -> std::result::Result<(), kreuzberg::KreuzbergError> {
        let reply_id = TRAIT_REPLY_COUNTER.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
        let (tx, rx) = tokio::sync::oneshot::channel::<Result<String, String>>();
        TRAIT_REPLY_CHANNELS.lock().unwrap().insert(reply_id, tx);

        let pid = self.inner;

        let args_json = {
            let args = serde_json::Map::new();
            serde_json::Value::Object(args).to_string()
        };

        let method = "shutdown";

        // Check if we're already in a tokio runtime context. If not, create one.
        let _rt_guard = if tokio::runtime::Handle::try_current().is_err() {
            Some(tokio::runtime::Runtime::new().unwrap())
        } else {
            None
        };

        if let Some(ref rt) = _rt_guard {
            rt.spawn_blocking(move || {
                let mut env = rustler::OwnedEnv::new();
                let _ = env.send_and_clear(&pid, |env| {
                    (
                        rustler::types::atom::Atom::from_str(env, "trait_call").unwrap(),
                        method,
                        args_json.as_str(),
                        reply_id,
                    )
                        .encode(env)
                });
            });
        } else {
            tokio::task::spawn_blocking(move || {
                let mut env = rustler::OwnedEnv::new();
                let _ = env.send_and_clear(&pid, |env| {
                    (
                        rustler::types::atom::Atom::from_str(env, "trait_call").unwrap(),
                        method,
                        args_json.as_str(),
                        reply_id,
                    )
                        .encode(env)
                });
            });
        }

        match rx.blocking_recv() {
            Ok(Ok(json)) => serde_json::from_str(&json)
                .map_err(|_e| kreuzberg::KreuzbergError::Other(format!("Failed to deserialize response: {}", _e))),
            Ok(Err(msg)) => Err(kreuzberg::KreuzbergError::Other(msg)),
            Err(_) => Err(kreuzberg::KreuzbergError::Other(
                "Channel closed before reply received".to_string(),
            )),
        }
    }
}

#[async_trait::async_trait]
impl kreuzberg::DocumentExtractor for RustlerDocumentExtractorBridge {
    async fn extract_bytes(
        &self,
        content: &[u8],
        mime_type: &str,
        config: &kreuzberg::ExtractionConfig,
    ) -> std::result::Result<kreuzberg::InternalDocument, kreuzberg::KreuzbergError> {
        let content = content.to_owned();
        let mime_type = mime_type.to_owned();
        let config = config.to_owned();
        let reply_id = TRAIT_REPLY_COUNTER.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
        let (tx, rx) = tokio::sync::oneshot::channel::<Result<String, String>>();
        TRAIT_REPLY_CHANNELS.lock().unwrap().insert(reply_id, tx);

        let pid = self.inner;

        let args_json = {
            let mut args = serde_json::Map::new();
            args.insert(
                "content".to_string(),
                serde_json::Value::String(format!("{:?}", content)),
            );
            args.insert(
                "mime_type".to_string(),
                serde_json::Value::String(mime_type.to_string()),
            );
            args.insert("config".to_string(), serde_json::Value::String(format!("{:?}", config)));
            serde_json::Value::Object(args).to_string()
        };

        let method = "extract_bytes";

        let _ = tokio::task::spawn_blocking(move || {
            let mut env = rustler::OwnedEnv::new();
            let _ = env.send_and_clear(&pid, |env| {
                (
                    rustler::types::atom::Atom::from_str(env, "trait_call").unwrap(),
                    method,
                    args_json.as_str(),
                    reply_id,
                )
                    .encode(env)
            });
        })
        .await;

        match rx.await {
            Ok(Ok(json)) => serde_json::from_str(&json)
                .map_err(|_e| kreuzberg::KreuzbergError::Other(format!("Failed to deserialize response: {}", _e))),
            Ok(Err(msg)) => Err(kreuzberg::KreuzbergError::Other(msg)),
            Err(_) => Err(kreuzberg::KreuzbergError::Other(
                "Channel closed before reply received".to_string(),
            )),
        }
    }

    fn supported_mime_types(&self) -> &[&str] {
        let __types: Vec<String> = {
            let reply_id = TRAIT_REPLY_COUNTER.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
            let (tx, rx) = tokio::sync::oneshot::channel::<Result<String, String>>();
            TRAIT_REPLY_CHANNELS.lock().unwrap().insert(reply_id, tx);

            let pid = self.inner;

            let args_json = {
                let args = serde_json::Map::new();
                serde_json::Value::Object(args).to_string()
            };

            let method = "supported_mime_types";

            // Check if we're already in a tokio runtime context. If not, create one.
            let _rt_guard = if tokio::runtime::Handle::try_current().is_err() {
                Some(tokio::runtime::Runtime::new().unwrap())
            } else {
                None
            };

            if let Some(ref rt) = _rt_guard {
                rt.spawn_blocking(move || {
                    let mut env = rustler::OwnedEnv::new();
                    let _ = env.send_and_clear(&pid, |env| {
                        (
                            rustler::types::atom::Atom::from_str(env, "trait_call").unwrap(),
                            method,
                            args_json.as_str(),
                            reply_id,
                        )
                            .encode(env)
                    });
                });
            } else {
                tokio::task::spawn_blocking(move || {
                    let mut env = rustler::OwnedEnv::new();
                    let _ = env.send_and_clear(&pid, |env| {
                        (
                            rustler::types::atom::Atom::from_str(env, "trait_call").unwrap(),
                            method,
                            args_json.as_str(),
                            reply_id,
                        )
                            .encode(env)
                    });
                });
            }

            match rx.blocking_recv() {
                Ok(Ok(json)) => serde_json::from_str(&json).unwrap_or_default(),
                _ => Default::default(),
            }
        };
        let __strs: Vec<&'static str> = __types
            .into_iter()
            .map(|s| -> &'static str { Box::leak(s.into_boxed_str()) })
            .collect();
        Box::leak(__strs.into_boxed_slice())
    }
}

#[rustler::nif]
pub fn register_document_extractor(
    env: rustler::Env<'_>,
    genserver_pid: rustler::LocalPid,
    plugin_name: String,
) -> rustler::Atom {
    let bridge = RustlerDocumentExtractorBridge::new(genserver_pid, plugin_name);
    let arc: Arc<dyn kreuzberg::DocumentExtractor> = Arc::new(bridge);

    let registry = kreuzberg::plugins::registry::get_document_extractor_registry();
    match registry.write().register(arc) {
        Ok(_) => rustler::types::atom::Atom::from_str(env, "ok").unwrap(),
        Err(_) => rustler::types::atom::Atom::from_str(env, "error").unwrap(),
    }
}

#[rustler::nif]
pub fn unregister_document_extractor(env: rustler::Env<'_>, name: String) -> rustler::Atom {
    match kreuzberg::plugins::document_extractor::unregister_document_extractor(&name) {
        Ok(_) => rustler::types::atom::Atom::from_str(env, "ok").unwrap(),
        Err(_) => rustler::types::atom::Atom::from_str(env, "error").unwrap(),
    }
}

#[rustler::nif]
pub fn clear_document_extractors(env: rustler::Env<'_>) -> rustler::Atom {
    match kreuzberg::plugins::document_extractor::clear_document_extractors() {
        Ok(_) => rustler::types::atom::Atom::from_str(env, "ok").unwrap(),
        Err(_) => rustler::types::atom::Atom::from_str(env, "error").unwrap(),
    }
}

/// Wrapper that bridges a foreign Rustler object to the `Renderer` trait.
pub struct RustlerRendererBridge {
    inner: rustler::LocalPid,
    cached_name: String,
}

impl std::fmt::Debug for RustlerRendererBridge {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        write!(f, "RustlerRendererBridge")
    }
}

impl RustlerRendererBridge {
    /// Create a new bridge wrapping an Elixir GenServer PID.
    ///
    /// The PID is copied (LocalPid is Copy + Send + Sync) and used to send
    /// messages to the backing GenServer. The plugin_name is cached for fast
    /// Plugin::name() lookups.
    pub fn new(pid: rustler::LocalPid, plugin_name: String) -> Self {
        Self {
            inner: pid,
            cached_name: plugin_name,
        }
    }
}

impl kreuzberg::plugins::Plugin for RustlerRendererBridge {
    fn name(&self) -> &str {
        &self.cached_name
    }

    fn version(&self) -> String {
        let reply_id = TRAIT_REPLY_COUNTER.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
        let (tx, rx) = tokio::sync::oneshot::channel::<Result<String, String>>();
        TRAIT_REPLY_CHANNELS.lock().unwrap().insert(reply_id, tx);

        let pid = self.inner;

        let args_json = {
            let args = serde_json::Map::new();
            serde_json::Value::Object(args).to_string()
        };

        let method = "version";

        // Check if we're already in a tokio runtime context. If not, create one.
        let _rt_guard = if tokio::runtime::Handle::try_current().is_err() {
            Some(tokio::runtime::Runtime::new().unwrap())
        } else {
            None
        };

        if let Some(ref rt) = _rt_guard {
            rt.spawn_blocking(move || {
                let mut env = rustler::OwnedEnv::new();
                let _ = env.send_and_clear(&pid, |env| {
                    (
                        rustler::types::atom::Atom::from_str(env, "trait_call").unwrap(),
                        method,
                        args_json.as_str(),
                        reply_id,
                    )
                        .encode(env)
                });
            });
        } else {
            tokio::task::spawn_blocking(move || {
                let mut env = rustler::OwnedEnv::new();
                let _ = env.send_and_clear(&pid, |env| {
                    (
                        rustler::types::atom::Atom::from_str(env, "trait_call").unwrap(),
                        method,
                        args_json.as_str(),
                        reply_id,
                    )
                        .encode(env)
                });
            });
        }

        match rx.blocking_recv() {
            Ok(Ok(json)) => serde_json::from_str(&json).unwrap_or_default(),
            _ => Default::default(),
        }
    }

    fn initialize(&self) -> std::result::Result<(), kreuzberg::KreuzbergError> {
        let reply_id = TRAIT_REPLY_COUNTER.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
        let (tx, rx) = tokio::sync::oneshot::channel::<Result<String, String>>();
        TRAIT_REPLY_CHANNELS.lock().unwrap().insert(reply_id, tx);

        let pid = self.inner;

        let args_json = {
            let args = serde_json::Map::new();
            serde_json::Value::Object(args).to_string()
        };

        let method = "initialize";

        // Check if we're already in a tokio runtime context. If not, create one.
        let _rt_guard = if tokio::runtime::Handle::try_current().is_err() {
            Some(tokio::runtime::Runtime::new().unwrap())
        } else {
            None
        };

        if let Some(ref rt) = _rt_guard {
            rt.spawn_blocking(move || {
                let mut env = rustler::OwnedEnv::new();
                let _ = env.send_and_clear(&pid, |env| {
                    (
                        rustler::types::atom::Atom::from_str(env, "trait_call").unwrap(),
                        method,
                        args_json.as_str(),
                        reply_id,
                    )
                        .encode(env)
                });
            });
        } else {
            tokio::task::spawn_blocking(move || {
                let mut env = rustler::OwnedEnv::new();
                let _ = env.send_and_clear(&pid, |env| {
                    (
                        rustler::types::atom::Atom::from_str(env, "trait_call").unwrap(),
                        method,
                        args_json.as_str(),
                        reply_id,
                    )
                        .encode(env)
                });
            });
        }

        match rx.blocking_recv() {
            Ok(Ok(json)) => serde_json::from_str(&json)
                .map_err(|_e| kreuzberg::KreuzbergError::Other(format!("Failed to deserialize response: {}", _e))),
            Ok(Err(msg)) => Err(kreuzberg::KreuzbergError::Other(msg)),
            Err(_) => Err(kreuzberg::KreuzbergError::Other(
                "Channel closed before reply received".to_string(),
            )),
        }
    }

    fn shutdown(&self) -> std::result::Result<(), kreuzberg::KreuzbergError> {
        let reply_id = TRAIT_REPLY_COUNTER.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
        let (tx, rx) = tokio::sync::oneshot::channel::<Result<String, String>>();
        TRAIT_REPLY_CHANNELS.lock().unwrap().insert(reply_id, tx);

        let pid = self.inner;

        let args_json = {
            let args = serde_json::Map::new();
            serde_json::Value::Object(args).to_string()
        };

        let method = "shutdown";

        // Check if we're already in a tokio runtime context. If not, create one.
        let _rt_guard = if tokio::runtime::Handle::try_current().is_err() {
            Some(tokio::runtime::Runtime::new().unwrap())
        } else {
            None
        };

        if let Some(ref rt) = _rt_guard {
            rt.spawn_blocking(move || {
                let mut env = rustler::OwnedEnv::new();
                let _ = env.send_and_clear(&pid, |env| {
                    (
                        rustler::types::atom::Atom::from_str(env, "trait_call").unwrap(),
                        method,
                        args_json.as_str(),
                        reply_id,
                    )
                        .encode(env)
                });
            });
        } else {
            tokio::task::spawn_blocking(move || {
                let mut env = rustler::OwnedEnv::new();
                let _ = env.send_and_clear(&pid, |env| {
                    (
                        rustler::types::atom::Atom::from_str(env, "trait_call").unwrap(),
                        method,
                        args_json.as_str(),
                        reply_id,
                    )
                        .encode(env)
                });
            });
        }

        match rx.blocking_recv() {
            Ok(Ok(json)) => serde_json::from_str(&json)
                .map_err(|_e| kreuzberg::KreuzbergError::Other(format!("Failed to deserialize response: {}", _e))),
            Ok(Err(msg)) => Err(kreuzberg::KreuzbergError::Other(msg)),
            Err(_) => Err(kreuzberg::KreuzbergError::Other(
                "Channel closed before reply received".to_string(),
            )),
        }
    }
}

impl kreuzberg::Renderer for RustlerRendererBridge {
    fn render(&self, doc: &kreuzberg::InternalDocument) -> std::result::Result<String, kreuzberg::KreuzbergError> {
        let doc = doc.to_owned();
        let reply_id = TRAIT_REPLY_COUNTER.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
        let (tx, rx) = tokio::sync::oneshot::channel::<Result<String, String>>();
        TRAIT_REPLY_CHANNELS.lock().unwrap().insert(reply_id, tx);

        let pid = self.inner;

        let args_json = {
            let mut args = serde_json::Map::new();
            args.insert("doc".to_string(), serde_json::Value::String(format!("{:?}", doc)));
            serde_json::Value::Object(args).to_string()
        };

        let method = "render";

        // Check if we're already in a tokio runtime context. If not, create one.
        let _rt_guard = if tokio::runtime::Handle::try_current().is_err() {
            Some(tokio::runtime::Runtime::new().unwrap())
        } else {
            None
        };

        if let Some(ref rt) = _rt_guard {
            rt.spawn_blocking(move || {
                let mut env = rustler::OwnedEnv::new();
                let _ = env.send_and_clear(&pid, |env| {
                    (
                        rustler::types::atom::Atom::from_str(env, "trait_call").unwrap(),
                        method,
                        args_json.as_str(),
                        reply_id,
                    )
                        .encode(env)
                });
            });
        } else {
            tokio::task::spawn_blocking(move || {
                let mut env = rustler::OwnedEnv::new();
                let _ = env.send_and_clear(&pid, |env| {
                    (
                        rustler::types::atom::Atom::from_str(env, "trait_call").unwrap(),
                        method,
                        args_json.as_str(),
                        reply_id,
                    )
                        .encode(env)
                });
            });
        }

        match rx.blocking_recv() {
            Ok(Ok(json)) => serde_json::from_str(&json)
                .map_err(|_e| kreuzberg::KreuzbergError::Other(format!("Failed to deserialize response: {}", _e))),
            Ok(Err(msg)) => Err(kreuzberg::KreuzbergError::Other(msg)),
            Err(_) => Err(kreuzberg::KreuzbergError::Other(
                "Channel closed before reply received".to_string(),
            )),
        }
    }
}

#[rustler::nif]
pub fn register_renderer(
    env: rustler::Env<'_>,
    genserver_pid: rustler::LocalPid,
    plugin_name: String,
) -> rustler::Atom {
    let bridge = RustlerRendererBridge::new(genserver_pid, plugin_name);
    let arc: Arc<dyn kreuzberg::Renderer> = Arc::new(bridge);

    let registry = kreuzberg::plugins::registry::get_renderer_registry();
    match registry.write().register(arc) {
        Ok(_) => rustler::types::atom::Atom::from_str(env, "ok").unwrap(),
        Err(_) => rustler::types::atom::Atom::from_str(env, "error").unwrap(),
    }
}

#[rustler::nif]
pub fn unregister_renderer(env: rustler::Env<'_>, name: String) -> rustler::Atom {
    match kreuzberg::plugins::renderer::unregister_renderer(&name) {
        Ok(_) => rustler::types::atom::Atom::from_str(env, "ok").unwrap(),
        Err(_) => rustler::types::atom::Atom::from_str(env, "error").unwrap(),
    }
}

#[rustler::nif]
pub fn clear_renderers(env: rustler::Env<'_>) -> rustler::Atom {
    match kreuzberg::plugins::renderer::clear_renderers() {
        Ok(_) => rustler::types::atom::Atom::from_str(env, "ok").unwrap(),
        Err(_) => rustler::types::atom::Atom::from_str(env, "error").unwrap(),
    }
}

/// Complete a pending trait call with a successful JSON result.
/// Called from Elixir GenServer after handling a trait method call.
#[rustler::nif]
pub fn complete_trait_call(env: rustler::Env, reply_id: u64, result_json: String) -> rustler::Atom {
    if let Some(tx) = TRAIT_REPLY_CHANNELS.lock().unwrap().remove(&reply_id) {
        let _ = tx.send(Ok(result_json));
    }
    rustler::types::atom::ok()
}

/// Fail a pending trait call with an error message.
/// Called from Elixir GenServer if handling fails.
#[rustler::nif]
pub fn fail_trait_call(env: rustler::Env, reply_id: u64, error_message: String) -> rustler::Atom {
    if let Some(tx) = TRAIT_REPLY_CHANNELS.lock().unwrap().remove(&reply_id) {
        let _ = tx.send(Err(error_message));
    }
    rustler::types::atom::ok()
}

#[rustler::nif]
pub fn contentfilterconfig_default() -> ContentFilterConfig {
    kreuzberg::ContentFilterConfig::default().into()
}

#[rustler::nif]
pub fn extractionconfig_default() -> ExtractionConfig {
    kreuzberg::ExtractionConfig::default().into()
}

/// Check if image processing is needed by examining OCR and image extraction settings.
///
/// Returns `true` if either OCR is enabled or image extraction is configured,
/// indicating that image decompression and processing should occur.
/// Returns `false` if both are disabled, allowing optimization to skip unnecessary
/// image decompression for text-only extraction workflows.
///
/// # Optimization Impact
/// For text-only extractions (no OCR, no image extraction), skipping image
/// decompression can improve CPU utilization by 5-10% by avoiding wasteful
/// image I/O and processing when results won't be used.
#[rustler::nif]
pub fn extractionconfig_needs_image_processing(obj: ExtractionConfig) -> bool {
    kreuzberg::ExtractionConfig::from(obj).needs_image_processing()
}

#[rustler::nif]
pub fn imageextractionconfig_default() -> ImageExtractionConfig {
    kreuzberg::ImageExtractionConfig::default().into()
}

#[rustler::nif]
pub fn tokenreductionoptions_default() -> TokenReductionOptions {
    kreuzberg::TokenReductionOptions::default().into()
}

#[rustler::nif]
pub fn languagedetectionconfig_default() -> LanguageDetectionConfig {
    kreuzberg::LanguageDetectionConfig::default().into()
}

#[rustler::nif]
pub fn htmloutputconfig_default() -> HtmlOutputConfig {
    kreuzberg::HtmlOutputConfig::default().into()
}

#[rustler::nif]
pub fn layoutdetectionconfig_default() -> LayoutDetectionConfig {
    kreuzberg::LayoutDetectionConfig::default().into()
}

#[rustler::nif]
pub fn ocrqualitythresholds_default() -> OcrQualityThresholds {
    kreuzberg::OcrQualityThresholds::default().into()
}

#[rustler::nif]
pub fn ocrconfig_default() -> OcrConfig {
    kreuzberg::OcrConfig::default().into()
}

#[rustler::nif]
pub fn pageconfig_default() -> PageConfig {
    kreuzberg::PageConfig::default().into()
}

#[rustler::nif]
pub fn pdfconfig_default() -> PdfConfig {
    kreuzberg::PdfConfig::default().into()
}

#[rustler::nif]
pub fn hierarchyconfig_default() -> HierarchyConfig {
    kreuzberg::HierarchyConfig::default().into()
}

#[rustler::nif]
pub fn postprocessorconfig_default() -> PostProcessorConfig {
    kreuzberg::PostProcessorConfig::default().into()
}

#[rustler::nif]
pub fn chunkingconfig_default() -> ChunkingConfig {
    kreuzberg::ChunkingConfig::default().into()
}

#[rustler::nif]
pub fn embeddingconfig_default() -> EmbeddingConfig {
    kreuzberg::EmbeddingConfig::default().into()
}

#[rustler::nif]
pub fn treesitterconfig_default() -> TreeSitterConfig {
    kreuzberg::TreeSitterConfig::default().into()
}

#[rustler::nif]
pub fn treesitterprocessconfig_default() -> TreeSitterProcessConfig {
    kreuzberg::TreeSitterProcessConfig::default().into()
}

#[rustler::nif]
pub fn serverconfig_default() -> ServerConfig {
    kreuzberg::ServerConfig::default().into()
}

/// Get the server listen address (host:port).
///
/// # Example
///
/// ```rust
/// use kreuzberg::core::ServerConfig;
///
/// let config = ServerConfig::default();
/// assert_eq!(config.listen_addr(), "127.0.0.1:8000");
/// ```
#[rustler::nif]
pub fn serverconfig_listen_addr(obj: ServerConfig) -> String {
    kreuzberg::ServerConfig::from(obj).listen_addr()
}

/// Check if CORS allows all origins.
///
/// Returns `true` if the `cors_origins` vector is empty, meaning all origins
/// are allowed. Returns `false` if specific origins are configured.
///
/// # Example
///
/// ```rust
/// use kreuzberg::core::ServerConfig;
///
/// let mut config = ServerConfig::default();
/// assert!(config.cors_allows_all());
///
/// config.cors_origins.push("https://example.com".to_string());
/// assert!(!config.cors_allows_all());
/// ```
#[rustler::nif]
pub fn serverconfig_cors_allows_all(obj: ServerConfig) -> bool {
    kreuzberg::ServerConfig::from(obj).cors_allows_all()
}

/// Check if a given origin is allowed by CORS configuration.
///
/// Returns `true` if:
/// - CORS allows all origins (empty origins list), or
/// - The given origin is in the allowed origins list
///
/// # Arguments
///
/// * `origin` - The origin to check (e.g., "https://example.com")
///
/// # Example
///
/// ```rust
/// use kreuzberg::core::ServerConfig;
///
/// let mut config = ServerConfig::default();
/// assert!(config.is_origin_allowed("https://example.com"));
///
/// config.cors_origins.push("https://allowed.com".to_string());
/// assert!(config.is_origin_allowed("https://allowed.com"));
/// assert!(!config.is_origin_allowed("https://denied.com"));
/// ```
#[rustler::nif]
pub fn serverconfig_is_origin_allowed(obj: ServerConfig, origin: String) -> bool {
    kreuzberg::ServerConfig::from(obj).is_origin_allowed(&origin)
}

/// Get maximum request body size in megabytes (rounded up).
///
/// # Example
///
/// ```rust
/// use kreuzberg::core::ServerConfig;
///
/// let mut config = ServerConfig::default();
/// assert_eq!(config.max_request_body_mb(), 100);
/// ```
#[rustler::nif]
pub fn serverconfig_max_request_body_mb(obj: ServerConfig) -> usize {
    kreuzberg::ServerConfig::from(obj).max_request_body_mb()
}

/// Get maximum multipart field size in megabytes (rounded up).
///
/// # Example
///
/// ```rust
/// use kreuzberg::core::ServerConfig;
///
/// let mut config = ServerConfig::default();
/// assert_eq!(config.max_multipart_field_mb(), 100);
/// ```
#[rustler::nif]
pub fn serverconfig_max_multipart_field_mb(obj: ServerConfig) -> usize {
    kreuzberg::ServerConfig::from(obj).max_multipart_field_mb()
}

#[rustler::nif]
pub fn securitylimits_default() -> SecurityLimits {
    kreuzberg::SecurityLimits::default().into()
}

#[rustler::nif]
pub fn tokenreductionconfig_default() -> TokenReductionConfig {
    kreuzberg::TokenReductionConfig::default().into()
}

/// Compute and populate the `node_types` field from the current `nodes`.
///
/// Call this after all nodes have been added to the structure. Internal
/// construction paths (builder, derivation) call this automatically.
///
/// # Examples
///
/// ```rust
/// use kreuzberg::types::document_structure::{DocumentStructure, DocumentNode, NodeContent, NodeId};
///
/// let mut structure = DocumentStructure {
///     nodes: vec![DocumentNode {
///         id: NodeId::from("n1"),
///         content: NodeContent::Paragraph { text: "Hello".into() },
///         parent: None,
///         children: vec![],
///         content_layer: Default::default(),
///         page: None,
///         page_end: None,
///         bbox: None,
///         annotations: vec![],
///         attributes: None,
///     }],
///     source_format: None,
///     relationships: vec![],
///     node_types: vec![],
/// };
/// structure.finalize_node_types();
/// assert!(structure.node_types.contains(&"paragraph".to_string()));
/// ```
#[rustler::nif]
pub fn documentstructure_finalize_node_types(obj: DocumentStructure) -> () {
    ()
}

/// Check if the document structure is empty.
#[rustler::nif]
pub fn documentstructure_is_empty(obj: DocumentStructure) -> bool {
    kreuzberg::DocumentStructure::from(obj).is_empty()
}

#[rustler::nif]
pub fn documentstructure_default() -> DocumentStructure {
    kreuzberg::DocumentStructure::default().into()
}

/// Convert from an OCR result.
#[rustler::nif]
pub fn extractionresult_from_ocr(ocr: OcrExtractionResult) -> ExtractionResult {
    let ocr_core: kreuzberg::OcrExtractionResult = ocr.into();
    kreuzberg::ExtractionResult::from_ocr(ocr_core).into()
}

#[rustler::nif]
pub fn imagepreprocessingconfig_default() -> ImagePreprocessingConfig {
    kreuzberg::ImagePreprocessingConfig::default().into()
}

#[rustler::nif]
pub fn tesseractconfig_default() -> TesseractConfig {
    kreuzberg::TesseractConfig::default().into()
}

/// Returns `true` when no metadata fields, format-specific metadata, or
/// additional postprocessor fields are populated.
#[rustler::nif]
pub fn metadata_is_empty(obj: Metadata) -> bool {
    kreuzberg::Metadata::from(obj).is_empty()
}

#[rustler::nif]
pub fn diffoptions_default() -> DiffOptions {
    kreuzberg::DiffOptions::default().into()
}

#[rustler::nif]
pub fn yakeparams_default() -> YakeParams {
    kreuzberg::YakeParams::default().into()
}

#[rustler::nif]
pub fn rakeparams_default() -> RakeParams {
    kreuzberg::RakeParams::default().into()
}

#[rustler::nif]
pub fn keywordconfig_default() -> KeywordConfig {
    kreuzberg::KeywordConfig::default().into()
}

/// Sets a custom cache directory for model files.
///
/// # Arguments
///
/// * `path` - Path to cache directory
///
/// # Examples
///
/// ```no_run
/// use kreuzberg::PaddleOcrConfig;
/// use std::path::PathBuf;
///
/// let config = PaddleOcrConfig::new("en")
///     .with_cache_dir(PathBuf::from("/tmp/paddle-cache"));
/// ```
#[rustler::nif]
pub fn paddleocrconfig_with_cache_dir(obj: PaddleOcrConfig, path: String) -> PaddleOcrConfig {
    kreuzberg::PaddleOcrConfig::from(obj)
        .with_cache_dir(std::path::PathBuf::from(path))
        .into()
}

/// Enables or disables table structure detection.
///
/// # Arguments
///
/// * `enable` - Whether to enable table detection
///
/// # Examples
///
/// ```no_run
/// use kreuzberg::PaddleOcrConfig;
///
/// let config = PaddleOcrConfig::new("en")
///     .with_table_detection(true);
/// ```
#[rustler::nif]
pub fn paddleocrconfig_with_table_detection(obj: PaddleOcrConfig, enable: bool) -> PaddleOcrConfig {
    kreuzberg::PaddleOcrConfig::from(obj)
        .with_table_detection(enable)
        .into()
}

/// Enables or disables angle classification for rotated text.
///
/// # Arguments
///
/// * `enable` - Whether to enable angle classification
#[rustler::nif]
pub fn paddleocrconfig_with_angle_cls(obj: PaddleOcrConfig, enable: bool) -> PaddleOcrConfig {
    kreuzberg::PaddleOcrConfig::from(obj).with_angle_cls(enable).into()
}

/// Sets the database threshold for text detection.
///
/// # Arguments
///
/// * `threshold` - Detection threshold (0.0-1.0)
#[rustler::nif]
pub fn paddleocrconfig_with_det_db_thresh(obj: PaddleOcrConfig, threshold: f32) -> PaddleOcrConfig {
    kreuzberg::PaddleOcrConfig::from(obj)
        .with_det_db_thresh(threshold)
        .into()
}

/// Sets the box threshold for text bounding box refinement.
///
/// # Arguments
///
/// * `threshold` - Box threshold (0.0-1.0)
#[rustler::nif]
pub fn paddleocrconfig_with_det_db_box_thresh(obj: PaddleOcrConfig, threshold: f32) -> PaddleOcrConfig {
    kreuzberg::PaddleOcrConfig::from(obj)
        .with_det_db_box_thresh(threshold)
        .into()
}

/// Sets the unclip ratio for expanding text bounding boxes.
///
/// # Arguments
///
/// * `ratio` - Unclip ratio (typically 1.5-2.0)
#[rustler::nif]
pub fn paddleocrconfig_with_det_db_unclip_ratio(obj: PaddleOcrConfig, ratio: f32) -> PaddleOcrConfig {
    kreuzberg::PaddleOcrConfig::from(obj)
        .with_det_db_unclip_ratio(ratio)
        .into()
}

/// Sets the maximum side length for detection images.
///
/// # Arguments
///
/// * `length` - Maximum side length in pixels
#[rustler::nif]
pub fn paddleocrconfig_with_det_limit_side_len(obj: PaddleOcrConfig, length: u32) -> PaddleOcrConfig {
    kreuzberg::PaddleOcrConfig::from(obj)
        .with_det_limit_side_len(length)
        .into()
}

/// Sets the batch size for recognition inference.
///
/// # Arguments
///
/// * `batch_size` - Number of text regions to process simultaneously
#[rustler::nif]
pub fn paddleocrconfig_with_rec_batch_num(obj: PaddleOcrConfig, batch_size: u32) -> PaddleOcrConfig {
    kreuzberg::PaddleOcrConfig::from(obj)
        .with_rec_batch_num(batch_size)
        .into()
}

/// Sets the minimum recognition confidence threshold.
///
/// # Arguments
///
/// * `score` - Minimum confidence (0.0-1.0), text below this is dropped
#[rustler::nif]
pub fn paddleocrconfig_with_drop_score(obj: PaddleOcrConfig, score: f32) -> PaddleOcrConfig {
    kreuzberg::PaddleOcrConfig::from(obj).with_drop_score(score).into()
}

/// Sets padding in pixels added around images before detection.
///
/// # Arguments
///
/// * `padding` - Padding in pixels (0-100)
#[rustler::nif]
pub fn paddleocrconfig_with_padding(obj: PaddleOcrConfig, padding: u32) -> PaddleOcrConfig {
    kreuzberg::PaddleOcrConfig::from(obj).with_padding(padding).into()
}

/// Sets the model tier controlling detection/recognition model size.
///
/// # Arguments
///
/// * `tier` - `"mobile"` (default, lightweight, faster) or `"server"` (high accuracy, GPU/complex documents)
#[rustler::nif]
pub fn paddleocrconfig_with_model_tier(obj: PaddleOcrConfig, tier: String) -> PaddleOcrConfig {
    kreuzberg::PaddleOcrConfig::from(obj).with_model_tier(tier).into()
}

/// Creates a default configuration with English language support.
#[rustler::nif]
pub fn paddleocrconfig_default() -> PaddleOcrConfig {
    kreuzberg::PaddleOcrConfig::default().into()
}

#[allow(clippy::redundant_closure, clippy::useless_conversion)]
impl From<AccelerationConfig> for kreuzberg::AccelerationConfig {
    fn from(val: AccelerationConfig) -> Self {
        Self {
            provider: val.provider.into(),
            device_id: val.device_id,
        }
    }
}

#[allow(clippy::redundant_closure, clippy::useless_conversion)]
impl From<kreuzberg::AccelerationConfig> for AccelerationConfig {
    fn from(val: kreuzberg::AccelerationConfig) -> Self {
        Self {
            provider: val.provider.into(),
            device_id: val.device_id,
        }
    }
}

#[allow(clippy::redundant_closure, clippy::useless_conversion)]
impl From<ContentFilterConfig> for kreuzberg::ContentFilterConfig {
    fn from(val: ContentFilterConfig) -> Self {
        Self {
            include_headers: val.include_headers,
            include_footers: val.include_footers,
            strip_repeating_text: val.strip_repeating_text,
            include_watermarks: val.include_watermarks,
        }
    }
}

#[allow(clippy::redundant_closure, clippy::useless_conversion)]
impl From<kreuzberg::ContentFilterConfig> for ContentFilterConfig {
    fn from(val: kreuzberg::ContentFilterConfig) -> Self {
        Self {
            include_headers: val.include_headers,
            include_footers: val.include_footers,
            strip_repeating_text: val.strip_repeating_text,
            include_watermarks: val.include_watermarks,
        }
    }
}

#[allow(clippy::redundant_closure, clippy::useless_conversion)]
impl From<EmailConfig> for kreuzberg::EmailConfig {
    fn from(val: EmailConfig) -> Self {
        Self {
            msg_fallback_codepage: val.msg_fallback_codepage,
        }
    }
}

#[allow(clippy::redundant_closure, clippy::useless_conversion)]
impl From<kreuzberg::EmailConfig> for EmailConfig {
    fn from(val: kreuzberg::EmailConfig) -> Self {
        Self {
            msg_fallback_codepage: val.msg_fallback_codepage,
        }
    }
}

#[allow(clippy::needless_update)]
#[allow(clippy::redundant_closure, clippy::useless_conversion)]
impl From<ExtractionConfig> for kreuzberg::ExtractionConfig {
    fn from(val: ExtractionConfig) -> Self {
        Self {
            use_cache: val.use_cache,
            enable_quality_processing: val.enable_quality_processing,
            ocr: val.ocr.map(Into::into),
            force_ocr: val.force_ocr,
            force_ocr_pages: val.force_ocr_pages.map(|v| v.into_iter().collect()),
            disable_ocr: val.disable_ocr,
            chunking: val.chunking.map(Into::into),
            content_filter: val.content_filter.map(Into::into),
            images: val.images.map(Into::into),
            pdf_options: val.pdf_options.map(Into::into),
            token_reduction: val.token_reduction.map(Into::into),
            language_detection: val.language_detection.map(Into::into),
            pages: val.pages.map(Into::into),
            keywords: val.keywords.map(Into::into),
            postprocessor: val.postprocessor.map(Into::into),
            html_options: Default::default(),
            html_output: val.html_output.map(Into::into),
            extraction_timeout_secs: val.extraction_timeout_secs,
            max_concurrent_extractions: val.max_concurrent_extractions,
            result_format: val.result_format.into(),
            security_limits: val.security_limits.map(Into::into),
            max_embedded_file_bytes: val.max_embedded_file_bytes,
            output_format: Default::default(),
            layout: val.layout.map(Into::into),
            use_layout_for_markdown: val.use_layout_for_markdown,
            include_document_structure: val.include_document_structure,
            acceleration: val.acceleration.map(Into::into),
            cache_namespace: val.cache_namespace,
            cache_ttl_secs: val.cache_ttl_secs,
            email: val.email.map(Into::into),
            concurrency: Default::default(),
            max_archive_depth: val.max_archive_depth,
            tree_sitter: val.tree_sitter.map(Into::into),
            structured_extraction: val.structured_extraction.map(Into::into),
            cancel_token: Default::default(),
            ..Default::default()
        }
    }
}

#[allow(clippy::redundant_closure, clippy::useless_conversion)]
impl From<kreuzberg::ExtractionConfig> for ExtractionConfig {
    fn from(val: kreuzberg::ExtractionConfig) -> Self {
        Self {
            use_cache: val.use_cache,
            enable_quality_processing: val.enable_quality_processing,
            ocr: val.ocr.map(Into::into),
            force_ocr: val.force_ocr,
            force_ocr_pages: val.force_ocr_pages.map(|v| v.into_iter().collect()),
            disable_ocr: val.disable_ocr,
            chunking: val.chunking.map(Into::into),
            content_filter: val.content_filter.map(Into::into),
            images: val.images.map(Into::into),
            pdf_options: val.pdf_options.map(Into::into),
            token_reduction: val.token_reduction.map(Into::into),
            language_detection: val.language_detection.map(Into::into),
            pages: val.pages.map(Into::into),
            keywords: val.keywords.map(Into::into),
            postprocessor: val.postprocessor.map(Into::into),
            html_options: val.html_options.as_ref().map(|v| format!("{v:?}")),
            html_output: val.html_output.map(Into::into),
            extraction_timeout_secs: val.extraction_timeout_secs,
            max_concurrent_extractions: val.max_concurrent_extractions,
            result_format: val.result_format.into(),
            security_limits: val.security_limits.map(Into::into),
            max_embedded_file_bytes: val.max_embedded_file_bytes,
            output_format: val.output_format.into(),
            layout: val.layout.map(Into::into),
            use_layout_for_markdown: val.use_layout_for_markdown,
            include_document_structure: val.include_document_structure,
            acceleration: val.acceleration.map(Into::into),
            cache_namespace: val.cache_namespace,
            cache_ttl_secs: val.cache_ttl_secs,
            email: val.email.map(Into::into),
            concurrency: val.concurrency.as_ref().map(|v| format!("{v:?}")),
            max_archive_depth: val.max_archive_depth,
            tree_sitter: val.tree_sitter.map(Into::into),
            structured_extraction: val.structured_extraction.map(Into::into),
            cancel_token: val.cancel_token.as_ref().map(|v| format!("{v:?}")),
        }
    }
}

#[allow(clippy::needless_update)]
#[allow(clippy::redundant_closure, clippy::useless_conversion)]
impl From<FileExtractionConfig> for kreuzberg::FileExtractionConfig {
    fn from(val: FileExtractionConfig) -> Self {
        Self {
            enable_quality_processing: val.enable_quality_processing,
            ocr: val.ocr.map(Into::into),
            force_ocr: val.force_ocr,
            force_ocr_pages: val.force_ocr_pages.map(|v| v.into_iter().collect()),
            disable_ocr: val.disable_ocr,
            chunking: val.chunking.map(Into::into),
            content_filter: val.content_filter.map(Into::into),
            images: val.images.map(Into::into),
            pdf_options: val.pdf_options.map(Into::into),
            token_reduction: val.token_reduction.map(Into::into),
            language_detection: val.language_detection.map(Into::into),
            pages: val.pages.map(Into::into),
            keywords: val.keywords.map(Into::into),
            postprocessor: val.postprocessor.map(Into::into),
            html_options: Default::default(),
            result_format: val.result_format.map(Into::into),
            output_format: Default::default(),
            include_document_structure: val.include_document_structure,
            layout: val.layout.map(Into::into),
            timeout_secs: val.timeout_secs,
            tree_sitter: val.tree_sitter.map(Into::into),
            structured_extraction: val.structured_extraction.map(Into::into),
            ..Default::default()
        }
    }
}

#[allow(clippy::redundant_closure, clippy::useless_conversion)]
impl From<kreuzberg::FileExtractionConfig> for FileExtractionConfig {
    fn from(val: kreuzberg::FileExtractionConfig) -> Self {
        Self {
            enable_quality_processing: val.enable_quality_processing,
            ocr: val.ocr.map(Into::into),
            force_ocr: val.force_ocr,
            force_ocr_pages: val.force_ocr_pages.map(|v| v.into_iter().collect()),
            disable_ocr: val.disable_ocr,
            chunking: val.chunking.map(Into::into),
            content_filter: val.content_filter.map(Into::into),
            images: val.images.map(Into::into),
            pdf_options: val.pdf_options.map(Into::into),
            token_reduction: val.token_reduction.map(Into::into),
            language_detection: val.language_detection.map(Into::into),
            pages: val.pages.map(Into::into),
            keywords: val.keywords.map(Into::into),
            postprocessor: val.postprocessor.map(Into::into),
            html_options: val.html_options.as_ref().map(|v| format!("{v:?}")),
            result_format: val.result_format.map(Into::into),
            output_format: val.output_format.map(Into::into),
            include_document_structure: val.include_document_structure,
            layout: val.layout.map(Into::into),
            timeout_secs: val.timeout_secs,
            tree_sitter: val.tree_sitter.map(Into::into),
            structured_extraction: val.structured_extraction.map(Into::into),
        }
    }
}

#[allow(clippy::redundant_closure, clippy::useless_conversion)]
impl From<BatchBytesItem> for kreuzberg::BatchBytesItem {
    fn from(val: BatchBytesItem) -> Self {
        Self {
            content: val.content.to_vec().into(),
            mime_type: val.mime_type,
            config: val.config.map(Into::into),
        }
    }
}

#[allow(clippy::redundant_closure, clippy::useless_conversion)]
impl From<kreuzberg::BatchBytesItem> for BatchBytesItem {
    fn from(val: kreuzberg::BatchBytesItem) -> Self {
        Self {
            content: val.content.to_vec().into(),
            mime_type: val.mime_type,
            config: val.config.map(Into::into),
        }
    }
}

#[allow(clippy::redundant_closure, clippy::useless_conversion)]
impl From<BatchFileItem> for kreuzberg::BatchFileItem {
    fn from(val: BatchFileItem) -> Self {
        Self {
            path: val.path.into(),
            config: val.config.map(Into::into),
        }
    }
}

#[allow(clippy::redundant_closure, clippy::useless_conversion)]
impl From<kreuzberg::BatchFileItem> for BatchFileItem {
    fn from(val: kreuzberg::BatchFileItem) -> Self {
        Self {
            path: val.path.to_string_lossy().to_string(),
            config: val.config.map(Into::into),
        }
    }
}

#[allow(clippy::redundant_closure, clippy::useless_conversion)]
impl From<ImageExtractionConfig> for kreuzberg::ImageExtractionConfig {
    fn from(val: ImageExtractionConfig) -> Self {
        Self {
            extract_images: val.extract_images,
            target_dpi: val.target_dpi,
            max_image_dimension: val.max_image_dimension,
            inject_placeholders: val.inject_placeholders,
            auto_adjust_dpi: val.auto_adjust_dpi,
            min_dpi: val.min_dpi,
            max_dpi: val.max_dpi,
            max_images_per_page: val.max_images_per_page,
            classify: val.classify,
            include_page_rasters: val.include_page_rasters,
            run_ocr_on_images: val.run_ocr_on_images,
            ocr_text_only: val.ocr_text_only,
            append_ocr_text: val.append_ocr_text,
        }
    }
}

#[allow(clippy::redundant_closure, clippy::useless_conversion)]
impl From<kreuzberg::ImageExtractionConfig> for ImageExtractionConfig {
    fn from(val: kreuzberg::ImageExtractionConfig) -> Self {
        Self {
            extract_images: val.extract_images,
            target_dpi: val.target_dpi,
            max_image_dimension: val.max_image_dimension,
            inject_placeholders: val.inject_placeholders,
            auto_adjust_dpi: val.auto_adjust_dpi,
            min_dpi: val.min_dpi,
            max_dpi: val.max_dpi,
            max_images_per_page: val.max_images_per_page,
            classify: val.classify,
            include_page_rasters: val.include_page_rasters,
            run_ocr_on_images: val.run_ocr_on_images,
            ocr_text_only: val.ocr_text_only,
            append_ocr_text: val.append_ocr_text,
        }
    }
}

#[allow(clippy::redundant_closure, clippy::useless_conversion)]
impl From<TokenReductionOptions> for kreuzberg::TokenReductionOptions {
    fn from(val: TokenReductionOptions) -> Self {
        Self {
            mode: val.mode,
            preserve_important_words: val.preserve_important_words,
        }
    }
}

#[allow(clippy::redundant_closure, clippy::useless_conversion)]
impl From<kreuzberg::TokenReductionOptions> for TokenReductionOptions {
    fn from(val: kreuzberg::TokenReductionOptions) -> Self {
        Self {
            mode: val.mode,
            preserve_important_words: val.preserve_important_words,
        }
    }
}

#[allow(clippy::redundant_closure, clippy::useless_conversion)]
impl From<LanguageDetectionConfig> for kreuzberg::LanguageDetectionConfig {
    fn from(val: LanguageDetectionConfig) -> Self {
        Self {
            enabled: val.enabled,
            min_confidence: val.min_confidence,
            detect_multiple: val.detect_multiple,
        }
    }
}

#[allow(clippy::redundant_closure, clippy::useless_conversion)]
impl From<kreuzberg::LanguageDetectionConfig> for LanguageDetectionConfig {
    fn from(val: kreuzberg::LanguageDetectionConfig) -> Self {
        Self {
            enabled: val.enabled,
            min_confidence: val.min_confidence,
            detect_multiple: val.detect_multiple,
        }
    }
}

#[allow(clippy::redundant_closure, clippy::useless_conversion)]
impl From<HtmlOutputConfig> for kreuzberg::HtmlOutputConfig {
    fn from(val: HtmlOutputConfig) -> Self {
        Self {
            css: val.css,
            css_file: val.css_file.map(Into::into),
            theme: val.theme.into(),
            class_prefix: val.class_prefix,
            embed_css: val.embed_css,
        }
    }
}

#[allow(clippy::redundant_closure, clippy::useless_conversion)]
impl From<kreuzberg::HtmlOutputConfig> for HtmlOutputConfig {
    fn from(val: kreuzberg::HtmlOutputConfig) -> Self {
        Self {
            css: val.css,
            css_file: val.css_file.map(|p| p.to_string_lossy().to_string()),
            theme: val.theme.into(),
            class_prefix: val.class_prefix,
            embed_css: val.embed_css,
        }
    }
}

#[allow(clippy::redundant_closure, clippy::useless_conversion)]
impl From<LayoutDetectionConfig> for kreuzberg::LayoutDetectionConfig {
    fn from(val: LayoutDetectionConfig) -> Self {
        Self {
            confidence_threshold: val.confidence_threshold,
            apply_heuristics: val.apply_heuristics,
            table_model: val.table_model.into(),
            acceleration: val.acceleration.map(Into::into),
        }
    }
}

#[allow(clippy::redundant_closure, clippy::useless_conversion)]
impl From<kreuzberg::LayoutDetectionConfig> for LayoutDetectionConfig {
    fn from(val: kreuzberg::LayoutDetectionConfig) -> Self {
        Self {
            confidence_threshold: val.confidence_threshold,
            apply_heuristics: val.apply_heuristics,
            table_model: val.table_model.into(),
            acceleration: val.acceleration.map(Into::into),
        }
    }
}

#[allow(clippy::redundant_closure, clippy::useless_conversion)]
impl From<LlmConfig> for kreuzberg::LlmConfig {
    fn from(val: LlmConfig) -> Self {
        Self {
            model: val.model,
            api_key: val.api_key,
            base_url: val.base_url,
            timeout_secs: val.timeout_secs,
            max_retries: val.max_retries,
            temperature: val.temperature,
            max_tokens: val.max_tokens,
        }
    }
}

#[allow(clippy::redundant_closure, clippy::useless_conversion)]
impl From<kreuzberg::LlmConfig> for LlmConfig {
    fn from(val: kreuzberg::LlmConfig) -> Self {
        Self {
            model: val.model,
            api_key: val.api_key,
            base_url: val.base_url,
            timeout_secs: val.timeout_secs,
            max_retries: val.max_retries,
            temperature: val.temperature,
            max_tokens: val.max_tokens,
        }
    }
}

#[allow(clippy::redundant_closure, clippy::useless_conversion)]
impl From<StructuredExtractionConfig> for kreuzberg::StructuredExtractionConfig {
    fn from(val: StructuredExtractionConfig) -> Self {
        Self {
            schema: serde_json::from_str(&val.schema).unwrap_or_default(),
            schema_name: val.schema_name,
            schema_description: val.schema_description,
            strict: val.strict,
            prompt: val.prompt,
            llm: val.llm.into(),
        }
    }
}

#[allow(clippy::redundant_closure, clippy::useless_conversion)]
impl From<kreuzberg::StructuredExtractionConfig> for StructuredExtractionConfig {
    fn from(val: kreuzberg::StructuredExtractionConfig) -> Self {
        Self {
            schema: val.schema.to_string(),
            schema_name: val.schema_name,
            schema_description: val.schema_description,
            strict: val.strict,
            prompt: val.prompt,
            llm: val.llm.into(),
        }
    }
}

#[allow(clippy::redundant_closure, clippy::useless_conversion)]
impl From<OcrQualityThresholds> for kreuzberg::OcrQualityThresholds {
    fn from(val: OcrQualityThresholds) -> Self {
        Self {
            min_total_non_whitespace: val.min_total_non_whitespace,
            min_non_whitespace_per_page: val.min_non_whitespace_per_page,
            min_meaningful_word_len: val.min_meaningful_word_len,
            min_meaningful_words: val.min_meaningful_words,
            min_alnum_ratio: val.min_alnum_ratio,
            min_garbage_chars: val.min_garbage_chars,
            max_fragmented_word_ratio: val.max_fragmented_word_ratio,
            critical_fragmented_word_ratio: val.critical_fragmented_word_ratio,
            min_avg_word_length: val.min_avg_word_length,
            min_words_for_avg_length_check: val.min_words_for_avg_length_check,
            min_consecutive_repeat_ratio: val.min_consecutive_repeat_ratio,
            min_words_for_repeat_check: val.min_words_for_repeat_check,
            substantive_min_chars: val.substantive_min_chars,
            non_text_min_chars: val.non_text_min_chars,
            alnum_ws_ratio_threshold: val.alnum_ws_ratio_threshold,
            pipeline_min_quality: val.pipeline_min_quality,
        }
    }
}

#[allow(clippy::redundant_closure, clippy::useless_conversion)]
impl From<kreuzberg::OcrQualityThresholds> for OcrQualityThresholds {
    fn from(val: kreuzberg::OcrQualityThresholds) -> Self {
        Self {
            min_total_non_whitespace: val.min_total_non_whitespace,
            min_non_whitespace_per_page: val.min_non_whitespace_per_page,
            min_meaningful_word_len: val.min_meaningful_word_len,
            min_meaningful_words: val.min_meaningful_words,
            min_alnum_ratio: val.min_alnum_ratio,
            min_garbage_chars: val.min_garbage_chars,
            max_fragmented_word_ratio: val.max_fragmented_word_ratio,
            critical_fragmented_word_ratio: val.critical_fragmented_word_ratio,
            min_avg_word_length: val.min_avg_word_length,
            min_words_for_avg_length_check: val.min_words_for_avg_length_check,
            min_consecutive_repeat_ratio: val.min_consecutive_repeat_ratio,
            min_words_for_repeat_check: val.min_words_for_repeat_check,
            substantive_min_chars: val.substantive_min_chars,
            non_text_min_chars: val.non_text_min_chars,
            alnum_ws_ratio_threshold: val.alnum_ws_ratio_threshold,
            pipeline_min_quality: val.pipeline_min_quality,
        }
    }
}

#[allow(clippy::redundant_closure, clippy::useless_conversion)]
impl From<OcrPipelineStage> for kreuzberg::OcrPipelineStage {
    fn from(val: OcrPipelineStage) -> Self {
        Self {
            backend: val.backend,
            priority: val.priority,
            language: val.language,
            tesseract_config: val.tesseract_config.map(Into::into),
            paddle_ocr_config: val
                .paddle_ocr_config
                .as_ref()
                .and_then(|s| serde_json::from_str(s).ok()),
            vlm_config: val.vlm_config.map(Into::into),
            backend_options: val.backend_options.as_ref().and_then(|s| serde_json::from_str(s).ok()),
        }
    }
}

#[allow(clippy::redundant_closure, clippy::useless_conversion)]
impl From<kreuzberg::OcrPipelineStage> for OcrPipelineStage {
    fn from(val: kreuzberg::OcrPipelineStage) -> Self {
        Self {
            backend: val.backend,
            priority: val.priority,
            language: val.language,
            tesseract_config: val.tesseract_config.map(Into::into),
            paddle_ocr_config: val.paddle_ocr_config.as_ref().map(ToString::to_string),
            vlm_config: val.vlm_config.map(Into::into),
            backend_options: val.backend_options.as_ref().map(ToString::to_string),
        }
    }
}

#[allow(clippy::redundant_closure, clippy::useless_conversion)]
impl From<OcrPipelineConfig> for kreuzberg::OcrPipelineConfig {
    fn from(val: OcrPipelineConfig) -> Self {
        Self {
            stages: val.stages.into_iter().map(Into::into).collect(),
            quality_thresholds: val.quality_thresholds.into(),
        }
    }
}

#[allow(clippy::redundant_closure, clippy::useless_conversion)]
impl From<kreuzberg::OcrPipelineConfig> for OcrPipelineConfig {
    fn from(val: kreuzberg::OcrPipelineConfig) -> Self {
        Self {
            stages: val.stages.into_iter().map(Into::into).collect(),
            quality_thresholds: val.quality_thresholds.into(),
        }
    }
}

#[allow(clippy::redundant_closure, clippy::useless_conversion)]
impl From<OcrConfig> for kreuzberg::OcrConfig {
    fn from(val: OcrConfig) -> Self {
        Self {
            enabled: val.enabled,
            backend: val.backend,
            language: val.language,
            tesseract_config: val.tesseract_config.map(Into::into),
            output_format: Default::default(),
            paddle_ocr_config: val
                .paddle_ocr_config
                .as_ref()
                .and_then(|s| serde_json::from_str(s).ok()),
            backend_options: val.backend_options.as_ref().and_then(|s| serde_json::from_str(s).ok()),
            element_config: val.element_config.map(Into::into),
            quality_thresholds: val.quality_thresholds.map(Into::into),
            pipeline: val.pipeline.map(Into::into),
            auto_rotate: val.auto_rotate,
            vlm_config: val.vlm_config.map(Into::into),
            vlm_prompt: val.vlm_prompt,
            acceleration: val.acceleration.map(Into::into),
            tessdata_bytes: val
                .tessdata_bytes
                .map(|m| m.into_iter().map(|(k, v)| (k, v.to_vec().into())).collect()),
        }
    }
}

#[allow(clippy::redundant_closure, clippy::useless_conversion)]
impl From<kreuzberg::OcrConfig> for OcrConfig {
    fn from(val: kreuzberg::OcrConfig) -> Self {
        Self {
            enabled: val.enabled,
            backend: val.backend,
            language: val.language,
            tesseract_config: val.tesseract_config.map(Into::into),
            output_format: val.output_format.map(Into::into),
            paddle_ocr_config: val.paddle_ocr_config.as_ref().map(ToString::to_string),
            backend_options: val.backend_options.as_ref().map(ToString::to_string),
            element_config: val.element_config.map(Into::into),
            quality_thresholds: val.quality_thresholds.map(Into::into),
            pipeline: val.pipeline.map(Into::into),
            auto_rotate: val.auto_rotate,
            vlm_config: val.vlm_config.map(Into::into),
            vlm_prompt: val.vlm_prompt,
            acceleration: val.acceleration.map(Into::into),
            tessdata_bytes: val
                .tessdata_bytes
                .map(|m| m.into_iter().map(|(k, v)| (k, v.to_vec().into())).collect()),
        }
    }
}

#[allow(clippy::redundant_closure, clippy::useless_conversion)]
impl From<PageConfig> for kreuzberg::PageConfig {
    fn from(val: PageConfig) -> Self {
        Self {
            extract_pages: val.extract_pages,
            insert_page_markers: val.insert_page_markers,
            marker_format: val.marker_format,
        }
    }
}

#[allow(clippy::redundant_closure, clippy::useless_conversion)]
impl From<kreuzberg::PageConfig> for PageConfig {
    fn from(val: kreuzberg::PageConfig) -> Self {
        Self {
            extract_pages: val.extract_pages,
            insert_page_markers: val.insert_page_markers,
            marker_format: val.marker_format,
        }
    }
}

#[allow(clippy::redundant_closure, clippy::useless_conversion)]
impl From<PdfConfig> for kreuzberg::PdfConfig {
    fn from(val: PdfConfig) -> Self {
        Self {
            extract_images: val.extract_images,
            extract_tables: val.extract_tables,
            passwords: val.passwords.map(|v| v.into_iter().collect()),
            extract_metadata: val.extract_metadata,
            hierarchy: val.hierarchy.map(Into::into),
            extract_annotations: val.extract_annotations,
            top_margin_fraction: val.top_margin_fraction,
            bottom_margin_fraction: val.bottom_margin_fraction,
            allow_single_column_tables: val.allow_single_column_tables,
            ocr_inline_images: val.ocr_inline_images,
        }
    }
}

#[allow(clippy::redundant_closure, clippy::useless_conversion)]
impl From<kreuzberg::PdfConfig> for PdfConfig {
    fn from(val: kreuzberg::PdfConfig) -> Self {
        Self {
            extract_images: val.extract_images,
            extract_tables: val.extract_tables,
            passwords: val.passwords.map(|v| v.into_iter().collect()),
            extract_metadata: val.extract_metadata,
            hierarchy: val.hierarchy.map(Into::into),
            extract_annotations: val.extract_annotations,
            top_margin_fraction: val.top_margin_fraction,
            bottom_margin_fraction: val.bottom_margin_fraction,
            allow_single_column_tables: val.allow_single_column_tables,
            ocr_inline_images: val.ocr_inline_images,
        }
    }
}

#[allow(clippy::redundant_closure, clippy::useless_conversion)]
impl From<HierarchyConfig> for kreuzberg::HierarchyConfig {
    fn from(val: HierarchyConfig) -> Self {
        Self {
            enabled: val.enabled,
            k_clusters: val.k_clusters,
            include_bbox: val.include_bbox,
            ocr_coverage_threshold: val.ocr_coverage_threshold,
        }
    }
}

#[allow(clippy::redundant_closure, clippy::useless_conversion)]
impl From<kreuzberg::HierarchyConfig> for HierarchyConfig {
    fn from(val: kreuzberg::HierarchyConfig) -> Self {
        Self {
            enabled: val.enabled,
            k_clusters: val.k_clusters,
            include_bbox: val.include_bbox,
            ocr_coverage_threshold: val.ocr_coverage_threshold,
        }
    }
}

#[allow(clippy::redundant_closure, clippy::useless_conversion)]
impl From<PostProcessorConfig> for kreuzberg::PostProcessorConfig {
    fn from(val: PostProcessorConfig) -> Self {
        Self {
            enabled: val.enabled,
            enabled_processors: val.enabled_processors.map(|v| v.into_iter().collect()),
            disabled_processors: val.disabled_processors.map(|v| v.into_iter().collect()),
            enabled_set: val.enabled_set.map(|v| v.into_iter().collect()),
            disabled_set: val.disabled_set.map(|v| v.into_iter().collect()),
        }
    }
}

#[allow(clippy::redundant_closure, clippy::useless_conversion)]
impl From<kreuzberg::PostProcessorConfig> for PostProcessorConfig {
    fn from(val: kreuzberg::PostProcessorConfig) -> Self {
        Self {
            enabled: val.enabled,
            enabled_processors: val.enabled_processors.map(|v| v.into_iter().collect()),
            disabled_processors: val.disabled_processors.map(|v| v.into_iter().collect()),
            enabled_set: val.enabled_set.map(|v| v.into_iter().collect()),
            disabled_set: val.disabled_set.map(|v| v.into_iter().collect()),
        }
    }
}

#[allow(clippy::redundant_closure, clippy::useless_conversion)]
impl From<ChunkingConfig> for kreuzberg::ChunkingConfig {
    fn from(val: ChunkingConfig) -> Self {
        Self {
            max_characters: val.max_characters,
            overlap: val.overlap,
            trim: val.trim,
            chunker_type: val.chunker_type.into(),
            embedding: val.embedding.map(Into::into),
            preset: val.preset,
            sizing: val.sizing.into(),
            prepend_heading_context: val.prepend_heading_context,
            topic_threshold: val.topic_threshold,
        }
    }
}

#[allow(clippy::redundant_closure, clippy::useless_conversion)]
impl From<kreuzberg::ChunkingConfig> for ChunkingConfig {
    fn from(val: kreuzberg::ChunkingConfig) -> Self {
        Self {
            max_characters: val.max_characters,
            overlap: val.overlap,
            trim: val.trim,
            chunker_type: val.chunker_type.into(),
            embedding: val.embedding.map(Into::into),
            preset: val.preset,
            sizing: val.sizing.into(),
            prepend_heading_context: val.prepend_heading_context,
            topic_threshold: val.topic_threshold,
        }
    }
}

#[allow(clippy::redundant_closure, clippy::useless_conversion)]
impl From<EmbeddingConfig> for kreuzberg::EmbeddingConfig {
    fn from(val: EmbeddingConfig) -> Self {
        Self {
            model: val.model.into(),
            normalize: val.normalize,
            batch_size: val.batch_size,
            show_download_progress: val.show_download_progress,
            cache_dir: val.cache_dir.map(Into::into),
            acceleration: val.acceleration.map(Into::into),
            max_embed_duration_secs: val.max_embed_duration_secs,
        }
    }
}

#[allow(clippy::redundant_closure, clippy::useless_conversion)]
impl From<kreuzberg::EmbeddingConfig> for EmbeddingConfig {
    fn from(val: kreuzberg::EmbeddingConfig) -> Self {
        Self {
            model: val.model.into(),
            normalize: val.normalize,
            batch_size: val.batch_size,
            show_download_progress: val.show_download_progress,
            cache_dir: val.cache_dir.map(|p| p.to_string_lossy().to_string()),
            acceleration: val.acceleration.map(Into::into),
            max_embed_duration_secs: val.max_embed_duration_secs,
        }
    }
}

#[allow(clippy::redundant_closure, clippy::useless_conversion)]
impl From<TreeSitterConfig> for kreuzberg::TreeSitterConfig {
    fn from(val: TreeSitterConfig) -> Self {
        Self {
            enabled: val.enabled,
            cache_dir: val.cache_dir.map(Into::into),
            languages: val.languages.map(|v| v.into_iter().collect()),
            groups: val.groups.map(|v| v.into_iter().collect()),
            process: val.process.into(),
        }
    }
}

#[allow(clippy::redundant_closure, clippy::useless_conversion)]
impl From<kreuzberg::TreeSitterConfig> for TreeSitterConfig {
    fn from(val: kreuzberg::TreeSitterConfig) -> Self {
        Self {
            enabled: val.enabled,
            cache_dir: val.cache_dir.map(|p| p.to_string_lossy().to_string()),
            languages: val.languages.map(|v| v.into_iter().collect()),
            groups: val.groups.map(|v| v.into_iter().collect()),
            process: val.process.into(),
        }
    }
}

#[allow(clippy::redundant_closure, clippy::useless_conversion)]
impl From<TreeSitterProcessConfig> for kreuzberg::TreeSitterProcessConfig {
    fn from(val: TreeSitterProcessConfig) -> Self {
        Self {
            structure: val.structure,
            imports: val.imports,
            exports: val.exports,
            comments: val.comments,
            docstrings: val.docstrings,
            symbols: val.symbols,
            diagnostics: val.diagnostics,
            chunk_max_size: val.chunk_max_size,
            content_mode: val.content_mode.into(),
        }
    }
}

#[allow(clippy::redundant_closure, clippy::useless_conversion)]
impl From<kreuzberg::TreeSitterProcessConfig> for TreeSitterProcessConfig {
    fn from(val: kreuzberg::TreeSitterProcessConfig) -> Self {
        Self {
            structure: val.structure,
            imports: val.imports,
            exports: val.exports,
            comments: val.comments,
            docstrings: val.docstrings,
            symbols: val.symbols,
            diagnostics: val.diagnostics,
            chunk_max_size: val.chunk_max_size,
            content_mode: val.content_mode.into(),
        }
    }
}

#[allow(clippy::redundant_closure, clippy::useless_conversion)]
impl From<ServerConfig> for kreuzberg::ServerConfig {
    fn from(val: ServerConfig) -> Self {
        Self {
            host: val.host,
            port: val.port,
            cors_origins: val.cors_origins.into_iter().collect(),
            max_request_body_bytes: val.max_request_body_bytes,
            max_multipart_field_bytes: val.max_multipart_field_bytes,
        }
    }
}

#[allow(clippy::redundant_closure, clippy::useless_conversion)]
impl From<kreuzberg::ServerConfig> for ServerConfig {
    fn from(val: kreuzberg::ServerConfig) -> Self {
        Self {
            host: val.host,
            port: val.port,
            cors_origins: val.cors_origins.into_iter().collect(),
            max_request_body_bytes: val.max_request_body_bytes,
            max_multipart_field_bytes: val.max_multipart_field_bytes,
        }
    }
}

#[allow(clippy::redundant_closure, clippy::useless_conversion)]
impl From<DocxAppProperties> for kreuzberg::DocxAppProperties {
    fn from(val: DocxAppProperties) -> Self {
        Self {
            application: val.application,
            app_version: val.app_version,
            template: val.template,
            total_time: val.total_time,
            pages: val.pages,
            words: val.words,
            characters: val.characters,
            characters_with_spaces: val.characters_with_spaces,
            lines: val.lines,
            paragraphs: val.paragraphs,
            company: val.company,
            doc_security: val.doc_security,
            scale_crop: val.scale_crop,
            links_up_to_date: val.links_up_to_date,
            shared_doc: val.shared_doc,
            hyperlinks_changed: val.hyperlinks_changed,
        }
    }
}

#[allow(clippy::redundant_closure, clippy::useless_conversion)]
impl From<kreuzberg::DocxAppProperties> for DocxAppProperties {
    fn from(val: kreuzberg::DocxAppProperties) -> Self {
        Self {
            application: val.application,
            app_version: val.app_version,
            template: val.template,
            total_time: val.total_time,
            pages: val.pages,
            words: val.words,
            characters: val.characters,
            characters_with_spaces: val.characters_with_spaces,
            lines: val.lines,
            paragraphs: val.paragraphs,
            company: val.company,
            doc_security: val.doc_security,
            scale_crop: val.scale_crop,
            links_up_to_date: val.links_up_to_date,
            shared_doc: val.shared_doc,
            hyperlinks_changed: val.hyperlinks_changed,
        }
    }
}

#[allow(clippy::redundant_closure, clippy::useless_conversion)]
impl From<CoreProperties> for kreuzberg::CoreProperties {
    fn from(val: CoreProperties) -> Self {
        Self {
            title: val.title,
            subject: val.subject,
            creator: val.creator,
            keywords: val.keywords,
            description: val.description,
            last_modified_by: val.last_modified_by,
            revision: val.revision,
            created: val.created,
            modified: val.modified,
            category: val.category,
            content_status: val.content_status,
            language: val.language,
            identifier: val.identifier,
            version: val.version,
            last_printed: val.last_printed,
        }
    }
}

#[allow(clippy::redundant_closure, clippy::useless_conversion)]
impl From<kreuzberg::CoreProperties> for CoreProperties {
    fn from(val: kreuzberg::CoreProperties) -> Self {
        Self {
            title: val.title,
            subject: val.subject,
            creator: val.creator,
            keywords: val.keywords,
            description: val.description,
            last_modified_by: val.last_modified_by,
            revision: val.revision,
            created: val.created,
            modified: val.modified,
            category: val.category,
            content_status: val.content_status,
            language: val.language,
            identifier: val.identifier,
            version: val.version,
            last_printed: val.last_printed,
        }
    }
}

#[allow(clippy::redundant_closure, clippy::useless_conversion)]
impl From<SecurityLimits> for kreuzberg::SecurityLimits {
    fn from(val: SecurityLimits) -> Self {
        Self {
            max_archive_size: val.max_archive_size,
            max_compression_ratio: val.max_compression_ratio,
            max_files_in_archive: val.max_files_in_archive,
            max_nesting_depth: val.max_nesting_depth,
            max_entity_length: val.max_entity_length,
            max_content_size: val.max_content_size,
            max_iterations: val.max_iterations,
            max_xml_depth: val.max_xml_depth,
            max_table_cells: val.max_table_cells,
        }
    }
}

#[allow(clippy::redundant_closure, clippy::useless_conversion)]
impl From<kreuzberg::SecurityLimits> for SecurityLimits {
    fn from(val: kreuzberg::SecurityLimits) -> Self {
        Self {
            max_archive_size: val.max_archive_size,
            max_compression_ratio: val.max_compression_ratio,
            max_files_in_archive: val.max_files_in_archive,
            max_nesting_depth: val.max_nesting_depth,
            max_entity_length: val.max_entity_length,
            max_content_size: val.max_content_size,
            max_iterations: val.max_iterations,
            max_xml_depth: val.max_xml_depth,
            max_table_cells: val.max_table_cells,
        }
    }
}

#[allow(clippy::redundant_closure, clippy::useless_conversion)]
impl From<TokenReductionConfig> for kreuzberg::TokenReductionConfig {
    fn from(val: TokenReductionConfig) -> Self {
        Self {
            level: val.level.into(),
            language_hint: val.language_hint,
            preserve_markdown: val.preserve_markdown,
            preserve_code: val.preserve_code,
            semantic_threshold: val.semantic_threshold,
            enable_parallel: val.enable_parallel,
            use_simd: val.use_simd,
            custom_stopwords: val.custom_stopwords.map(|m| m.into_iter().collect()),
            preserve_patterns: val.preserve_patterns.into_iter().collect(),
            target_reduction: val.target_reduction,
            enable_semantic_clustering: val.enable_semantic_clustering,
        }
    }
}

#[allow(clippy::redundant_closure, clippy::useless_conversion)]
impl From<kreuzberg::TokenReductionConfig> for TokenReductionConfig {
    fn from(val: kreuzberg::TokenReductionConfig) -> Self {
        Self {
            level: val.level.into(),
            language_hint: val.language_hint,
            preserve_markdown: val.preserve_markdown,
            preserve_code: val.preserve_code,
            semantic_threshold: val.semantic_threshold,
            enable_parallel: val.enable_parallel,
            use_simd: val.use_simd,
            custom_stopwords: val.custom_stopwords.map(|m| m.into_iter().collect()),
            preserve_patterns: val.preserve_patterns.into_iter().collect(),
            target_reduction: val.target_reduction,
            enable_semantic_clustering: val.enable_semantic_clustering,
        }
    }
}

#[allow(clippy::redundant_closure, clippy::useless_conversion)]
impl From<PdfAnnotation> for kreuzberg::PdfAnnotation {
    fn from(val: PdfAnnotation) -> Self {
        Self {
            annotation_type: val.annotation_type.into(),
            content: val.content,
            page_number: val.page_number,
            bounding_box: val.bounding_box.map(Into::into),
        }
    }
}

#[allow(clippy::redundant_closure, clippy::useless_conversion)]
impl From<kreuzberg::PdfAnnotation> for PdfAnnotation {
    fn from(val: kreuzberg::PdfAnnotation) -> Self {
        Self {
            annotation_type: val.annotation_type.into(),
            content: val.content,
            page_number: val.page_number,
            bounding_box: val.bounding_box.map(Into::into),
        }
    }
}

#[allow(clippy::redundant_closure, clippy::useless_conversion)]
impl From<DjotContent> for kreuzberg::DjotContent {
    fn from(val: DjotContent) -> Self {
        Self {
            plain_text: val.plain_text,
            blocks: val.blocks.into_iter().map(Into::into).collect(),
            metadata: val.metadata.into(),
            tables: val.tables.into_iter().map(Into::into).collect(),
            images: val.images.into_iter().map(Into::into).collect(),
            links: val.links.into_iter().map(Into::into).collect(),
            footnotes: val.footnotes.into_iter().map(Into::into).collect(),
            attributes: Default::default(),
        }
    }
}

#[allow(clippy::redundant_closure, clippy::useless_conversion)]
impl From<kreuzberg::DjotContent> for DjotContent {
    fn from(val: kreuzberg::DjotContent) -> Self {
        Self {
            plain_text: val.plain_text,
            blocks: val.blocks.into_iter().map(Into::into).collect(),
            metadata: val.metadata.into(),
            tables: val.tables.into_iter().map(Into::into).collect(),
            images: val.images.into_iter().map(Into::into).collect(),
            links: val.links.into_iter().map(Into::into).collect(),
            footnotes: val.footnotes.into_iter().map(Into::into).collect(),
            attributes: val.attributes.iter().map(|i| format!("{:?}", i)).collect(),
        }
    }
}

#[allow(clippy::redundant_closure, clippy::useless_conversion)]
impl From<FormattedBlock> for kreuzberg::FormattedBlock {
    fn from(val: FormattedBlock) -> Self {
        Self {
            block_type: val.block_type.into(),
            level: val.level,
            inline_content: val.inline_content.into_iter().map(Into::into).collect(),
            attributes: Default::default(),
            language: val.language,
            code: val.code,
            children: val.children.into_iter().map(Into::into).collect(),
        }
    }
}

#[allow(clippy::redundant_closure, clippy::useless_conversion)]
impl From<kreuzberg::FormattedBlock> for FormattedBlock {
    fn from(val: kreuzberg::FormattedBlock) -> Self {
        Self {
            block_type: val.block_type.into(),
            level: val.level,
            inline_content: val.inline_content.into_iter().map(Into::into).collect(),
            attributes: val.attributes.as_ref().map(|v| format!("{v:?}")),
            language: val.language,
            code: val.code,
            children: val.children.into_iter().map(Into::into).collect(),
        }
    }
}

#[allow(clippy::redundant_closure, clippy::useless_conversion)]
impl From<InlineElement> for kreuzberg::InlineElement {
    fn from(val: InlineElement) -> Self {
        Self {
            element_type: val.element_type.into(),
            content: val.content,
            attributes: Default::default(),
            metadata: val
                .metadata
                .map(|m| m.into_iter().map(|(k, v)| (k.into(), v.into())).collect()),
        }
    }
}

#[allow(clippy::redundant_closure, clippy::useless_conversion)]
impl From<kreuzberg::InlineElement> for InlineElement {
    fn from(val: kreuzberg::InlineElement) -> Self {
        Self {
            element_type: val.element_type.into(),
            content: val.content,
            attributes: val.attributes.as_ref().map(|v| format!("{v:?}")),
            metadata: val
                .metadata
                .map(|m| m.into_iter().map(|(k, v)| (k.into(), v.into())).collect()),
        }
    }
}

#[allow(clippy::redundant_closure, clippy::useless_conversion)]
impl From<DjotImage> for kreuzberg::DjotImage {
    fn from(val: DjotImage) -> Self {
        Self {
            src: val.src,
            alt: val.alt,
            title: val.title,
            attributes: Default::default(),
        }
    }
}

#[allow(clippy::redundant_closure, clippy::useless_conversion)]
impl From<kreuzberg::DjotImage> for DjotImage {
    fn from(val: kreuzberg::DjotImage) -> Self {
        Self {
            src: val.src,
            alt: val.alt,
            title: val.title,
            attributes: val.attributes.as_ref().map(|v| format!("{v:?}")),
        }
    }
}

#[allow(clippy::redundant_closure, clippy::useless_conversion)]
impl From<DjotLink> for kreuzberg::DjotLink {
    fn from(val: DjotLink) -> Self {
        Self {
            url: val.url,
            text: val.text,
            title: val.title,
            attributes: Default::default(),
        }
    }
}

#[allow(clippy::redundant_closure, clippy::useless_conversion)]
impl From<kreuzberg::DjotLink> for DjotLink {
    fn from(val: kreuzberg::DjotLink) -> Self {
        Self {
            url: val.url,
            text: val.text,
            title: val.title,
            attributes: val.attributes.as_ref().map(|v| format!("{v:?}")),
        }
    }
}

#[allow(clippy::redundant_closure, clippy::useless_conversion)]
impl From<Footnote> for kreuzberg::Footnote {
    fn from(val: Footnote) -> Self {
        Self {
            label: val.label,
            content: val.content.into_iter().map(Into::into).collect(),
        }
    }
}

#[allow(clippy::redundant_closure, clippy::useless_conversion)]
impl From<kreuzberg::Footnote> for Footnote {
    fn from(val: kreuzberg::Footnote) -> Self {
        Self {
            label: val.label,
            content: val.content.into_iter().map(Into::into).collect(),
        }
    }
}

#[allow(clippy::redundant_closure, clippy::useless_conversion)]
impl From<DocumentStructure> for kreuzberg::DocumentStructure {
    fn from(val: DocumentStructure) -> Self {
        Self {
            nodes: val.nodes.into_iter().map(Into::into).collect(),
            source_format: val.source_format,
            relationships: val.relationships.into_iter().map(Into::into).collect(),
            node_types: val.node_types.into_iter().collect(),
        }
    }
}

#[allow(clippy::redundant_closure, clippy::useless_conversion)]
impl From<kreuzberg::DocumentStructure> for DocumentStructure {
    fn from(val: kreuzberg::DocumentStructure) -> Self {
        Self {
            nodes: val.nodes.into_iter().map(Into::into).collect(),
            source_format: val.source_format,
            relationships: val.relationships.into_iter().map(Into::into).collect(),
            node_types: val.node_types.into_iter().collect(),
        }
    }
}

#[allow(clippy::redundant_closure, clippy::useless_conversion)]
impl From<DocumentRelationship> for kreuzberg::DocumentRelationship {
    fn from(val: DocumentRelationship) -> Self {
        Self {
            source: kreuzberg::NodeIndex(val.source),
            target: kreuzberg::NodeIndex(val.target),
            kind: val.kind.into(),
        }
    }
}

#[allow(clippy::redundant_closure, clippy::useless_conversion)]
impl From<kreuzberg::DocumentRelationship> for DocumentRelationship {
    fn from(val: kreuzberg::DocumentRelationship) -> Self {
        Self {
            source: val.source.0,
            target: val.target.0,
            kind: val.kind.into(),
        }
    }
}

#[allow(clippy::redundant_closure, clippy::useless_conversion)]
impl From<DocumentNode> for kreuzberg::DocumentNode {
    fn from(val: DocumentNode) -> Self {
        Self {
            id: Default::default(),
            content: val.content.into(),
            parent: (val.parent).map(kreuzberg::NodeIndex),
            children: (val.children.into_iter().collect::<Vec<_>>())
                .into_iter()
                .map(kreuzberg::NodeIndex)
                .collect(),
            content_layer: val.content_layer.into(),
            page: val.page,
            page_end: val.page_end,
            bbox: val.bbox.map(Into::into),
            annotations: val.annotations.into_iter().map(Into::into).collect(),
            attributes: val
                .attributes
                .map(|m| m.into_iter().map(|(k, v)| (k.into(), v.into())).collect()),
        }
    }
}

#[allow(clippy::redundant_closure, clippy::useless_conversion)]
impl From<kreuzberg::DocumentNode> for DocumentNode {
    fn from(val: kreuzberg::DocumentNode) -> Self {
        Self {
            id: format!("{:?}", val.id),
            content: val.content.into(),
            parent: val.parent.map(|v| v.0),
            children: val
                .children
                .iter()
                .map(|v| v.0)
                .collect::<Vec<_>>()
                .into_iter()
                .collect(),
            content_layer: val.content_layer.into(),
            page: val.page,
            page_end: val.page_end,
            bbox: val.bbox.map(Into::into),
            annotations: val.annotations.into_iter().map(Into::into).collect(),
            attributes: val
                .attributes
                .map(|m| m.into_iter().map(|(k, v)| (k.into(), v.into())).collect()),
        }
    }
}

#[allow(clippy::redundant_closure, clippy::useless_conversion)]
impl From<TableGrid> for kreuzberg::TableGrid {
    fn from(val: TableGrid) -> Self {
        Self {
            rows: val.rows,
            cols: val.cols,
            cells: val.cells.into_iter().map(Into::into).collect(),
        }
    }
}

#[allow(clippy::redundant_closure, clippy::useless_conversion)]
impl From<kreuzberg::TableGrid> for TableGrid {
    fn from(val: kreuzberg::TableGrid) -> Self {
        Self {
            rows: val.rows,
            cols: val.cols,
            cells: val.cells.into_iter().map(Into::into).collect(),
        }
    }
}

#[allow(clippy::redundant_closure, clippy::useless_conversion)]
impl From<GridCell> for kreuzberg::GridCell {
    fn from(val: GridCell) -> Self {
        Self {
            content: val.content,
            row: val.row,
            col: val.col,
            row_span: val.row_span,
            col_span: val.col_span,
            is_header: val.is_header,
            bbox: val.bbox.map(Into::into),
        }
    }
}

#[allow(clippy::redundant_closure, clippy::useless_conversion)]
impl From<kreuzberg::GridCell> for GridCell {
    fn from(val: kreuzberg::GridCell) -> Self {
        Self {
            content: val.content,
            row: val.row,
            col: val.col,
            row_span: val.row_span,
            col_span: val.col_span,
            is_header: val.is_header,
            bbox: val.bbox.map(Into::into),
        }
    }
}

#[allow(clippy::redundant_closure, clippy::useless_conversion)]
impl From<TextAnnotation> for kreuzberg::TextAnnotation {
    fn from(val: TextAnnotation) -> Self {
        Self {
            start: val.start,
            end: val.end,
            kind: val.kind.into(),
        }
    }
}

#[allow(clippy::redundant_closure, clippy::useless_conversion)]
impl From<kreuzberg::TextAnnotation> for TextAnnotation {
    fn from(val: kreuzberg::TextAnnotation) -> Self {
        Self {
            start: val.start,
            end: val.end,
            kind: val.kind.into(),
        }
    }
}

#[allow(clippy::needless_update)]
#[allow(clippy::redundant_closure, clippy::useless_conversion)]
impl From<ExtractionResult> for kreuzberg::ExtractionResult {
    fn from(val: ExtractionResult) -> Self {
        Self {
            content: val.content,
            mime_type: val.mime_type.into(),
            metadata: val.metadata.into(),
            extraction_method: val.extraction_method.map(Into::into),
            tables: val.tables.into_iter().map(Into::into).collect(),
            detected_languages: val.detected_languages.map(|v| v.into_iter().collect()),
            chunks: val.chunks.map(|v| v.into_iter().map(Into::into).collect()),
            images: val.images.map(|v| v.into_iter().map(Into::into).collect()),
            pages: val.pages.map(|v| v.into_iter().map(Into::into).collect()),
            elements: val.elements.map(|v| v.into_iter().map(Into::into).collect()),
            djot_content: val.djot_content.map(Into::into),
            ocr_elements: val.ocr_elements.map(|v| v.into_iter().map(Into::into).collect()),
            document: val.document.map(Into::into),
            extracted_keywords: val.extracted_keywords.map(|v| v.into_iter().map(Into::into).collect()),
            quality_score: val.quality_score,
            processing_warnings: val.processing_warnings.into_iter().map(Into::into).collect(),
            annotations: val.annotations.map(|v| v.into_iter().map(Into::into).collect()),
            children: val.children.map(|v| v.into_iter().map(Into::into).collect()),
            uris: val.uris.map(|v| v.into_iter().map(Into::into).collect()),
            revisions: val.revisions.map(|v| v.into_iter().map(Into::into).collect()),
            structured_output: val
                .structured_output
                .as_ref()
                .and_then(|s| serde_json::from_str(s).ok()),
            code_intelligence: val
                .code_intelligence
                .as_ref()
                .and_then(|s| serde_json::from_str(s).ok()),
            llm_usage: val.llm_usage.map(|v| v.into_iter().map(Into::into).collect()),
            formatted_content: val.formatted_content,
            ocr_internal_document: Default::default(),
            ..Default::default()
        }
    }
}

#[allow(clippy::redundant_closure, clippy::useless_conversion)]
impl From<kreuzberg::ExtractionResult> for ExtractionResult {
    fn from(val: kreuzberg::ExtractionResult) -> Self {
        Self {
            content: val.content,
            mime_type: val.mime_type.to_string(),
            metadata: val.metadata.into(),
            extraction_method: val.extraction_method.map(Into::into),
            tables: val.tables.into_iter().map(Into::into).collect(),
            detected_languages: val.detected_languages.map(|v| v.into_iter().collect()),
            chunks: val.chunks.map(|v| v.into_iter().map(Into::into).collect()),
            images: val.images.map(|v| v.into_iter().map(Into::into).collect()),
            pages: val.pages.map(|v| v.into_iter().map(Into::into).collect()),
            elements: val.elements.map(|v| v.into_iter().map(Into::into).collect()),
            djot_content: val.djot_content.map(Into::into),
            ocr_elements: val.ocr_elements.map(|v| v.into_iter().map(Into::into).collect()),
            document: val.document.map(Into::into),
            extracted_keywords: val.extracted_keywords.map(|v| v.into_iter().map(Into::into).collect()),
            quality_score: val.quality_score,
            processing_warnings: val.processing_warnings.into_iter().map(Into::into).collect(),
            annotations: val.annotations.map(|v| v.into_iter().map(Into::into).collect()),
            children: val.children.map(|v| v.into_iter().map(Into::into).collect()),
            uris: val.uris.map(|v| v.into_iter().map(Into::into).collect()),
            revisions: val.revisions.map(|v| v.into_iter().map(Into::into).collect()),
            structured_output: val.structured_output.as_ref().map(ToString::to_string),
            code_intelligence: val.code_intelligence.as_ref().map(ToString::to_string),
            llm_usage: val.llm_usage.map(|v| v.into_iter().map(Into::into).collect()),
            formatted_content: val.formatted_content,
            ocr_internal_document: val.ocr_internal_document.as_ref().map(|v| format!("{v:?}")),
        }
    }
}

#[allow(clippy::redundant_closure, clippy::useless_conversion)]
impl From<ArchiveEntry> for kreuzberg::ArchiveEntry {
    fn from(val: ArchiveEntry) -> Self {
        Self {
            path: val.path,
            mime_type: val.mime_type,
            result: Box::new(val.result.into()),
        }
    }
}

#[allow(clippy::redundant_closure, clippy::useless_conversion)]
impl From<kreuzberg::ArchiveEntry> for ArchiveEntry {
    fn from(val: kreuzberg::ArchiveEntry) -> Self {
        Self {
            path: val.path,
            mime_type: val.mime_type,
            result: (*val.result).into(),
        }
    }
}

#[allow(clippy::redundant_closure, clippy::useless_conversion)]
impl From<ProcessingWarning> for kreuzberg::ProcessingWarning {
    fn from(val: ProcessingWarning) -> Self {
        Self {
            source: val.source.into(),
            message: val.message.into(),
        }
    }
}

#[allow(clippy::redundant_closure, clippy::useless_conversion)]
impl From<kreuzberg::ProcessingWarning> for ProcessingWarning {
    fn from(val: kreuzberg::ProcessingWarning) -> Self {
        Self {
            source: val.source.to_string(),
            message: val.message.to_string(),
        }
    }
}

#[allow(clippy::redundant_closure, clippy::useless_conversion)]
impl From<LlmUsage> for kreuzberg::LlmUsage {
    fn from(val: LlmUsage) -> Self {
        Self {
            model: val.model,
            source: val.source,
            input_tokens: val.input_tokens,
            output_tokens: val.output_tokens,
            total_tokens: val.total_tokens,
            estimated_cost: val.estimated_cost,
            finish_reason: val.finish_reason,
        }
    }
}

#[allow(clippy::redundant_closure, clippy::useless_conversion)]
impl From<kreuzberg::LlmUsage> for LlmUsage {
    fn from(val: kreuzberg::LlmUsage) -> Self {
        Self {
            model: val.model,
            source: val.source,
            input_tokens: val.input_tokens,
            output_tokens: val.output_tokens,
            total_tokens: val.total_tokens,
            estimated_cost: val.estimated_cost,
            finish_reason: val.finish_reason,
        }
    }
}

#[allow(clippy::redundant_closure, clippy::useless_conversion)]
impl From<Chunk> for kreuzberg::Chunk {
    fn from(val: Chunk) -> Self {
        Self {
            content: val.content,
            chunk_type: val.chunk_type.into(),
            embedding: val.embedding.map(|v| v.into_iter().collect()),
            metadata: val.metadata.into(),
        }
    }
}

#[allow(clippy::redundant_closure, clippy::useless_conversion)]
impl From<kreuzberg::Chunk> for Chunk {
    fn from(val: kreuzberg::Chunk) -> Self {
        Self {
            content: val.content,
            chunk_type: val.chunk_type.into(),
            embedding: val.embedding.map(|v| v.into_iter().collect()),
            metadata: val.metadata.into(),
        }
    }
}

#[allow(clippy::redundant_closure, clippy::useless_conversion)]
impl From<HeadingContext> for kreuzberg::HeadingContext {
    fn from(val: HeadingContext) -> Self {
        Self {
            headings: val.headings.into_iter().map(Into::into).collect(),
        }
    }
}

#[allow(clippy::redundant_closure, clippy::useless_conversion)]
impl From<kreuzberg::HeadingContext> for HeadingContext {
    fn from(val: kreuzberg::HeadingContext) -> Self {
        Self {
            headings: val.headings.into_iter().map(Into::into).collect(),
        }
    }
}

#[allow(clippy::redundant_closure, clippy::useless_conversion)]
impl From<HeadingLevel> for kreuzberg::HeadingLevel {
    fn from(val: HeadingLevel) -> Self {
        Self {
            level: val.level,
            text: val.text,
        }
    }
}

#[allow(clippy::redundant_closure, clippy::useless_conversion)]
impl From<kreuzberg::HeadingLevel> for HeadingLevel {
    fn from(val: kreuzberg::HeadingLevel) -> Self {
        Self {
            level: val.level,
            text: val.text,
        }
    }
}

#[allow(clippy::redundant_closure, clippy::useless_conversion)]
impl From<ChunkMetadata> for kreuzberg::ChunkMetadata {
    fn from(val: ChunkMetadata) -> Self {
        Self {
            byte_start: val.byte_start,
            byte_end: val.byte_end,
            token_count: val.token_count,
            chunk_index: val.chunk_index,
            total_chunks: val.total_chunks,
            first_page: val.first_page,
            last_page: val.last_page,
            heading_context: val.heading_context.map(Into::into),
            image_indices: val.image_indices.into_iter().collect(),
        }
    }
}

#[allow(clippy::redundant_closure, clippy::useless_conversion)]
impl From<kreuzberg::ChunkMetadata> for ChunkMetadata {
    fn from(val: kreuzberg::ChunkMetadata) -> Self {
        Self {
            byte_start: val.byte_start,
            byte_end: val.byte_end,
            token_count: val.token_count,
            chunk_index: val.chunk_index,
            total_chunks: val.total_chunks,
            first_page: val.first_page,
            last_page: val.last_page,
            heading_context: val.heading_context.map(Into::into),
            image_indices: val.image_indices.into_iter().collect(),
        }
    }
}

#[allow(clippy::redundant_closure, clippy::useless_conversion)]
impl From<ExtractedImage> for kreuzberg::ExtractedImage {
    fn from(val: ExtractedImage) -> Self {
        Self {
            data: val.data.to_vec().into(),
            format: val.format.into(),
            image_index: val.image_index,
            page_number: val.page_number,
            width: val.width,
            height: val.height,
            colorspace: val.colorspace,
            bits_per_component: val.bits_per_component,
            is_mask: val.is_mask,
            description: val.description,
            ocr_result: val.ocr_result.map(Into::into).map(Box::new),
            bounding_box: val.bounding_box.map(Into::into),
            source_path: val.source_path,
            image_kind: val.image_kind.map(Into::into),
            kind_confidence: val.kind_confidence,
            cluster_id: val.cluster_id,
        }
    }
}

#[allow(clippy::redundant_closure, clippy::useless_conversion)]
impl From<kreuzberg::ExtractedImage> for ExtractedImage {
    fn from(val: kreuzberg::ExtractedImage) -> Self {
        Self {
            data: val.data.to_vec().into(),
            format: val.format.to_string(),
            image_index: val.image_index,
            page_number: val.page_number,
            width: val.width,
            height: val.height,
            colorspace: val.colorspace,
            bits_per_component: val.bits_per_component,
            is_mask: val.is_mask,
            description: val.description,
            ocr_result: val.ocr_result.map(|v| (*v).into()),
            bounding_box: val.bounding_box.map(Into::into),
            source_path: val.source_path,
            image_kind: val.image_kind.map(Into::into),
            kind_confidence: val.kind_confidence,
            cluster_id: val.cluster_id,
        }
    }
}

#[allow(clippy::redundant_closure, clippy::useless_conversion)]
impl From<BoundingBox> for kreuzberg::BoundingBox {
    fn from(val: BoundingBox) -> Self {
        Self {
            x0: val.x0,
            y0: val.y0,
            x1: val.x1,
            y1: val.y1,
        }
    }
}

#[allow(clippy::redundant_closure, clippy::useless_conversion)]
impl From<kreuzberg::BoundingBox> for BoundingBox {
    fn from(val: kreuzberg::BoundingBox) -> Self {
        Self {
            x0: val.x0,
            y0: val.y0,
            x1: val.x1,
            y1: val.y1,
        }
    }
}

#[allow(clippy::redundant_closure, clippy::useless_conversion)]
impl From<ElementMetadata> for kreuzberg::ElementMetadata {
    fn from(val: ElementMetadata) -> Self {
        Self {
            page_number: val.page_number,
            filename: val.filename,
            coordinates: val.coordinates.map(Into::into),
            element_index: val.element_index,
            additional: val.additional.into_iter().map(|(k, v)| (k.into(), v.into())).collect(),
        }
    }
}

#[allow(clippy::redundant_closure, clippy::useless_conversion)]
impl From<kreuzberg::ElementMetadata> for ElementMetadata {
    fn from(val: kreuzberg::ElementMetadata) -> Self {
        Self {
            page_number: val.page_number,
            filename: val.filename,
            coordinates: val.coordinates.map(Into::into),
            element_index: val.element_index,
            additional: val.additional.into_iter().map(|(k, v)| (k.into(), v.into())).collect(),
        }
    }
}

#[allow(clippy::redundant_closure, clippy::useless_conversion)]
impl From<Element> for kreuzberg::Element {
    fn from(val: Element) -> Self {
        Self {
            element_id: Default::default(),
            element_type: val.element_type.into(),
            text: val.text,
            metadata: val.metadata.into(),
        }
    }
}

#[allow(clippy::redundant_closure, clippy::useless_conversion)]
impl From<kreuzberg::Element> for Element {
    fn from(val: kreuzberg::Element) -> Self {
        Self {
            element_id: format!("{:?}", val.element_id),
            element_type: val.element_type.into(),
            text: val.text,
            metadata: val.metadata.into(),
        }
    }
}

#[allow(clippy::redundant_closure, clippy::useless_conversion)]
impl From<OcrExtractionResult> for kreuzberg::OcrExtractionResult {
    fn from(val: OcrExtractionResult) -> Self {
        Self {
            content: val.content,
            mime_type: val.mime_type,
            metadata: val
                .metadata
                .into_iter()
                .map(|(k, v)| (k.into(), serde_json::from_str(&v).unwrap_or_default()))
                .collect(),
            tables: val.tables.into_iter().map(Into::into).collect(),
            ocr_elements: val.ocr_elements.map(|v| v.into_iter().map(Into::into).collect()),
            internal_document: Default::default(),
        }
    }
}

#[allow(clippy::redundant_closure, clippy::useless_conversion)]
impl From<kreuzberg::OcrExtractionResult> for OcrExtractionResult {
    fn from(val: kreuzberg::OcrExtractionResult) -> Self {
        Self {
            content: val.content,
            mime_type: val.mime_type,
            metadata: val
                .metadata
                .into_iter()
                .map(|(k, v)| (k.to_string(), v.to_string()))
                .collect(),
            tables: val.tables.into_iter().map(Into::into).collect(),
            ocr_elements: val.ocr_elements.map(|v| v.into_iter().map(Into::into).collect()),
            internal_document: val.internal_document.as_ref().map(|v| format!("{v:?}")),
        }
    }
}

#[allow(clippy::redundant_closure, clippy::useless_conversion)]
impl From<OcrTable> for kreuzberg::OcrTable {
    fn from(val: OcrTable) -> Self {
        Self {
            cells: val.cells.into_iter().collect(),
            markdown: val.markdown,
            page_number: val.page_number,
            bounding_box: val.bounding_box.map(Into::into),
        }
    }
}

#[allow(clippy::redundant_closure, clippy::useless_conversion)]
impl From<kreuzberg::OcrTable> for OcrTable {
    fn from(val: kreuzberg::OcrTable) -> Self {
        Self {
            cells: val.cells.into_iter().collect(),
            markdown: val.markdown,
            page_number: val.page_number,
            bounding_box: val.bounding_box.map(Into::into),
        }
    }
}

#[allow(clippy::redundant_closure, clippy::useless_conversion)]
impl From<OcrTableBoundingBox> for kreuzberg::OcrTableBoundingBox {
    fn from(val: OcrTableBoundingBox) -> Self {
        Self {
            left: val.left,
            top: val.top,
            right: val.right,
            bottom: val.bottom,
        }
    }
}

#[allow(clippy::redundant_closure, clippy::useless_conversion)]
impl From<kreuzberg::OcrTableBoundingBox> for OcrTableBoundingBox {
    fn from(val: kreuzberg::OcrTableBoundingBox) -> Self {
        Self {
            left: val.left,
            top: val.top,
            right: val.right,
            bottom: val.bottom,
        }
    }
}

#[allow(clippy::redundant_closure, clippy::useless_conversion)]
impl From<ImagePreprocessingConfig> for kreuzberg::ImagePreprocessingConfig {
    fn from(val: ImagePreprocessingConfig) -> Self {
        Self {
            target_dpi: val.target_dpi,
            auto_rotate: val.auto_rotate,
            deskew: val.deskew,
            denoise: val.denoise,
            contrast_enhance: val.contrast_enhance,
            binarization_method: val.binarization_method,
            invert_colors: val.invert_colors,
        }
    }
}

#[allow(clippy::redundant_closure, clippy::useless_conversion)]
impl From<kreuzberg::ImagePreprocessingConfig> for ImagePreprocessingConfig {
    fn from(val: kreuzberg::ImagePreprocessingConfig) -> Self {
        Self {
            target_dpi: val.target_dpi,
            auto_rotate: val.auto_rotate,
            deskew: val.deskew,
            denoise: val.denoise,
            contrast_enhance: val.contrast_enhance,
            binarization_method: val.binarization_method,
            invert_colors: val.invert_colors,
        }
    }
}

#[allow(clippy::redundant_closure, clippy::useless_conversion)]
impl From<TesseractConfig> for kreuzberg::TesseractConfig {
    fn from(val: TesseractConfig) -> Self {
        Self {
            language: val.language,
            psm: val.psm,
            output_format: val.output_format,
            oem: val.oem,
            min_confidence: val.min_confidence,
            preprocessing: val.preprocessing.map(Into::into),
            enable_table_detection: val.enable_table_detection,
            table_min_confidence: val.table_min_confidence,
            table_column_threshold: val.table_column_threshold,
            table_row_threshold_ratio: val.table_row_threshold_ratio,
            use_cache: val.use_cache,
            classify_use_pre_adapted_templates: val.classify_use_pre_adapted_templates,
            language_model_ngram_on: val.language_model_ngram_on,
            tessedit_dont_blkrej_good_wds: val.tessedit_dont_blkrej_good_wds,
            tessedit_dont_rowrej_good_wds: val.tessedit_dont_rowrej_good_wds,
            tessedit_enable_dict_correction: val.tessedit_enable_dict_correction,
            tessedit_char_whitelist: val.tessedit_char_whitelist,
            tessedit_char_blacklist: val.tessedit_char_blacklist,
            tessedit_use_primary_params_model: val.tessedit_use_primary_params_model,
            textord_space_size_is_variable: val.textord_space_size_is_variable,
            thresholding_method: val.thresholding_method,
        }
    }
}

#[allow(clippy::redundant_closure, clippy::useless_conversion)]
impl From<kreuzberg::TesseractConfig> for TesseractConfig {
    fn from(val: kreuzberg::TesseractConfig) -> Self {
        Self {
            language: val.language,
            psm: val.psm,
            output_format: val.output_format,
            oem: val.oem,
            min_confidence: val.min_confidence,
            preprocessing: val.preprocessing.map(Into::into),
            enable_table_detection: val.enable_table_detection,
            table_min_confidence: val.table_min_confidence,
            table_column_threshold: val.table_column_threshold,
            table_row_threshold_ratio: val.table_row_threshold_ratio,
            use_cache: val.use_cache,
            classify_use_pre_adapted_templates: val.classify_use_pre_adapted_templates,
            language_model_ngram_on: val.language_model_ngram_on,
            tessedit_dont_blkrej_good_wds: val.tessedit_dont_blkrej_good_wds,
            tessedit_dont_rowrej_good_wds: val.tessedit_dont_rowrej_good_wds,
            tessedit_enable_dict_correction: val.tessedit_enable_dict_correction,
            tessedit_char_whitelist: val.tessedit_char_whitelist,
            tessedit_char_blacklist: val.tessedit_char_blacklist,
            tessedit_use_primary_params_model: val.tessedit_use_primary_params_model,
            textord_space_size_is_variable: val.textord_space_size_is_variable,
            thresholding_method: val.thresholding_method,
        }
    }
}

#[allow(clippy::redundant_closure, clippy::useless_conversion)]
impl From<ImagePreprocessingMetadata> for kreuzberg::ImagePreprocessingMetadata {
    fn from(val: ImagePreprocessingMetadata) -> Self {
        Self {
            original_dimensions: Default::default(),
            original_dpi: Default::default(),
            target_dpi: val.target_dpi,
            scale_factor: val.scale_factor,
            auto_adjusted: val.auto_adjusted,
            final_dpi: val.final_dpi,
            new_dimensions: Default::default(),
            resample_method: val.resample_method,
            dimension_clamped: val.dimension_clamped,
            calculated_dpi: val.calculated_dpi,
            skipped_resize: val.skipped_resize,
            resize_error: val.resize_error,
        }
    }
}

#[allow(clippy::redundant_closure, clippy::useless_conversion)]
impl From<kreuzberg::ImagePreprocessingMetadata> for ImagePreprocessingMetadata {
    fn from(val: kreuzberg::ImagePreprocessingMetadata) -> Self {
        Self {
            original_dimensions: vec![val.original_dimensions.0 as _, val.original_dimensions.1 as _],
            original_dpi: vec![val.original_dpi.0 as _, val.original_dpi.1 as _],
            target_dpi: val.target_dpi,
            scale_factor: val.scale_factor,
            auto_adjusted: val.auto_adjusted,
            final_dpi: val.final_dpi,
            new_dimensions: val.new_dimensions.map(|t| {
                let arr: Vec<_> = [t.0, t.1].into_iter().map(|v| v as _).collect();
                arr
            }),
            resample_method: val.resample_method,
            dimension_clamped: val.dimension_clamped,
            calculated_dpi: val.calculated_dpi,
            skipped_resize: val.skipped_resize,
            resize_error: val.resize_error,
        }
    }
}

#[allow(clippy::redundant_closure, clippy::useless_conversion)]
impl From<Metadata> for kreuzberg::Metadata {
    fn from(val: Metadata) -> Self {
        Self {
            title: val.title,
            subject: val.subject,
            authors: val.authors.map(|v| v.into_iter().collect()),
            keywords: val.keywords.map(|v| v.into_iter().collect()),
            language: val.language,
            created_at: val.created_at,
            modified_at: val.modified_at,
            created_by: val.created_by,
            modified_by: val.modified_by,
            pages: val.pages.map(Into::into),
            format: Default::default(),
            image_preprocessing: val.image_preprocessing.map(Into::into),
            json_schema: val.json_schema.as_ref().and_then(|s| serde_json::from_str(s).ok()),
            error: val.error.map(Into::into),
            extraction_duration_ms: val.extraction_duration_ms,
            category: val.category,
            tags: val.tags.map(|v| v.into_iter().collect()),
            document_version: val.document_version,
            abstract_text: val.abstract_text,
            output_format: val.output_format,
            ocr_used: val.ocr_used,
            additional: val
                .additional
                .into_iter()
                .map(|(k, v)| (k.into(), serde_json::from_str(&v).unwrap_or_default()))
                .collect(),
        }
    }
}

#[allow(clippy::redundant_closure, clippy::useless_conversion)]
impl From<kreuzberg::Metadata> for Metadata {
    fn from(val: kreuzberg::Metadata) -> Self {
        Self {
            title: val.title,
            subject: val.subject,
            authors: val.authors.map(|v| v.into_iter().collect()),
            keywords: val.keywords.map(|v| v.into_iter().collect()),
            language: val.language,
            created_at: val.created_at,
            modified_at: val.modified_at,
            created_by: val.created_by,
            modified_by: val.modified_by,
            pages: val.pages.map(Into::into),
            format: val.format.map(Into::into),
            image_preprocessing: val.image_preprocessing.map(Into::into),
            json_schema: val.json_schema.as_ref().map(ToString::to_string),
            error: val.error.map(Into::into),
            extraction_duration_ms: val.extraction_duration_ms,
            category: val.category,
            tags: val.tags.map(|v| v.into_iter().collect()),
            document_version: val.document_version,
            abstract_text: val.abstract_text,
            output_format: val.output_format,
            ocr_used: val.ocr_used,
            additional: val
                .additional
                .into_iter()
                .map(|(k, v)| (k.to_string(), v.to_string()))
                .collect(),
        }
    }
}

#[allow(clippy::redundant_closure, clippy::useless_conversion)]
impl From<ExcelMetadata> for kreuzberg::ExcelMetadata {
    fn from(val: ExcelMetadata) -> Self {
        Self {
            sheet_count: val.sheet_count,
            sheet_names: val.sheet_names.map(|v| v.into_iter().collect()),
        }
    }
}

#[allow(clippy::redundant_closure, clippy::useless_conversion)]
impl From<kreuzberg::ExcelMetadata> for ExcelMetadata {
    fn from(val: kreuzberg::ExcelMetadata) -> Self {
        Self {
            sheet_count: val.sheet_count,
            sheet_names: val.sheet_names.map(|v| v.into_iter().collect()),
        }
    }
}

#[allow(clippy::redundant_closure, clippy::useless_conversion)]
impl From<EmailMetadata> for kreuzberg::EmailMetadata {
    fn from(val: EmailMetadata) -> Self {
        Self {
            from_email: val.from_email,
            from_name: val.from_name,
            to_emails: val.to_emails.into_iter().collect(),
            cc_emails: val.cc_emails.into_iter().collect(),
            bcc_emails: val.bcc_emails.into_iter().collect(),
            message_id: val.message_id,
            attachments: val.attachments.into_iter().collect(),
        }
    }
}

#[allow(clippy::redundant_closure, clippy::useless_conversion)]
impl From<kreuzberg::EmailMetadata> for EmailMetadata {
    fn from(val: kreuzberg::EmailMetadata) -> Self {
        Self {
            from_email: val.from_email,
            from_name: val.from_name,
            to_emails: val.to_emails.into_iter().collect(),
            cc_emails: val.cc_emails.into_iter().collect(),
            bcc_emails: val.bcc_emails.into_iter().collect(),
            message_id: val.message_id,
            attachments: val.attachments.into_iter().collect(),
        }
    }
}

#[allow(clippy::redundant_closure, clippy::useless_conversion)]
impl From<ArchiveMetadata> for kreuzberg::ArchiveMetadata {
    fn from(val: ArchiveMetadata) -> Self {
        Self {
            format: val.format.into(),
            file_count: val.file_count,
            file_list: val.file_list.into_iter().collect(),
            total_size: val.total_size,
            compressed_size: val.compressed_size,
        }
    }
}

#[allow(clippy::redundant_closure, clippy::useless_conversion)]
impl From<kreuzberg::ArchiveMetadata> for ArchiveMetadata {
    fn from(val: kreuzberg::ArchiveMetadata) -> Self {
        Self {
            format: val.format.to_string(),
            file_count: val.file_count,
            file_list: val.file_list.into_iter().collect(),
            total_size: val.total_size,
            compressed_size: val.compressed_size,
        }
    }
}

#[allow(clippy::redundant_closure, clippy::useless_conversion)]
impl From<ImageMetadata> for kreuzberg::ImageMetadata {
    fn from(val: ImageMetadata) -> Self {
        Self {
            width: val.width,
            height: val.height,
            format: val.format,
            exif: val.exif.into_iter().map(|(k, v)| (k.into(), v.into())).collect(),
        }
    }
}

#[allow(clippy::redundant_closure, clippy::useless_conversion)]
impl From<kreuzberg::ImageMetadata> for ImageMetadata {
    fn from(val: kreuzberg::ImageMetadata) -> Self {
        Self {
            width: val.width,
            height: val.height,
            format: val.format,
            exif: val.exif.into_iter().map(|(k, v)| (k.into(), v.into())).collect(),
        }
    }
}

#[allow(clippy::redundant_closure, clippy::useless_conversion)]
impl From<XmlMetadata> for kreuzberg::XmlMetadata {
    fn from(val: XmlMetadata) -> Self {
        Self {
            element_count: val.element_count,
            unique_elements: val.unique_elements.into_iter().collect(),
        }
    }
}

#[allow(clippy::redundant_closure, clippy::useless_conversion)]
impl From<kreuzberg::XmlMetadata> for XmlMetadata {
    fn from(val: kreuzberg::XmlMetadata) -> Self {
        Self {
            element_count: val.element_count,
            unique_elements: val.unique_elements.into_iter().collect(),
        }
    }
}

#[allow(clippy::redundant_closure, clippy::useless_conversion)]
impl From<TextMetadata> for kreuzberg::TextMetadata {
    fn from(val: TextMetadata) -> Self {
        Self {
            line_count: val.line_count,
            word_count: val.word_count,
            character_count: val.character_count,
            headers: val.headers.map(|v| v.into_iter().collect()),
            links: Default::default(),
            code_blocks: Default::default(),
        }
    }
}

#[allow(clippy::redundant_closure, clippy::useless_conversion)]
impl From<kreuzberg::TextMetadata> for TextMetadata {
    fn from(val: kreuzberg::TextMetadata) -> Self {
        Self {
            line_count: val.line_count,
            word_count: val.word_count,
            character_count: val.character_count,
            headers: val.headers.map(|v| v.into_iter().collect()),
            links: val.links.as_ref().map(|v| {
                v.iter()
                    .map(|(a, b)| vec![a.to_string(), b.to_string()])
                    .collect::<Vec<Vec<String>>>()
            }),
            code_blocks: val.code_blocks.as_ref().map(|v| {
                v.iter()
                    .map(|(a, b)| vec![a.to_string(), b.to_string()])
                    .collect::<Vec<Vec<String>>>()
            }),
        }
    }
}

#[allow(clippy::redundant_closure, clippy::useless_conversion)]
impl From<HeaderMetadata> for kreuzberg::HeaderMetadata {
    fn from(val: HeaderMetadata) -> Self {
        Self {
            level: val.level,
            text: val.text,
            id: val.id,
            depth: val.depth,
            html_offset: val.html_offset,
        }
    }
}

#[allow(clippy::redundant_closure, clippy::useless_conversion)]
impl From<kreuzberg::HeaderMetadata> for HeaderMetadata {
    fn from(val: kreuzberg::HeaderMetadata) -> Self {
        Self {
            level: val.level,
            text: val.text,
            id: val.id,
            depth: val.depth,
            html_offset: val.html_offset,
        }
    }
}

#[allow(clippy::redundant_closure, clippy::useless_conversion)]
impl From<LinkMetadata> for kreuzberg::LinkMetadata {
    fn from(val: LinkMetadata) -> Self {
        Self {
            href: val.href,
            text: val.text,
            title: val.title,
            link_type: val.link_type.into(),
            rel: val.rel.into_iter().collect(),
            attributes: Default::default(),
        }
    }
}

#[allow(clippy::redundant_closure, clippy::useless_conversion)]
impl From<kreuzberg::LinkMetadata> for LinkMetadata {
    fn from(val: kreuzberg::LinkMetadata) -> Self {
        Self {
            href: val.href,
            text: val.text,
            title: val.title,
            link_type: val.link_type.into(),
            rel: val.rel.into_iter().collect(),
            attributes: val
                .attributes
                .iter()
                .map(|(a, b)| vec![a.to_string(), b.to_string()])
                .collect::<Vec<Vec<String>>>(),
        }
    }
}

#[allow(clippy::redundant_closure, clippy::useless_conversion)]
impl From<ImageMetadataType> for kreuzberg::ImageMetadataType {
    fn from(val: ImageMetadataType) -> Self {
        Self {
            src: val.src,
            alt: val.alt,
            title: val.title,
            dimensions: Default::default(),
            image_type: val.image_type.into(),
            attributes: Default::default(),
        }
    }
}

#[allow(clippy::redundant_closure, clippy::useless_conversion)]
impl From<kreuzberg::ImageMetadataType> for ImageMetadataType {
    fn from(val: kreuzberg::ImageMetadataType) -> Self {
        Self {
            src: val.src,
            alt: val.alt,
            title: val.title,
            dimensions: val.dimensions.map(|t| {
                let arr: Vec<_> = [t.0, t.1].into_iter().map(|v| v as _).collect();
                arr
            }),
            image_type: val.image_type.into(),
            attributes: val
                .attributes
                .iter()
                .map(|(a, b)| vec![a.to_string(), b.to_string()])
                .collect::<Vec<Vec<String>>>(),
        }
    }
}

#[allow(clippy::redundant_closure, clippy::useless_conversion)]
impl From<StructuredData> for kreuzberg::StructuredData {
    fn from(val: StructuredData) -> Self {
        Self {
            data_type: val.data_type.into(),
            raw_json: val.raw_json,
            schema_type: val.schema_type,
        }
    }
}

#[allow(clippy::redundant_closure, clippy::useless_conversion)]
impl From<kreuzberg::StructuredData> for StructuredData {
    fn from(val: kreuzberg::StructuredData) -> Self {
        Self {
            data_type: val.data_type.into(),
            raw_json: val.raw_json,
            schema_type: val.schema_type,
        }
    }
}

#[allow(clippy::redundant_closure, clippy::useless_conversion)]
impl From<HtmlMetadata> for kreuzberg::HtmlMetadata {
    fn from(val: HtmlMetadata) -> Self {
        Self {
            title: val.title,
            description: val.description,
            keywords: val.keywords.into_iter().collect(),
            author: val.author,
            canonical_url: val.canonical_url,
            base_href: val.base_href,
            language: val.language,
            text_direction: val.text_direction.map(Into::into),
            open_graph: val.open_graph.into_iter().map(|(k, v)| (k.into(), v.into())).collect(),
            twitter_card: val
                .twitter_card
                .into_iter()
                .map(|(k, v)| (k.into(), v.into()))
                .collect(),
            meta_tags: val.meta_tags.into_iter().map(|(k, v)| (k.into(), v.into())).collect(),
            headers: val.headers.into_iter().map(Into::into).collect(),
            links: val.links.into_iter().map(Into::into).collect(),
            images: val.images.into_iter().map(Into::into).collect(),
            structured_data: val.structured_data.into_iter().map(Into::into).collect(),
        }
    }
}

#[allow(clippy::redundant_closure, clippy::useless_conversion)]
impl From<kreuzberg::HtmlMetadata> for HtmlMetadata {
    fn from(val: kreuzberg::HtmlMetadata) -> Self {
        Self {
            title: val.title,
            description: val.description,
            keywords: val.keywords.into_iter().collect(),
            author: val.author,
            canonical_url: val.canonical_url,
            base_href: val.base_href,
            language: val.language,
            text_direction: val.text_direction.map(Into::into),
            open_graph: val.open_graph.into_iter().map(|(k, v)| (k.into(), v.into())).collect(),
            twitter_card: val
                .twitter_card
                .into_iter()
                .map(|(k, v)| (k.into(), v.into()))
                .collect(),
            meta_tags: val.meta_tags.into_iter().map(|(k, v)| (k.into(), v.into())).collect(),
            headers: val.headers.into_iter().map(Into::into).collect(),
            links: val.links.into_iter().map(Into::into).collect(),
            images: val.images.into_iter().map(Into::into).collect(),
            structured_data: val.structured_data.into_iter().map(Into::into).collect(),
        }
    }
}

#[allow(clippy::redundant_closure, clippy::useless_conversion)]
impl From<OcrMetadata> for kreuzberg::OcrMetadata {
    fn from(val: OcrMetadata) -> Self {
        Self {
            language: val.language,
            psm: val.psm,
            output_format: val.output_format,
            table_count: val.table_count,
            table_rows: val.table_rows,
            table_cols: val.table_cols,
        }
    }
}

#[allow(clippy::redundant_closure, clippy::useless_conversion)]
impl From<kreuzberg::OcrMetadata> for OcrMetadata {
    fn from(val: kreuzberg::OcrMetadata) -> Self {
        Self {
            language: val.language,
            psm: val.psm,
            output_format: val.output_format,
            table_count: val.table_count,
            table_rows: val.table_rows,
            table_cols: val.table_cols,
        }
    }
}

#[allow(clippy::redundant_closure, clippy::useless_conversion)]
impl From<ErrorMetadata> for kreuzberg::ErrorMetadata {
    fn from(val: ErrorMetadata) -> Self {
        Self {
            error_type: val.error_type,
            message: val.message,
        }
    }
}

#[allow(clippy::redundant_closure, clippy::useless_conversion)]
impl From<kreuzberg::ErrorMetadata> for ErrorMetadata {
    fn from(val: kreuzberg::ErrorMetadata) -> Self {
        Self {
            error_type: val.error_type,
            message: val.message,
        }
    }
}

#[allow(clippy::redundant_closure, clippy::useless_conversion)]
impl From<PptxMetadata> for kreuzberg::PptxMetadata {
    fn from(val: PptxMetadata) -> Self {
        Self {
            slide_count: val.slide_count,
            slide_names: val.slide_names.into_iter().collect(),
            image_count: val.image_count,
            table_count: val.table_count,
        }
    }
}

#[allow(clippy::redundant_closure, clippy::useless_conversion)]
impl From<kreuzberg::PptxMetadata> for PptxMetadata {
    fn from(val: kreuzberg::PptxMetadata) -> Self {
        Self {
            slide_count: val.slide_count,
            slide_names: val.slide_names.into_iter().collect(),
            image_count: val.image_count,
            table_count: val.table_count,
        }
    }
}

#[allow(clippy::redundant_closure, clippy::useless_conversion)]
impl From<DocxMetadata> for kreuzberg::DocxMetadata {
    fn from(val: DocxMetadata) -> Self {
        Self {
            core_properties: val.core_properties.map(Into::into),
            app_properties: val.app_properties.map(Into::into),
            custom_properties: val.custom_properties.map(|m| {
                m.into_iter()
                    .map(|(k, v)| (k.into(), serde_json::from_str(&v).unwrap_or_default()))
                    .collect()
            }),
        }
    }
}

#[allow(clippy::redundant_closure, clippy::useless_conversion)]
impl From<kreuzberg::DocxMetadata> for DocxMetadata {
    fn from(val: kreuzberg::DocxMetadata) -> Self {
        Self {
            core_properties: val.core_properties.map(Into::into),
            app_properties: val.app_properties.map(Into::into),
            custom_properties: val
                .custom_properties
                .map(|m| m.into_iter().map(|(k, v)| (k.to_string(), v.to_string())).collect()),
        }
    }
}

#[allow(clippy::redundant_closure, clippy::useless_conversion)]
impl From<CsvMetadata> for kreuzberg::CsvMetadata {
    fn from(val: CsvMetadata) -> Self {
        Self {
            row_count: val.row_count,
            column_count: val.column_count,
            delimiter: val.delimiter,
            has_header: val.has_header,
            column_types: val.column_types.map(|v| v.into_iter().collect()),
        }
    }
}

#[allow(clippy::redundant_closure, clippy::useless_conversion)]
impl From<kreuzberg::CsvMetadata> for CsvMetadata {
    fn from(val: kreuzberg::CsvMetadata) -> Self {
        Self {
            row_count: val.row_count,
            column_count: val.column_count,
            delimiter: val.delimiter,
            has_header: val.has_header,
            column_types: val.column_types.map(|v| v.into_iter().collect()),
        }
    }
}

#[allow(clippy::redundant_closure, clippy::useless_conversion)]
impl From<BibtexMetadata> for kreuzberg::BibtexMetadata {
    fn from(val: BibtexMetadata) -> Self {
        Self {
            entry_count: val.entry_count,
            citation_keys: val.citation_keys.into_iter().collect(),
            authors: val.authors.into_iter().collect(),
            year_range: val.year_range.map(Into::into),
            entry_types: val.entry_types.map(|m| m.into_iter().collect()),
        }
    }
}

#[allow(clippy::redundant_closure, clippy::useless_conversion)]
impl From<kreuzberg::BibtexMetadata> for BibtexMetadata {
    fn from(val: kreuzberg::BibtexMetadata) -> Self {
        Self {
            entry_count: val.entry_count,
            citation_keys: val.citation_keys.into_iter().collect(),
            authors: val.authors.into_iter().collect(),
            year_range: val.year_range.map(Into::into),
            entry_types: val.entry_types.map(|m| m.into_iter().collect()),
        }
    }
}

#[allow(clippy::redundant_closure, clippy::useless_conversion)]
impl From<CitationMetadata> for kreuzberg::CitationMetadata {
    fn from(val: CitationMetadata) -> Self {
        Self {
            citation_count: val.citation_count,
            format: val.format,
            authors: val.authors.into_iter().collect(),
            year_range: val.year_range.map(Into::into),
            dois: val.dois.into_iter().collect(),
            keywords: val.keywords.into_iter().collect(),
        }
    }
}

#[allow(clippy::redundant_closure, clippy::useless_conversion)]
impl From<kreuzberg::CitationMetadata> for CitationMetadata {
    fn from(val: kreuzberg::CitationMetadata) -> Self {
        Self {
            citation_count: val.citation_count,
            format: val.format,
            authors: val.authors.into_iter().collect(),
            year_range: val.year_range.map(Into::into),
            dois: val.dois.into_iter().collect(),
            keywords: val.keywords.into_iter().collect(),
        }
    }
}

#[allow(clippy::redundant_closure, clippy::useless_conversion)]
impl From<YearRange> for kreuzberg::YearRange {
    fn from(val: YearRange) -> Self {
        Self {
            min: val.min,
            max: val.max,
            years: val.years.into_iter().collect(),
        }
    }
}

#[allow(clippy::redundant_closure, clippy::useless_conversion)]
impl From<kreuzberg::YearRange> for YearRange {
    fn from(val: kreuzberg::YearRange) -> Self {
        Self {
            min: val.min,
            max: val.max,
            years: val.years.into_iter().collect(),
        }
    }
}

#[allow(clippy::redundant_closure, clippy::useless_conversion)]
impl From<FictionBookMetadata> for kreuzberg::FictionBookMetadata {
    fn from(val: FictionBookMetadata) -> Self {
        Self {
            genres: val.genres.into_iter().collect(),
            sequences: val.sequences.into_iter().collect(),
            annotation: val.annotation,
        }
    }
}

#[allow(clippy::redundant_closure, clippy::useless_conversion)]
impl From<kreuzberg::FictionBookMetadata> for FictionBookMetadata {
    fn from(val: kreuzberg::FictionBookMetadata) -> Self {
        Self {
            genres: val.genres.into_iter().collect(),
            sequences: val.sequences.into_iter().collect(),
            annotation: val.annotation,
        }
    }
}

#[allow(clippy::redundant_closure, clippy::useless_conversion)]
impl From<DbfMetadata> for kreuzberg::DbfMetadata {
    fn from(val: DbfMetadata) -> Self {
        Self {
            record_count: val.record_count,
            field_count: val.field_count,
            fields: val.fields.into_iter().map(Into::into).collect(),
        }
    }
}

#[allow(clippy::redundant_closure, clippy::useless_conversion)]
impl From<kreuzberg::DbfMetadata> for DbfMetadata {
    fn from(val: kreuzberg::DbfMetadata) -> Self {
        Self {
            record_count: val.record_count,
            field_count: val.field_count,
            fields: val.fields.into_iter().map(Into::into).collect(),
        }
    }
}

#[allow(clippy::redundant_closure, clippy::useless_conversion)]
impl From<DbfFieldInfo> for kreuzberg::DbfFieldInfo {
    fn from(val: DbfFieldInfo) -> Self {
        Self {
            name: val.name,
            field_type: val.field_type,
        }
    }
}

#[allow(clippy::redundant_closure, clippy::useless_conversion)]
impl From<kreuzberg::DbfFieldInfo> for DbfFieldInfo {
    fn from(val: kreuzberg::DbfFieldInfo) -> Self {
        Self {
            name: val.name,
            field_type: val.field_type,
        }
    }
}

#[allow(clippy::redundant_closure, clippy::useless_conversion)]
impl From<JatsMetadata> for kreuzberg::JatsMetadata {
    fn from(val: JatsMetadata) -> Self {
        Self {
            copyright: val.copyright,
            license: val.license,
            history_dates: val
                .history_dates
                .into_iter()
                .map(|(k, v)| (k.into(), v.into()))
                .collect(),
            contributor_roles: val.contributor_roles.into_iter().map(Into::into).collect(),
        }
    }
}

#[allow(clippy::redundant_closure, clippy::useless_conversion)]
impl From<kreuzberg::JatsMetadata> for JatsMetadata {
    fn from(val: kreuzberg::JatsMetadata) -> Self {
        Self {
            copyright: val.copyright,
            license: val.license,
            history_dates: val
                .history_dates
                .into_iter()
                .map(|(k, v)| (k.into(), v.into()))
                .collect(),
            contributor_roles: val.contributor_roles.into_iter().map(Into::into).collect(),
        }
    }
}

#[allow(clippy::redundant_closure, clippy::useless_conversion)]
impl From<ContributorRole> for kreuzberg::ContributorRole {
    fn from(val: ContributorRole) -> Self {
        Self {
            name: val.name,
            role: val.role,
        }
    }
}

#[allow(clippy::redundant_closure, clippy::useless_conversion)]
impl From<kreuzberg::ContributorRole> for ContributorRole {
    fn from(val: kreuzberg::ContributorRole) -> Self {
        Self {
            name: val.name,
            role: val.role,
        }
    }
}

#[allow(clippy::redundant_closure, clippy::useless_conversion)]
impl From<EpubMetadata> for kreuzberg::EpubMetadata {
    fn from(val: EpubMetadata) -> Self {
        Self {
            coverage: val.coverage,
            dc_format: val.dc_format,
            relation: val.relation,
            source: val.source,
            dc_type: val.dc_type,
            cover_image: val.cover_image,
        }
    }
}

#[allow(clippy::redundant_closure, clippy::useless_conversion)]
impl From<kreuzberg::EpubMetadata> for EpubMetadata {
    fn from(val: kreuzberg::EpubMetadata) -> Self {
        Self {
            coverage: val.coverage,
            dc_format: val.dc_format,
            relation: val.relation,
            source: val.source,
            dc_type: val.dc_type,
            cover_image: val.cover_image,
        }
    }
}

#[allow(clippy::redundant_closure, clippy::useless_conversion)]
impl From<PstMetadata> for kreuzberg::PstMetadata {
    fn from(val: PstMetadata) -> Self {
        Self {
            message_count: val.message_count,
        }
    }
}

#[allow(clippy::redundant_closure, clippy::useless_conversion)]
impl From<kreuzberg::PstMetadata> for PstMetadata {
    fn from(val: kreuzberg::PstMetadata) -> Self {
        Self {
            message_count: val.message_count,
        }
    }
}

#[allow(clippy::redundant_closure, clippy::useless_conversion)]
impl From<OcrConfidence> for kreuzberg::OcrConfidence {
    fn from(val: OcrConfidence) -> Self {
        Self {
            detection: val.detection,
            recognition: val.recognition,
        }
    }
}

#[allow(clippy::redundant_closure, clippy::useless_conversion)]
impl From<kreuzberg::OcrConfidence> for OcrConfidence {
    fn from(val: kreuzberg::OcrConfidence) -> Self {
        Self {
            detection: val.detection,
            recognition: val.recognition,
        }
    }
}

#[allow(clippy::redundant_closure, clippy::useless_conversion)]
impl From<OcrRotation> for kreuzberg::OcrRotation {
    fn from(val: OcrRotation) -> Self {
        Self {
            angle_degrees: val.angle_degrees,
            confidence: val.confidence,
        }
    }
}

#[allow(clippy::redundant_closure, clippy::useless_conversion)]
impl From<kreuzberg::OcrRotation> for OcrRotation {
    fn from(val: kreuzberg::OcrRotation) -> Self {
        Self {
            angle_degrees: val.angle_degrees,
            confidence: val.confidence,
        }
    }
}

#[allow(clippy::redundant_closure, clippy::useless_conversion)]
impl From<OcrElement> for kreuzberg::OcrElement {
    fn from(val: OcrElement) -> Self {
        Self {
            text: val.text,
            geometry: val.geometry.into(),
            confidence: val.confidence.into(),
            level: val.level.into(),
            rotation: val.rotation.map(Into::into),
            page_number: val.page_number,
            parent_id: val.parent_id,
            backend_metadata: val
                .backend_metadata
                .into_iter()
                .map(|(k, v)| (k.into(), serde_json::from_str(&v).unwrap_or_default()))
                .collect(),
        }
    }
}

#[allow(clippy::redundant_closure, clippy::useless_conversion)]
impl From<kreuzberg::OcrElement> for OcrElement {
    fn from(val: kreuzberg::OcrElement) -> Self {
        Self {
            text: val.text,
            geometry: val.geometry.into(),
            confidence: val.confidence.into(),
            level: val.level.into(),
            rotation: val.rotation.map(Into::into),
            page_number: val.page_number,
            parent_id: val.parent_id,
            backend_metadata: val
                .backend_metadata
                .into_iter()
                .map(|(k, v)| (k.to_string(), v.to_string()))
                .collect(),
        }
    }
}

#[allow(clippy::redundant_closure, clippy::useless_conversion)]
impl From<OcrElementConfig> for kreuzberg::OcrElementConfig {
    fn from(val: OcrElementConfig) -> Self {
        Self {
            include_elements: val.include_elements,
            min_level: val.min_level.into(),
            min_confidence: val.min_confidence,
            build_hierarchy: val.build_hierarchy,
        }
    }
}

#[allow(clippy::redundant_closure, clippy::useless_conversion)]
impl From<kreuzberg::OcrElementConfig> for OcrElementConfig {
    fn from(val: kreuzberg::OcrElementConfig) -> Self {
        Self {
            include_elements: val.include_elements,
            min_level: val.min_level.into(),
            min_confidence: val.min_confidence,
            build_hierarchy: val.build_hierarchy,
        }
    }
}

#[allow(clippy::redundant_closure, clippy::useless_conversion)]
impl From<PageStructure> for kreuzberg::PageStructure {
    fn from(val: PageStructure) -> Self {
        Self {
            total_count: val.total_count,
            unit_type: val.unit_type.into(),
            boundaries: val.boundaries.map(|v| v.into_iter().map(Into::into).collect()),
            pages: val.pages.map(|v| v.into_iter().map(Into::into).collect()),
        }
    }
}

#[allow(clippy::redundant_closure, clippy::useless_conversion)]
impl From<kreuzberg::PageStructure> for PageStructure {
    fn from(val: kreuzberg::PageStructure) -> Self {
        Self {
            total_count: val.total_count,
            unit_type: val.unit_type.into(),
            boundaries: val.boundaries.map(|v| v.into_iter().map(Into::into).collect()),
            pages: val.pages.map(|v| v.into_iter().map(Into::into).collect()),
        }
    }
}

#[allow(clippy::redundant_closure, clippy::useless_conversion)]
impl From<PageBoundary> for kreuzberg::PageBoundary {
    fn from(val: PageBoundary) -> Self {
        Self {
            byte_start: val.byte_start,
            byte_end: val.byte_end,
            page_number: val.page_number,
        }
    }
}

#[allow(clippy::redundant_closure, clippy::useless_conversion)]
impl From<kreuzberg::PageBoundary> for PageBoundary {
    fn from(val: kreuzberg::PageBoundary) -> Self {
        Self {
            byte_start: val.byte_start,
            byte_end: val.byte_end,
            page_number: val.page_number,
        }
    }
}

#[allow(clippy::redundant_closure, clippy::useless_conversion)]
impl From<PageInfo> for kreuzberg::PageInfo {
    fn from(val: PageInfo) -> Self {
        Self {
            number: val.number,
            title: val.title,
            dimensions: Default::default(),
            image_count: val.image_count,
            table_count: val.table_count,
            hidden: val.hidden,
            is_blank: val.is_blank,
            has_vector_graphics: val.has_vector_graphics,
        }
    }
}

#[allow(clippy::redundant_closure, clippy::useless_conversion)]
impl From<kreuzberg::PageInfo> for PageInfo {
    fn from(val: kreuzberg::PageInfo) -> Self {
        Self {
            number: val.number,
            title: val.title,
            dimensions: val.dimensions.map(|t| {
                let arr: Vec<_> = [t.0, t.1].into_iter().map(|v| v as _).collect();
                arr
            }),
            image_count: val.image_count,
            table_count: val.table_count,
            hidden: val.hidden,
            is_blank: val.is_blank,
            has_vector_graphics: val.has_vector_graphics,
        }
    }
}

#[allow(clippy::redundant_closure, clippy::useless_conversion)]
impl From<PageContent> for kreuzberg::PageContent {
    fn from(val: PageContent) -> Self {
        Self {
            page_number: val.page_number,
            content: val.content,
            tables: val.tables.into_iter().map(|v| std::sync::Arc::new(v.into())).collect(),
            image_indices: val.image_indices.into_iter().collect(),
            hierarchy: val.hierarchy.map(Into::into),
            is_blank: val.is_blank,
            layout_regions: val.layout_regions.map(|v| v.into_iter().map(Into::into).collect()),
            speaker_notes: val.speaker_notes,
            section_name: val.section_name,
            sheet_name: val.sheet_name,
        }
    }
}

#[allow(clippy::redundant_closure, clippy::useless_conversion)]
impl From<kreuzberg::PageContent> for PageContent {
    fn from(val: kreuzberg::PageContent) -> Self {
        Self {
            page_number: val.page_number,
            content: val.content,
            tables: val.tables.into_iter().map(|v| (*v).clone().into()).collect(),
            image_indices: val.image_indices.into_iter().collect(),
            hierarchy: val.hierarchy.map(Into::into),
            is_blank: val.is_blank,
            layout_regions: val.layout_regions.map(|v| v.into_iter().map(Into::into).collect()),
            speaker_notes: val.speaker_notes,
            section_name: val.section_name,
            sheet_name: val.sheet_name,
        }
    }
}

#[allow(clippy::redundant_closure, clippy::useless_conversion)]
impl From<LayoutRegion> for kreuzberg::LayoutRegion {
    fn from(val: LayoutRegion) -> Self {
        Self {
            class_name: val.class_name,
            confidence: val.confidence,
            bounding_box: val.bounding_box.into(),
            area_fraction: val.area_fraction,
        }
    }
}

#[allow(clippy::redundant_closure, clippy::useless_conversion)]
impl From<kreuzberg::LayoutRegion> for LayoutRegion {
    fn from(val: kreuzberg::LayoutRegion) -> Self {
        Self {
            class_name: val.class_name,
            confidence: val.confidence,
            bounding_box: val.bounding_box.into(),
            area_fraction: val.area_fraction,
        }
    }
}

#[allow(clippy::redundant_closure, clippy::useless_conversion)]
impl From<PageHierarchy> for kreuzberg::PageHierarchy {
    fn from(val: PageHierarchy) -> Self {
        Self {
            block_count: val.block_count,
            blocks: val.blocks.into_iter().map(Into::into).collect(),
        }
    }
}

#[allow(clippy::redundant_closure, clippy::useless_conversion)]
impl From<kreuzberg::PageHierarchy> for PageHierarchy {
    fn from(val: kreuzberg::PageHierarchy) -> Self {
        Self {
            block_count: val.block_count,
            blocks: val.blocks.into_iter().map(Into::into).collect(),
        }
    }
}

#[allow(clippy::redundant_closure, clippy::useless_conversion)]
impl From<HierarchicalBlock> for kreuzberg::HierarchicalBlock {
    fn from(val: HierarchicalBlock) -> Self {
        Self {
            text: val.text,
            font_size: val.font_size,
            level: val.level,
            bbox: Default::default(),
        }
    }
}

#[allow(clippy::redundant_closure, clippy::useless_conversion)]
impl From<kreuzberg::HierarchicalBlock> for HierarchicalBlock {
    fn from(val: kreuzberg::HierarchicalBlock) -> Self {
        Self {
            text: val.text,
            font_size: val.font_size,
            level: val.level,
            bbox: val.bbox.map(|t| {
                let arr: Vec<_> = [t.0, t.1].into_iter().map(|v| v as _).collect();
                arr
            }),
        }
    }
}

#[allow(clippy::redundant_closure, clippy::useless_conversion)]
impl From<CellChange> for kreuzberg::CellChange {
    fn from(val: CellChange) -> Self {
        Self {
            row: val.row,
            col: val.col,
            from: val.from,
            to: val.to,
        }
    }
}

#[allow(clippy::redundant_closure, clippy::useless_conversion)]
impl From<kreuzberg::CellChange> for CellChange {
    fn from(val: kreuzberg::CellChange) -> Self {
        Self {
            row: val.row,
            col: val.col,
            from: val.from,
            to: val.to,
        }
    }
}

#[allow(clippy::redundant_closure, clippy::useless_conversion)]
impl From<DocumentRevision> for kreuzberg::DocumentRevision {
    fn from(val: DocumentRevision) -> Self {
        Self {
            revision_id: val.revision_id,
            author: val.author,
            timestamp: val.timestamp,
            kind: val.kind.into(),
            anchor: val.anchor.map(Into::into),
            delta: val.delta.into(),
        }
    }
}

#[allow(clippy::redundant_closure, clippy::useless_conversion)]
impl From<kreuzberg::DocumentRevision> for DocumentRevision {
    fn from(val: kreuzberg::DocumentRevision) -> Self {
        Self {
            revision_id: val.revision_id,
            author: val.author,
            timestamp: val.timestamp,
            kind: val.kind.into(),
            anchor: val.anchor.map(Into::into),
            delta: val.delta.into(),
        }
    }
}

#[allow(clippy::redundant_closure, clippy::useless_conversion)]
impl From<RevisionDelta> for kreuzberg::RevisionDelta {
    fn from(val: RevisionDelta) -> Self {
        Self {
            content: val.content.into_iter().map(Into::into).collect(),
            table_changes: val.table_changes.into_iter().map(Into::into).collect(),
        }
    }
}

#[allow(clippy::redundant_closure, clippy::useless_conversion)]
impl From<kreuzberg::RevisionDelta> for RevisionDelta {
    fn from(val: kreuzberg::RevisionDelta) -> Self {
        Self {
            content: val.content.into_iter().map(Into::into).collect(),
            table_changes: val.table_changes.into_iter().map(Into::into).collect(),
        }
    }
}

#[allow(clippy::redundant_closure, clippy::useless_conversion)]
impl From<Table> for kreuzberg::Table {
    fn from(val: Table) -> Self {
        Self {
            cells: val.cells.into_iter().collect(),
            markdown: val.markdown,
            page_number: val.page_number,
            bounding_box: val.bounding_box.map(Into::into),
        }
    }
}

#[allow(clippy::redundant_closure, clippy::useless_conversion)]
impl From<kreuzberg::Table> for Table {
    fn from(val: kreuzberg::Table) -> Self {
        Self {
            cells: val.cells.into_iter().collect(),
            markdown: val.markdown,
            page_number: val.page_number,
            bounding_box: val.bounding_box.map(Into::into),
        }
    }
}

#[allow(clippy::redundant_closure, clippy::useless_conversion)]
impl From<ExtractedUri> for kreuzberg::ExtractedUri {
    fn from(val: ExtractedUri) -> Self {
        Self {
            url: val.url,
            label: val.label,
            page: val.page,
            kind: val.kind.into(),
        }
    }
}

#[allow(clippy::redundant_closure, clippy::useless_conversion)]
impl From<kreuzberg::ExtractedUri> for ExtractedUri {
    fn from(val: kreuzberg::ExtractedUri) -> Self {
        Self {
            url: val.url,
            label: val.label,
            page: val.page,
            kind: val.kind.into(),
        }
    }
}

#[allow(clippy::redundant_closure, clippy::useless_conversion)]
impl From<DiffOptions> for kreuzberg::DiffOptions {
    fn from(val: DiffOptions) -> Self {
        Self {
            include_metadata: val.include_metadata,
            include_embedded: val.include_embedded,
            max_content_chars: val.max_content_chars,
        }
    }
}

#[allow(clippy::redundant_closure, clippy::useless_conversion)]
impl From<kreuzberg::DiffOptions> for DiffOptions {
    fn from(val: kreuzberg::DiffOptions) -> Self {
        Self {
            include_metadata: val.include_metadata,
            include_embedded: val.include_embedded,
            max_content_chars: val.max_content_chars,
        }
    }
}

#[allow(clippy::redundant_closure, clippy::useless_conversion)]
impl From<ExtractionDiff> for kreuzberg::ExtractionDiff {
    fn from(val: ExtractionDiff) -> Self {
        Self {
            content_diff: val.content_diff.into_iter().map(Into::into).collect(),
            tables_added: val.tables_added.into_iter().map(Into::into).collect(),
            tables_removed: val.tables_removed.into_iter().map(Into::into).collect(),
            tables_changed: val.tables_changed.into_iter().map(Into::into).collect(),
            metadata_changed: serde_json::from_str(&val.metadata_changed).unwrap_or_default(),
            embedded_changes: val.embedded_changes.into(),
        }
    }
}

#[allow(clippy::redundant_closure, clippy::useless_conversion)]
impl From<kreuzberg::ExtractionDiff> for ExtractionDiff {
    fn from(val: kreuzberg::ExtractionDiff) -> Self {
        Self {
            content_diff: val.content_diff.into_iter().map(Into::into).collect(),
            tables_added: val.tables_added.into_iter().map(Into::into).collect(),
            tables_removed: val.tables_removed.into_iter().map(Into::into).collect(),
            tables_changed: val.tables_changed.into_iter().map(Into::into).collect(),
            metadata_changed: val.metadata_changed.to_string(),
            embedded_changes: val.embedded_changes.into(),
        }
    }
}

#[allow(clippy::redundant_closure, clippy::useless_conversion)]
impl From<DiffHunk> for kreuzberg::DiffHunk {
    fn from(val: DiffHunk) -> Self {
        Self {
            from_line: val.from_line,
            from_count: val.from_count,
            to_line: val.to_line,
            to_count: val.to_count,
            lines: val.lines.into_iter().map(Into::into).collect(),
        }
    }
}

#[allow(clippy::redundant_closure, clippy::useless_conversion)]
impl From<kreuzberg::DiffHunk> for DiffHunk {
    fn from(val: kreuzberg::DiffHunk) -> Self {
        Self {
            from_line: val.from_line,
            from_count: val.from_count,
            to_line: val.to_line,
            to_count: val.to_count,
            lines: val.lines.into_iter().map(Into::into).collect(),
        }
    }
}

#[allow(clippy::redundant_closure, clippy::useless_conversion)]
impl From<TableDiff> for kreuzberg::TableDiff {
    fn from(val: TableDiff) -> Self {
        Self {
            from_index: val.from_index,
            to_index: val.to_index,
            cell_changes: val.cell_changes.into_iter().map(Into::into).collect(),
        }
    }
}

#[allow(clippy::redundant_closure, clippy::useless_conversion)]
impl From<kreuzberg::TableDiff> for TableDiff {
    fn from(val: kreuzberg::TableDiff) -> Self {
        Self {
            from_index: val.from_index,
            to_index: val.to_index,
            cell_changes: val.cell_changes.into_iter().map(Into::into).collect(),
        }
    }
}

#[allow(clippy::redundant_closure, clippy::useless_conversion)]
impl From<EmbeddedChanges> for kreuzberg::EmbeddedChanges {
    fn from(val: EmbeddedChanges) -> Self {
        Self {
            added: val.added.into_iter().map(Into::into).collect(),
            removed: val.removed.into_iter().map(Into::into).collect(),
            changed: val.changed.into_iter().map(Into::into).collect(),
        }
    }
}

#[allow(clippy::redundant_closure, clippy::useless_conversion)]
impl From<kreuzberg::EmbeddedChanges> for EmbeddedChanges {
    fn from(val: kreuzberg::EmbeddedChanges) -> Self {
        Self {
            added: val.added.into_iter().map(Into::into).collect(),
            removed: val.removed.into_iter().map(Into::into).collect(),
            changed: val.changed.into_iter().map(Into::into).collect(),
        }
    }
}

#[allow(clippy::redundant_closure, clippy::useless_conversion)]
impl From<EmbeddedDiff> for kreuzberg::EmbeddedDiff {
    fn from(val: EmbeddedDiff) -> Self {
        Self {
            path: val.path,
            diff: Box::new(val.diff.into()),
        }
    }
}

#[allow(clippy::redundant_closure, clippy::useless_conversion)]
impl From<kreuzberg::EmbeddedDiff> for EmbeddedDiff {
    fn from(val: kreuzberg::EmbeddedDiff) -> Self {
        Self {
            path: val.path,
            diff: (*val.diff).into(),
        }
    }
}

#[allow(clippy::redundant_closure, clippy::useless_conversion)]
impl From<EmbeddingPreset> for kreuzberg::EmbeddingPreset {
    fn from(val: EmbeddingPreset) -> Self {
        Self {
            name: val.name,
            chunk_size: val.chunk_size,
            overlap: val.overlap,
            model_repo: val.model_repo,
            pooling: val.pooling,
            model_file: val.model_file,
            dimensions: val.dimensions,
            description: val.description,
        }
    }
}

#[allow(clippy::redundant_closure, clippy::useless_conversion)]
impl From<kreuzberg::EmbeddingPreset> for EmbeddingPreset {
    fn from(val: kreuzberg::EmbeddingPreset) -> Self {
        Self {
            name: val.name,
            chunk_size: val.chunk_size,
            overlap: val.overlap,
            model_repo: val.model_repo,
            pooling: val.pooling,
            model_file: val.model_file,
            dimensions: val.dimensions,
            description: val.description,
        }
    }
}

#[allow(clippy::redundant_closure, clippy::useless_conversion)]
impl From<YakeParams> for kreuzberg::YakeParams {
    fn from(val: YakeParams) -> Self {
        Self {
            window_size: val.window_size,
        }
    }
}

#[allow(clippy::redundant_closure, clippy::useless_conversion)]
impl From<kreuzberg::YakeParams> for YakeParams {
    fn from(val: kreuzberg::YakeParams) -> Self {
        Self {
            window_size: val.window_size,
        }
    }
}

#[allow(clippy::redundant_closure, clippy::useless_conversion)]
impl From<RakeParams> for kreuzberg::RakeParams {
    fn from(val: RakeParams) -> Self {
        Self {
            min_word_length: val.min_word_length,
            max_words_per_phrase: val.max_words_per_phrase,
        }
    }
}

#[allow(clippy::redundant_closure, clippy::useless_conversion)]
impl From<kreuzberg::RakeParams> for RakeParams {
    fn from(val: kreuzberg::RakeParams) -> Self {
        Self {
            min_word_length: val.min_word_length,
            max_words_per_phrase: val.max_words_per_phrase,
        }
    }
}

#[allow(clippy::needless_update)]
#[allow(clippy::redundant_closure, clippy::useless_conversion)]
impl From<KeywordConfig> for kreuzberg::KeywordConfig {
    fn from(val: KeywordConfig) -> Self {
        Self {
            algorithm: val.algorithm.into(),
            max_keywords: val.max_keywords,
            min_score: val.min_score,
            ngram_range: Default::default(),
            language: val.language,
            yake_params: val.yake_params.map(Into::into),
            rake_params: val.rake_params.map(Into::into),
            ..Default::default()
        }
    }
}

#[allow(clippy::redundant_closure, clippy::useless_conversion)]
impl From<kreuzberg::KeywordConfig> for KeywordConfig {
    fn from(val: kreuzberg::KeywordConfig) -> Self {
        Self {
            algorithm: val.algorithm.into(),
            max_keywords: val.max_keywords,
            min_score: val.min_score,
            ngram_range: vec![val.ngram_range.0 as _, val.ngram_range.1 as _],
            language: val.language,
            yake_params: val.yake_params.map(Into::into),
            rake_params: val.rake_params.map(Into::into),
        }
    }
}

#[allow(clippy::redundant_closure, clippy::useless_conversion)]
impl From<Keyword> for kreuzberg::Keyword {
    fn from(val: Keyword) -> Self {
        Self {
            text: val.text,
            score: val.score,
            algorithm: val.algorithm.into(),
            positions: val.positions.map(|v| v.into_iter().collect()),
        }
    }
}

#[allow(clippy::redundant_closure, clippy::useless_conversion)]
impl From<kreuzberg::Keyword> for Keyword {
    fn from(val: kreuzberg::Keyword) -> Self {
        Self {
            text: val.text,
            score: val.score,
            algorithm: val.algorithm.into(),
            positions: val.positions.map(|v| v.into_iter().collect()),
        }
    }
}

#[allow(clippy::redundant_closure, clippy::useless_conversion)]
impl From<PaddleOcrConfig> for kreuzberg::PaddleOcrConfig {
    fn from(val: PaddleOcrConfig) -> Self {
        Self {
            language: val.language,
            cache_dir: val.cache_dir.map(Into::into),
            use_angle_cls: val.use_angle_cls,
            enable_table_detection: val.enable_table_detection,
            det_db_thresh: val.det_db_thresh,
            det_db_box_thresh: val.det_db_box_thresh,
            det_db_unclip_ratio: val.det_db_unclip_ratio,
            det_limit_side_len: val.det_limit_side_len,
            rec_batch_num: val.rec_batch_num,
            padding: val.padding,
            drop_score: val.drop_score,
            model_tier: val.model_tier,
        }
    }
}

#[allow(clippy::redundant_closure, clippy::useless_conversion)]
impl From<kreuzberg::PaddleOcrConfig> for PaddleOcrConfig {
    fn from(val: kreuzberg::PaddleOcrConfig) -> Self {
        Self {
            language: val.language,
            cache_dir: val.cache_dir.map(|p| p.to_string_lossy().to_string()),
            use_angle_cls: val.use_angle_cls,
            enable_table_detection: val.enable_table_detection,
            det_db_thresh: val.det_db_thresh,
            det_db_box_thresh: val.det_db_box_thresh,
            det_db_unclip_ratio: val.det_db_unclip_ratio,
            det_limit_side_len: val.det_limit_side_len,
            rec_batch_num: val.rec_batch_num,
            padding: val.padding,
            drop_score: val.drop_score,
            model_tier: val.model_tier,
        }
    }
}

#[allow(clippy::redundant_closure, clippy::useless_conversion)]
impl From<PdfMetadata> for kreuzberg::pdf::metadata::PdfMetadata {
    fn from(val: PdfMetadata) -> Self {
        Self {
            pdf_version: val.pdf_version,
            producer: val.producer,
            is_encrypted: val.is_encrypted,
            width: val.width,
            height: val.height,
            page_count: val.page_count,
        }
    }
}

#[allow(clippy::redundant_closure, clippy::useless_conversion)]
impl From<kreuzberg::pdf::metadata::PdfMetadata> for PdfMetadata {
    fn from(val: kreuzberg::pdf::metadata::PdfMetadata) -> Self {
        Self {
            pdf_version: val.pdf_version,
            producer: val.producer,
            is_encrypted: val.is_encrypted,
            width: val.width,
            height: val.height,
            page_count: val.page_count,
        }
    }
}

impl From<ExecutionProviderType> for kreuzberg::ExecutionProviderType {
    fn from(val: ExecutionProviderType) -> Self {
        match val {
            ExecutionProviderType::Auto => Self::Auto,
            ExecutionProviderType::Cpu => Self::Cpu,
            ExecutionProviderType::CoreMl => Self::CoreMl,
            ExecutionProviderType::Cuda => Self::Cuda,
            ExecutionProviderType::TensorRt => Self::TensorRt,
        }
    }
}

impl From<kreuzberg::ExecutionProviderType> for ExecutionProviderType {
    fn from(val: kreuzberg::ExecutionProviderType) -> Self {
        match val {
            kreuzberg::ExecutionProviderType::Auto => Self::Auto,
            kreuzberg::ExecutionProviderType::Cpu => Self::Cpu,
            kreuzberg::ExecutionProviderType::CoreMl => Self::CoreMl,
            kreuzberg::ExecutionProviderType::Cuda => Self::Cuda,
            kreuzberg::ExecutionProviderType::TensorRt => Self::TensorRt,
        }
    }
}

impl From<kreuzberg::OutputFormat> for OutputFormat {
    fn from(val: kreuzberg::OutputFormat) -> Self {
        match val {
            kreuzberg::OutputFormat::Plain => Self {
                format_type: "Plain".to_string(),
                ..Default::default()
            },
            kreuzberg::OutputFormat::Markdown => Self {
                format_type: "Markdown".to_string(),
                ..Default::default()
            },
            kreuzberg::OutputFormat::Djot => Self {
                format_type: "Djot".to_string(),
                ..Default::default()
            },
            kreuzberg::OutputFormat::Html => Self {
                format_type: "Html".to_string(),
                ..Default::default()
            },
            kreuzberg::OutputFormat::Json => Self {
                format_type: "Json".to_string(),
                ..Default::default()
            },
            kreuzberg::OutputFormat::Structured => Self {
                format_type: "Structured".to_string(),
                ..Default::default()
            },
            kreuzberg::OutputFormat::Custom(_0) => Self {
                format_type: "Custom".to_string(),
                custom: Some(_0.into()),
                ..Default::default()
            },
        }
    }
}

impl From<OutputFormat> for kreuzberg::OutputFormat {
    fn from(val: OutputFormat) -> Self {
        match val.format_type.as_str() {
            "Plain" => kreuzberg::OutputFormat::Plain,
            "Markdown" => kreuzberg::OutputFormat::Markdown,
            "Djot" => kreuzberg::OutputFormat::Djot,
            "Html" => kreuzberg::OutputFormat::Html,
            "Json" => kreuzberg::OutputFormat::Json,
            "Structured" => kreuzberg::OutputFormat::Structured,
            "Custom" => kreuzberg::OutputFormat::Custom(val.custom.unwrap_or_default().into()),
            _ => Default::default(),
        }
    }
}

impl From<HtmlTheme> for kreuzberg::HtmlTheme {
    fn from(val: HtmlTheme) -> Self {
        match val {
            HtmlTheme::Default => Self::Default,
            HtmlTheme::GitHub => Self::GitHub,
            HtmlTheme::Dark => Self::Dark,
            HtmlTheme::Light => Self::Light,
            HtmlTheme::Unstyled => Self::Unstyled,
        }
    }
}

impl From<kreuzberg::HtmlTheme> for HtmlTheme {
    fn from(val: kreuzberg::HtmlTheme) -> Self {
        match val {
            kreuzberg::HtmlTheme::Default => Self::Default,
            kreuzberg::HtmlTheme::GitHub => Self::GitHub,
            kreuzberg::HtmlTheme::Dark => Self::Dark,
            kreuzberg::HtmlTheme::Light => Self::Light,
            kreuzberg::HtmlTheme::Unstyled => Self::Unstyled,
        }
    }
}

impl From<TableModel> for kreuzberg::TableModel {
    fn from(val: TableModel) -> Self {
        match val {
            TableModel::Tatr => Self::Tatr,
            TableModel::SlanetWired => Self::SlanetWired,
            TableModel::SlanetWireless => Self::SlanetWireless,
            TableModel::SlanetPlus => Self::SlanetPlus,
            TableModel::SlanetAuto => Self::SlanetAuto,
            TableModel::Disabled => Self::Disabled,
        }
    }
}

impl From<kreuzberg::TableModel> for TableModel {
    fn from(val: kreuzberg::TableModel) -> Self {
        match val {
            kreuzberg::TableModel::Tatr => Self::Tatr,
            kreuzberg::TableModel::SlanetWired => Self::SlanetWired,
            kreuzberg::TableModel::SlanetWireless => Self::SlanetWireless,
            kreuzberg::TableModel::SlanetPlus => Self::SlanetPlus,
            kreuzberg::TableModel::SlanetAuto => Self::SlanetAuto,
            kreuzberg::TableModel::Disabled => Self::Disabled,
        }
    }
}

impl From<ChunkerType> for kreuzberg::ChunkerType {
    fn from(val: ChunkerType) -> Self {
        match val {
            ChunkerType::Text => Self::Text,
            ChunkerType::Markdown => Self::Markdown,
            ChunkerType::Yaml => Self::Yaml,
            ChunkerType::Semantic => Self::Semantic,
        }
    }
}

impl From<kreuzberg::ChunkerType> for ChunkerType {
    fn from(val: kreuzberg::ChunkerType) -> Self {
        match val {
            kreuzberg::ChunkerType::Text => Self::Text,
            kreuzberg::ChunkerType::Markdown => Self::Markdown,
            kreuzberg::ChunkerType::Yaml => Self::Yaml,
            kreuzberg::ChunkerType::Semantic => Self::Semantic,
        }
    }
}

impl From<ChunkSizing> for kreuzberg::ChunkSizing {
    fn from(val: ChunkSizing) -> Self {
        match val {
            ChunkSizing::Characters => Self::Characters,
            ChunkSizing::Tokenizer { model, cache_dir } => Self::Tokenizer {
                model,
                cache_dir: cache_dir.map(Into::into),
            },
        }
    }
}

impl From<kreuzberg::ChunkSizing> for ChunkSizing {
    fn from(val: kreuzberg::ChunkSizing) -> Self {
        match val {
            kreuzberg::ChunkSizing::Characters => Self::Characters,
            kreuzberg::ChunkSizing::Tokenizer { model, cache_dir } => Self::Tokenizer {
                model,
                cache_dir: cache_dir.map(|p| p.to_string_lossy().to_string()),
            },
        }
    }
}

impl From<EmbeddingModelType> for kreuzberg::EmbeddingModelType {
    fn from(val: EmbeddingModelType) -> Self {
        match val {
            EmbeddingModelType::Preset { name } => Self::Preset { name },
            EmbeddingModelType::Custom { model_id, dimensions } => Self::Custom { model_id, dimensions },
            EmbeddingModelType::Llm { llm } => Self::Llm { llm: llm.into() },
            EmbeddingModelType::Plugin { name } => Self::Plugin { name },
        }
    }
}

impl From<kreuzberg::EmbeddingModelType> for EmbeddingModelType {
    fn from(val: kreuzberg::EmbeddingModelType) -> Self {
        match val {
            kreuzberg::EmbeddingModelType::Preset { name } => Self::Preset { name },
            kreuzberg::EmbeddingModelType::Custom { model_id, dimensions } => Self::Custom { model_id, dimensions },
            kreuzberg::EmbeddingModelType::Llm { llm } => Self::Llm { llm: llm.into() },
            kreuzberg::EmbeddingModelType::Plugin { name } => Self::Plugin { name },
        }
    }
}

impl From<CodeContentMode> for kreuzberg::CodeContentMode {
    fn from(val: CodeContentMode) -> Self {
        match val {
            CodeContentMode::Chunks => Self::Chunks,
            CodeContentMode::Raw => Self::Raw,
            CodeContentMode::Structure => Self::Structure,
        }
    }
}

impl From<kreuzberg::CodeContentMode> for CodeContentMode {
    fn from(val: kreuzberg::CodeContentMode) -> Self {
        match val {
            kreuzberg::CodeContentMode::Chunks => Self::Chunks,
            kreuzberg::CodeContentMode::Raw => Self::Raw,
            kreuzberg::CodeContentMode::Structure => Self::Structure,
        }
    }
}

impl From<kreuzberg::extraction::transform::ListType> for ListType {
    fn from(val: kreuzberg::extraction::transform::ListType) -> Self {
        match val {
            kreuzberg::extraction::transform::ListType::Bullet => Self::Bullet,
            kreuzberg::extraction::transform::ListType::Numbered => Self::Numbered,
            kreuzberg::extraction::transform::ListType::Lettered => Self::Lettered,
            kreuzberg::extraction::transform::ListType::Indented => Self::Indented,
        }
    }
}

impl From<kreuzberg::OcrBackendType> for OcrBackendType {
    fn from(val: kreuzberg::OcrBackendType) -> Self {
        match val {
            kreuzberg::OcrBackendType::Tesseract => Self::Tesseract,
            kreuzberg::OcrBackendType::EasyOCR => Self::EasyOCR,
            kreuzberg::OcrBackendType::PaddleOCR => Self::PaddleOCR,
            kreuzberg::OcrBackendType::Custom => Self::Custom,
        }
    }
}

impl From<kreuzberg::ProcessingStage> for ProcessingStage {
    fn from(val: kreuzberg::ProcessingStage) -> Self {
        match val {
            kreuzberg::ProcessingStage::Early => Self::Early,
            kreuzberg::ProcessingStage::Middle => Self::Middle,
            kreuzberg::ProcessingStage::Late => Self::Late,
        }
    }
}

impl From<ReductionLevel> for kreuzberg::ReductionLevel {
    fn from(val: ReductionLevel) -> Self {
        match val {
            ReductionLevel::Off => Self::Off,
            ReductionLevel::Light => Self::Light,
            ReductionLevel::Moderate => Self::Moderate,
            ReductionLevel::Aggressive => Self::Aggressive,
            ReductionLevel::Maximum => Self::Maximum,
        }
    }
}

impl From<kreuzberg::ReductionLevel> for ReductionLevel {
    fn from(val: kreuzberg::ReductionLevel) -> Self {
        match val {
            kreuzberg::ReductionLevel::Off => Self::Off,
            kreuzberg::ReductionLevel::Light => Self::Light,
            kreuzberg::ReductionLevel::Moderate => Self::Moderate,
            kreuzberg::ReductionLevel::Aggressive => Self::Aggressive,
            kreuzberg::ReductionLevel::Maximum => Self::Maximum,
        }
    }
}

impl From<PdfAnnotationType> for kreuzberg::PdfAnnotationType {
    fn from(val: PdfAnnotationType) -> Self {
        match val {
            PdfAnnotationType::Text => Self::Text,
            PdfAnnotationType::Highlight => Self::Highlight,
            PdfAnnotationType::Link => Self::Link,
            PdfAnnotationType::Stamp => Self::Stamp,
            PdfAnnotationType::Underline => Self::Underline,
            PdfAnnotationType::StrikeOut => Self::StrikeOut,
            PdfAnnotationType::Other => Self::Other,
        }
    }
}

impl From<kreuzberg::PdfAnnotationType> for PdfAnnotationType {
    fn from(val: kreuzberg::PdfAnnotationType) -> Self {
        match val {
            kreuzberg::PdfAnnotationType::Text => Self::Text,
            kreuzberg::PdfAnnotationType::Highlight => Self::Highlight,
            kreuzberg::PdfAnnotationType::Link => Self::Link,
            kreuzberg::PdfAnnotationType::Stamp => Self::Stamp,
            kreuzberg::PdfAnnotationType::Underline => Self::Underline,
            kreuzberg::PdfAnnotationType::StrikeOut => Self::StrikeOut,
            kreuzberg::PdfAnnotationType::Other => Self::Other,
        }
    }
}

impl From<BlockType> for kreuzberg::BlockType {
    fn from(val: BlockType) -> Self {
        match val {
            BlockType::Paragraph => Self::Paragraph,
            BlockType::Heading => Self::Heading,
            BlockType::Blockquote => Self::Blockquote,
            BlockType::CodeBlock => Self::CodeBlock,
            BlockType::ListItem => Self::ListItem,
            BlockType::OrderedList => Self::OrderedList,
            BlockType::BulletList => Self::BulletList,
            BlockType::TaskList => Self::TaskList,
            BlockType::DefinitionList => Self::DefinitionList,
            BlockType::DefinitionTerm => Self::DefinitionTerm,
            BlockType::DefinitionDescription => Self::DefinitionDescription,
            BlockType::Div => Self::Div,
            BlockType::Section => Self::Section,
            BlockType::ThematicBreak => Self::ThematicBreak,
            BlockType::RawBlock => Self::RawBlock,
            BlockType::MathDisplay => Self::MathDisplay,
        }
    }
}

impl From<kreuzberg::BlockType> for BlockType {
    fn from(val: kreuzberg::BlockType) -> Self {
        match val {
            kreuzberg::BlockType::Paragraph => Self::Paragraph,
            kreuzberg::BlockType::Heading => Self::Heading,
            kreuzberg::BlockType::Blockquote => Self::Blockquote,
            kreuzberg::BlockType::CodeBlock => Self::CodeBlock,
            kreuzberg::BlockType::ListItem => Self::ListItem,
            kreuzberg::BlockType::OrderedList => Self::OrderedList,
            kreuzberg::BlockType::BulletList => Self::BulletList,
            kreuzberg::BlockType::TaskList => Self::TaskList,
            kreuzberg::BlockType::DefinitionList => Self::DefinitionList,
            kreuzberg::BlockType::DefinitionTerm => Self::DefinitionTerm,
            kreuzberg::BlockType::DefinitionDescription => Self::DefinitionDescription,
            kreuzberg::BlockType::Div => Self::Div,
            kreuzberg::BlockType::Section => Self::Section,
            kreuzberg::BlockType::ThematicBreak => Self::ThematicBreak,
            kreuzberg::BlockType::RawBlock => Self::RawBlock,
            kreuzberg::BlockType::MathDisplay => Self::MathDisplay,
        }
    }
}

impl From<InlineType> for kreuzberg::InlineType {
    fn from(val: InlineType) -> Self {
        match val {
            InlineType::Text => Self::Text,
            InlineType::Strong => Self::Strong,
            InlineType::Emphasis => Self::Emphasis,
            InlineType::Highlight => Self::Highlight,
            InlineType::Subscript => Self::Subscript,
            InlineType::Superscript => Self::Superscript,
            InlineType::Insert => Self::Insert,
            InlineType::Delete => Self::Delete,
            InlineType::Code => Self::Code,
            InlineType::Link => Self::Link,
            InlineType::Image => Self::Image,
            InlineType::Span => Self::Span,
            InlineType::Math => Self::Math,
            InlineType::RawInline => Self::RawInline,
            InlineType::FootnoteRef => Self::FootnoteRef,
            InlineType::Symbol => Self::Symbol,
        }
    }
}

impl From<kreuzberg::InlineType> for InlineType {
    fn from(val: kreuzberg::InlineType) -> Self {
        match val {
            kreuzberg::InlineType::Text => Self::Text,
            kreuzberg::InlineType::Strong => Self::Strong,
            kreuzberg::InlineType::Emphasis => Self::Emphasis,
            kreuzberg::InlineType::Highlight => Self::Highlight,
            kreuzberg::InlineType::Subscript => Self::Subscript,
            kreuzberg::InlineType::Superscript => Self::Superscript,
            kreuzberg::InlineType::Insert => Self::Insert,
            kreuzberg::InlineType::Delete => Self::Delete,
            kreuzberg::InlineType::Code => Self::Code,
            kreuzberg::InlineType::Link => Self::Link,
            kreuzberg::InlineType::Image => Self::Image,
            kreuzberg::InlineType::Span => Self::Span,
            kreuzberg::InlineType::Math => Self::Math,
            kreuzberg::InlineType::RawInline => Self::RawInline,
            kreuzberg::InlineType::FootnoteRef => Self::FootnoteRef,
            kreuzberg::InlineType::Symbol => Self::Symbol,
        }
    }
}

impl From<RelationshipKind> for kreuzberg::RelationshipKind {
    fn from(val: RelationshipKind) -> Self {
        match val {
            RelationshipKind::FootnoteReference => Self::FootnoteReference,
            RelationshipKind::CitationReference => Self::CitationReference,
            RelationshipKind::InternalLink => Self::InternalLink,
            RelationshipKind::Caption => Self::Caption,
            RelationshipKind::Label => Self::Label,
            RelationshipKind::TocEntry => Self::TocEntry,
            RelationshipKind::CrossReference => Self::CrossReference,
        }
    }
}

impl From<kreuzberg::RelationshipKind> for RelationshipKind {
    fn from(val: kreuzberg::RelationshipKind) -> Self {
        match val {
            kreuzberg::RelationshipKind::FootnoteReference => Self::FootnoteReference,
            kreuzberg::RelationshipKind::CitationReference => Self::CitationReference,
            kreuzberg::RelationshipKind::InternalLink => Self::InternalLink,
            kreuzberg::RelationshipKind::Caption => Self::Caption,
            kreuzberg::RelationshipKind::Label => Self::Label,
            kreuzberg::RelationshipKind::TocEntry => Self::TocEntry,
            kreuzberg::RelationshipKind::CrossReference => Self::CrossReference,
        }
    }
}

impl From<ContentLayer> for kreuzberg::ContentLayer {
    fn from(val: ContentLayer) -> Self {
        match val {
            ContentLayer::Body => Self::Body,
            ContentLayer::Header => Self::Header,
            ContentLayer::Footer => Self::Footer,
            ContentLayer::Footnote => Self::Footnote,
        }
    }
}

impl From<kreuzberg::ContentLayer> for ContentLayer {
    fn from(val: kreuzberg::ContentLayer) -> Self {
        match val {
            kreuzberg::ContentLayer::Body => Self::Body,
            kreuzberg::ContentLayer::Header => Self::Header,
            kreuzberg::ContentLayer::Footer => Self::Footer,
            kreuzberg::ContentLayer::Footnote => Self::Footnote,
        }
    }
}

impl From<NodeContent> for kreuzberg::NodeContent {
    fn from(val: NodeContent) -> Self {
        match val {
            NodeContent::Title { text } => Self::Title { text },
            NodeContent::Heading { level, text } => Self::Heading { level, text },
            NodeContent::Paragraph { text } => Self::Paragraph { text },
            NodeContent::List { ordered } => Self::List { ordered },
            NodeContent::ListItem { text } => Self::ListItem { text },
            NodeContent::Table { grid } => Self::Table { grid: grid.into() },
            NodeContent::Image {
                description,
                image_index,
                src,
            } => Self::Image {
                description,
                image_index,
                src,
            },
            NodeContent::Code { text, language } => Self::Code { text, language },
            NodeContent::Quote => Self::Quote,
            NodeContent::Formula { text } => Self::Formula { text },
            NodeContent::Footnote { text } => Self::Footnote { text },
            NodeContent::Group {
                label,
                heading_level,
                heading_text,
            } => Self::Group {
                label,
                heading_level,
                heading_text,
            },
            NodeContent::PageBreak => Self::PageBreak,
            NodeContent::Slide { number, title } => Self::Slide { number, title },
            NodeContent::DefinitionList => Self::DefinitionList,
            NodeContent::DefinitionItem { term, definition } => Self::DefinitionItem { term, definition },
            NodeContent::Citation { key, text } => Self::Citation { key, text },
            NodeContent::Admonition { kind, title } => Self::Admonition { kind, title },
            NodeContent::RawBlock { format, content } => Self::RawBlock { format, content },
            NodeContent::MetadataBlock { entries } => Self::MetadataBlock {
                entries: entries
                    .iter()
                    .filter_map(|inner| {
                        let mut it = inner.iter().cloned();
                        Some((it.next()?, it.next()?))
                    })
                    .collect(),
            },
        }
    }
}

impl From<kreuzberg::NodeContent> for NodeContent {
    fn from(val: kreuzberg::NodeContent) -> Self {
        match val {
            kreuzberg::NodeContent::Title { text } => Self::Title { text },
            kreuzberg::NodeContent::Heading { level, text } => Self::Heading { level, text },
            kreuzberg::NodeContent::Paragraph { text } => Self::Paragraph { text },
            kreuzberg::NodeContent::List { ordered } => Self::List { ordered },
            kreuzberg::NodeContent::ListItem { text } => Self::ListItem { text },
            kreuzberg::NodeContent::Table { grid } => Self::Table { grid: grid.into() },
            kreuzberg::NodeContent::Image {
                description,
                image_index,
                src,
            } => Self::Image {
                description,
                image_index,
                src,
            },
            kreuzberg::NodeContent::Code { text, language } => Self::Code { text, language },
            kreuzberg::NodeContent::Quote => Self::Quote,
            kreuzberg::NodeContent::Formula { text } => Self::Formula { text },
            kreuzberg::NodeContent::Footnote { text } => Self::Footnote { text },
            kreuzberg::NodeContent::Group {
                label,
                heading_level,
                heading_text,
            } => Self::Group {
                label,
                heading_level,
                heading_text,
            },
            kreuzberg::NodeContent::PageBreak => Self::PageBreak,
            kreuzberg::NodeContent::Slide { number, title } => Self::Slide { number, title },
            kreuzberg::NodeContent::DefinitionList => Self::DefinitionList,
            kreuzberg::NodeContent::DefinitionItem { term, definition } => Self::DefinitionItem { term, definition },
            kreuzberg::NodeContent::Citation { key, text } => Self::Citation { key, text },
            kreuzberg::NodeContent::Admonition { kind, title } => Self::Admonition { kind, title },
            kreuzberg::NodeContent::RawBlock { format, content } => Self::RawBlock { format, content },
            kreuzberg::NodeContent::MetadataBlock { entries } => Self::MetadataBlock {
                entries: entries
                    .iter()
                    .map(|(a, b)| vec![a.to_string(), b.to_string()])
                    .collect::<Vec<Vec<String>>>(),
            },
        }
    }
}

impl From<AnnotationKind> for kreuzberg::AnnotationKind {
    fn from(val: AnnotationKind) -> Self {
        match val {
            AnnotationKind::Bold => Self::Bold,
            AnnotationKind::Italic => Self::Italic,
            AnnotationKind::Underline => Self::Underline,
            AnnotationKind::Strikethrough => Self::Strikethrough,
            AnnotationKind::Code => Self::Code,
            AnnotationKind::Subscript => Self::Subscript,
            AnnotationKind::Superscript => Self::Superscript,
            AnnotationKind::Link { url, title } => Self::Link { url, title },
            AnnotationKind::Highlight => Self::Highlight,
            AnnotationKind::Color { value } => Self::Color { value },
            AnnotationKind::FontSize { value } => Self::FontSize { value },
            AnnotationKind::Custom { name, value } => Self::Custom { name, value },
        }
    }
}

impl From<kreuzberg::AnnotationKind> for AnnotationKind {
    fn from(val: kreuzberg::AnnotationKind) -> Self {
        match val {
            kreuzberg::AnnotationKind::Bold => Self::Bold,
            kreuzberg::AnnotationKind::Italic => Self::Italic,
            kreuzberg::AnnotationKind::Underline => Self::Underline,
            kreuzberg::AnnotationKind::Strikethrough => Self::Strikethrough,
            kreuzberg::AnnotationKind::Code => Self::Code,
            kreuzberg::AnnotationKind::Subscript => Self::Subscript,
            kreuzberg::AnnotationKind::Superscript => Self::Superscript,
            kreuzberg::AnnotationKind::Link { url, title } => Self::Link { url, title },
            kreuzberg::AnnotationKind::Highlight => Self::Highlight,
            kreuzberg::AnnotationKind::Color { value } => Self::Color { value },
            kreuzberg::AnnotationKind::FontSize { value } => Self::FontSize { value },
            kreuzberg::AnnotationKind::Custom { name, value } => Self::Custom { name, value },
        }
    }
}

impl From<ExtractionMethod> for kreuzberg::ExtractionMethod {
    fn from(val: ExtractionMethod) -> Self {
        match val {
            ExtractionMethod::Native => Self::Native,
            ExtractionMethod::Ocr => Self::Ocr,
            ExtractionMethod::Mixed => Self::Mixed,
        }
    }
}

impl From<kreuzberg::ExtractionMethod> for ExtractionMethod {
    fn from(val: kreuzberg::ExtractionMethod) -> Self {
        match val {
            kreuzberg::ExtractionMethod::Native => Self::Native,
            kreuzberg::ExtractionMethod::Ocr => Self::Ocr,
            kreuzberg::ExtractionMethod::Mixed => Self::Mixed,
        }
    }
}

impl From<ChunkType> for kreuzberg::ChunkType {
    fn from(val: ChunkType) -> Self {
        match val {
            ChunkType::Heading => Self::Heading,
            ChunkType::PartyList => Self::PartyList,
            ChunkType::Definitions => Self::Definitions,
            ChunkType::OperativeClause => Self::OperativeClause,
            ChunkType::SignatureBlock => Self::SignatureBlock,
            ChunkType::Schedule => Self::Schedule,
            ChunkType::TableLike => Self::TableLike,
            ChunkType::Formula => Self::Formula,
            ChunkType::CodeBlock => Self::CodeBlock,
            ChunkType::Image => Self::Image,
            ChunkType::OrgChart => Self::OrgChart,
            ChunkType::Diagram => Self::Diagram,
            ChunkType::Unknown => Self::Unknown,
        }
    }
}

impl From<kreuzberg::ChunkType> for ChunkType {
    fn from(val: kreuzberg::ChunkType) -> Self {
        match val {
            kreuzberg::ChunkType::Heading => Self::Heading,
            kreuzberg::ChunkType::PartyList => Self::PartyList,
            kreuzberg::ChunkType::Definitions => Self::Definitions,
            kreuzberg::ChunkType::OperativeClause => Self::OperativeClause,
            kreuzberg::ChunkType::SignatureBlock => Self::SignatureBlock,
            kreuzberg::ChunkType::Schedule => Self::Schedule,
            kreuzberg::ChunkType::TableLike => Self::TableLike,
            kreuzberg::ChunkType::Formula => Self::Formula,
            kreuzberg::ChunkType::CodeBlock => Self::CodeBlock,
            kreuzberg::ChunkType::Image => Self::Image,
            kreuzberg::ChunkType::OrgChart => Self::OrgChart,
            kreuzberg::ChunkType::Diagram => Self::Diagram,
            kreuzberg::ChunkType::Unknown => Self::Unknown,
        }
    }
}

impl From<ImageKind> for kreuzberg::ImageKind {
    fn from(val: ImageKind) -> Self {
        match val {
            ImageKind::Photograph => Self::Photograph,
            ImageKind::Diagram => Self::Diagram,
            ImageKind::Chart => Self::Chart,
            ImageKind::Drawing => Self::Drawing,
            ImageKind::TextBlock => Self::TextBlock,
            ImageKind::Decoration => Self::Decoration,
            ImageKind::Logo => Self::Logo,
            ImageKind::Icon => Self::Icon,
            ImageKind::TileFragment => Self::TileFragment,
            ImageKind::Mask => Self::Mask,
            ImageKind::PageRaster => Self::PageRaster,
            ImageKind::Unknown => Self::Unknown,
        }
    }
}

impl From<kreuzberg::ImageKind> for ImageKind {
    fn from(val: kreuzberg::ImageKind) -> Self {
        match val {
            kreuzberg::ImageKind::Photograph => Self::Photograph,
            kreuzberg::ImageKind::Diagram => Self::Diagram,
            kreuzberg::ImageKind::Chart => Self::Chart,
            kreuzberg::ImageKind::Drawing => Self::Drawing,
            kreuzberg::ImageKind::TextBlock => Self::TextBlock,
            kreuzberg::ImageKind::Decoration => Self::Decoration,
            kreuzberg::ImageKind::Logo => Self::Logo,
            kreuzberg::ImageKind::Icon => Self::Icon,
            kreuzberg::ImageKind::TileFragment => Self::TileFragment,
            kreuzberg::ImageKind::Mask => Self::Mask,
            kreuzberg::ImageKind::PageRaster => Self::PageRaster,
            kreuzberg::ImageKind::Unknown => Self::Unknown,
        }
    }
}

impl From<ResultFormat> for kreuzberg::ResultFormat {
    fn from(val: ResultFormat) -> Self {
        match val {
            ResultFormat::Unified => Self::Unified,
            ResultFormat::ElementBased => Self::ElementBased,
        }
    }
}

impl From<kreuzberg::ResultFormat> for ResultFormat {
    fn from(val: kreuzberg::ResultFormat) -> Self {
        match val {
            kreuzberg::ResultFormat::Unified => Self::Unified,
            kreuzberg::ResultFormat::ElementBased => Self::ElementBased,
        }
    }
}

impl From<ElementType> for kreuzberg::ElementType {
    fn from(val: ElementType) -> Self {
        match val {
            ElementType::Title => Self::Title,
            ElementType::NarrativeText => Self::NarrativeText,
            ElementType::Heading => Self::Heading,
            ElementType::ListItem => Self::ListItem,
            ElementType::Table => Self::Table,
            ElementType::Image => Self::Image,
            ElementType::PageBreak => Self::PageBreak,
            ElementType::CodeBlock => Self::CodeBlock,
            ElementType::BlockQuote => Self::BlockQuote,
            ElementType::Footer => Self::Footer,
            ElementType::Header => Self::Header,
        }
    }
}

impl From<kreuzberg::ElementType> for ElementType {
    fn from(val: kreuzberg::ElementType) -> Self {
        match val {
            kreuzberg::ElementType::Title => Self::Title,
            kreuzberg::ElementType::NarrativeText => Self::NarrativeText,
            kreuzberg::ElementType::Heading => Self::Heading,
            kreuzberg::ElementType::ListItem => Self::ListItem,
            kreuzberg::ElementType::Table => Self::Table,
            kreuzberg::ElementType::Image => Self::Image,
            kreuzberg::ElementType::PageBreak => Self::PageBreak,
            kreuzberg::ElementType::CodeBlock => Self::CodeBlock,
            kreuzberg::ElementType::BlockQuote => Self::BlockQuote,
            kreuzberg::ElementType::Footer => Self::Footer,
            kreuzberg::ElementType::Header => Self::Header,
        }
    }
}

impl From<kreuzberg::FormatMetadata> for FormatMetadata {
    fn from(val: kreuzberg::FormatMetadata) -> Self {
        match val {
            kreuzberg::FormatMetadata::Pdf(_0) => Self {
                format_type: "pdf".to_string(),
                pdf: Some(_0.into()),
                ..Default::default()
            },
            kreuzberg::FormatMetadata::Docx(_0) => Self {
                format_type: "docx".to_string(),
                docx: Some((*_0).into()),
                ..Default::default()
            },
            kreuzberg::FormatMetadata::Excel(_0) => Self {
                format_type: "excel".to_string(),
                excel: Some(_0.into()),
                ..Default::default()
            },
            kreuzberg::FormatMetadata::Email(_0) => Self {
                format_type: "email".to_string(),
                email: Some(_0.into()),
                ..Default::default()
            },
            kreuzberg::FormatMetadata::Pptx(_0) => Self {
                format_type: "pptx".to_string(),
                pptx: Some(_0.into()),
                ..Default::default()
            },
            kreuzberg::FormatMetadata::Archive(_0) => Self {
                format_type: "archive".to_string(),
                archive: Some(_0.into()),
                ..Default::default()
            },
            kreuzberg::FormatMetadata::Image(_0) => Self {
                format_type: "image".to_string(),
                image: Some(_0.into()),
                ..Default::default()
            },
            kreuzberg::FormatMetadata::Xml(_0) => Self {
                format_type: "xml".to_string(),
                xml: Some(_0.into()),
                ..Default::default()
            },
            kreuzberg::FormatMetadata::Text(_0) => Self {
                format_type: "text".to_string(),
                text: Some(_0.into()),
                ..Default::default()
            },
            kreuzberg::FormatMetadata::Html(_0) => Self {
                format_type: "html".to_string(),
                html: Some((*_0).into()),
                ..Default::default()
            },
            kreuzberg::FormatMetadata::Ocr(_0) => Self {
                format_type: "ocr".to_string(),
                ocr: Some(_0.into()),
                ..Default::default()
            },
            kreuzberg::FormatMetadata::Csv(_0) => Self {
                format_type: "csv".to_string(),
                csv: Some(_0.into()),
                ..Default::default()
            },
            kreuzberg::FormatMetadata::Bibtex(_0) => Self {
                format_type: "bibtex".to_string(),
                bibtex: Some(_0.into()),
                ..Default::default()
            },
            kreuzberg::FormatMetadata::Citation(_0) => Self {
                format_type: "citation".to_string(),
                citation: Some(_0.into()),
                ..Default::default()
            },
            kreuzberg::FormatMetadata::FictionBook(_0) => Self {
                format_type: "fiction_book".to_string(),
                fiction_book: Some(_0.into()),
                ..Default::default()
            },
            kreuzberg::FormatMetadata::Dbf(_0) => Self {
                format_type: "dbf".to_string(),
                dbf: Some(_0.into()),
                ..Default::default()
            },
            kreuzberg::FormatMetadata::Jats(_0) => Self {
                format_type: "jats".to_string(),
                jats: Some(_0.into()),
                ..Default::default()
            },
            kreuzberg::FormatMetadata::Epub(_0) => Self {
                format_type: "epub".to_string(),
                epub: Some(_0.into()),
                ..Default::default()
            },
            kreuzberg::FormatMetadata::Pst(_0) => Self {
                format_type: "pst".to_string(),
                pst: Some(_0.into()),
                ..Default::default()
            },
            kreuzberg::FormatMetadata::Code(_0) => Self {
                format_type: "code".to_string(),
                code: Some(format!("{:?}", _0)),
                ..Default::default()
            },
        }
    }
}

impl From<FormatMetadata> for kreuzberg::FormatMetadata {
    fn from(val: FormatMetadata) -> Self {
        match val.format_type.as_str() {
            "pdf" => kreuzberg::FormatMetadata::Pdf(val.pdf.unwrap_or_default().into()),
            "docx" => kreuzberg::FormatMetadata::Docx(Box::new(val.docx.unwrap_or_default().into())),
            "excel" => kreuzberg::FormatMetadata::Excel(val.excel.unwrap_or_default().into()),
            "email" => kreuzberg::FormatMetadata::Email(val.email.unwrap_or_default().into()),
            "pptx" => kreuzberg::FormatMetadata::Pptx(val.pptx.unwrap_or_default().into()),
            "archive" => kreuzberg::FormatMetadata::Archive(val.archive.unwrap_or_default().into()),
            "image" => kreuzberg::FormatMetadata::Image(val.image.unwrap_or_default().into()),
            "xml" => kreuzberg::FormatMetadata::Xml(val.xml.unwrap_or_default().into()),
            "text" => kreuzberg::FormatMetadata::Text(val.text.unwrap_or_default().into()),
            "html" => kreuzberg::FormatMetadata::Html(Box::new(val.html.unwrap_or_default().into())),
            "ocr" => kreuzberg::FormatMetadata::Ocr(val.ocr.unwrap_or_default().into()),
            "csv" => kreuzberg::FormatMetadata::Csv(val.csv.unwrap_or_default().into()),
            "bibtex" => kreuzberg::FormatMetadata::Bibtex(val.bibtex.unwrap_or_default().into()),
            "citation" => kreuzberg::FormatMetadata::Citation(val.citation.unwrap_or_default().into()),
            "fiction_book" => kreuzberg::FormatMetadata::FictionBook(val.fiction_book.unwrap_or_default().into()),
            "dbf" => kreuzberg::FormatMetadata::Dbf(val.dbf.unwrap_or_default().into()),
            "jats" => kreuzberg::FormatMetadata::Jats(val.jats.unwrap_or_default().into()),
            "epub" => kreuzberg::FormatMetadata::Epub(val.epub.unwrap_or_default().into()),
            "pst" => kreuzberg::FormatMetadata::Pst(val.pst.unwrap_or_default().into()),
            "code" => kreuzberg::FormatMetadata::Code(Default::default()),
            _ => Default::default(),
        }
    }
}

impl From<TextDirection> for kreuzberg::TextDirection {
    fn from(val: TextDirection) -> Self {
        match val {
            TextDirection::LeftToRight => Self::LeftToRight,
            TextDirection::RightToLeft => Self::RightToLeft,
            TextDirection::Auto => Self::Auto,
        }
    }
}

impl From<kreuzberg::TextDirection> for TextDirection {
    fn from(val: kreuzberg::TextDirection) -> Self {
        match val {
            kreuzberg::TextDirection::LeftToRight => Self::LeftToRight,
            kreuzberg::TextDirection::RightToLeft => Self::RightToLeft,
            kreuzberg::TextDirection::Auto => Self::Auto,
        }
    }
}

impl From<LinkType> for kreuzberg::LinkType {
    fn from(val: LinkType) -> Self {
        match val {
            LinkType::Anchor => Self::Anchor,
            LinkType::Internal => Self::Internal,
            LinkType::External => Self::External,
            LinkType::Email => Self::Email,
            LinkType::Phone => Self::Phone,
            LinkType::Other => Self::Other,
        }
    }
}

impl From<kreuzberg::LinkType> for LinkType {
    fn from(val: kreuzberg::LinkType) -> Self {
        match val {
            kreuzberg::LinkType::Anchor => Self::Anchor,
            kreuzberg::LinkType::Internal => Self::Internal,
            kreuzberg::LinkType::External => Self::External,
            kreuzberg::LinkType::Email => Self::Email,
            kreuzberg::LinkType::Phone => Self::Phone,
            kreuzberg::LinkType::Other => Self::Other,
        }
    }
}

impl From<ImageType> for kreuzberg::ImageType {
    fn from(val: ImageType) -> Self {
        match val {
            ImageType::DataUri => Self::DataUri,
            ImageType::InlineSvg => Self::InlineSvg,
            ImageType::External => Self::External,
            ImageType::Relative => Self::Relative,
        }
    }
}

impl From<kreuzberg::ImageType> for ImageType {
    fn from(val: kreuzberg::ImageType) -> Self {
        match val {
            kreuzberg::ImageType::DataUri => Self::DataUri,
            kreuzberg::ImageType::InlineSvg => Self::InlineSvg,
            kreuzberg::ImageType::External => Self::External,
            kreuzberg::ImageType::Relative => Self::Relative,
        }
    }
}

impl From<StructuredDataType> for kreuzberg::StructuredDataType {
    fn from(val: StructuredDataType) -> Self {
        match val {
            StructuredDataType::JsonLd => Self::JsonLd,
            StructuredDataType::Microdata => Self::Microdata,
            StructuredDataType::RDFa => Self::RDFa,
        }
    }
}

impl From<kreuzberg::StructuredDataType> for StructuredDataType {
    fn from(val: kreuzberg::StructuredDataType) -> Self {
        match val {
            kreuzberg::StructuredDataType::JsonLd => Self::JsonLd,
            kreuzberg::StructuredDataType::Microdata => Self::Microdata,
            kreuzberg::StructuredDataType::RDFa => Self::RDFa,
        }
    }
}

impl From<OcrBoundingGeometry> for kreuzberg::OcrBoundingGeometry {
    fn from(val: OcrBoundingGeometry) -> Self {
        match val {
            OcrBoundingGeometry::Rectangle {
                left,
                top,
                width,
                height,
            } => Self::Rectangle {
                left,
                top,
                width,
                height,
            },
            OcrBoundingGeometry::Quadrilateral { points } => Self::Quadrilateral {
                points: serde_json::from_str(&points).unwrap_or_default(),
            },
        }
    }
}

impl From<kreuzberg::OcrBoundingGeometry> for OcrBoundingGeometry {
    fn from(val: kreuzberg::OcrBoundingGeometry) -> Self {
        match val {
            kreuzberg::OcrBoundingGeometry::Rectangle {
                left,
                top,
                width,
                height,
            } => Self::Rectangle {
                left,
                top,
                width,
                height,
            },
            kreuzberg::OcrBoundingGeometry::Quadrilateral { points } => Self::Quadrilateral {
                points: format!("{:?}", points),
            },
        }
    }
}

impl From<OcrElementLevel> for kreuzberg::OcrElementLevel {
    fn from(val: OcrElementLevel) -> Self {
        match val {
            OcrElementLevel::Word => Self::Word,
            OcrElementLevel::Line => Self::Line,
            OcrElementLevel::Block => Self::Block,
            OcrElementLevel::Page => Self::Page,
        }
    }
}

impl From<kreuzberg::OcrElementLevel> for OcrElementLevel {
    fn from(val: kreuzberg::OcrElementLevel) -> Self {
        match val {
            kreuzberg::OcrElementLevel::Word => Self::Word,
            kreuzberg::OcrElementLevel::Line => Self::Line,
            kreuzberg::OcrElementLevel::Block => Self::Block,
            kreuzberg::OcrElementLevel::Page => Self::Page,
        }
    }
}

impl From<PageUnitType> for kreuzberg::PageUnitType {
    fn from(val: PageUnitType) -> Self {
        match val {
            PageUnitType::Page => Self::Page,
            PageUnitType::Slide => Self::Slide,
            PageUnitType::Sheet => Self::Sheet,
        }
    }
}

impl From<kreuzberg::PageUnitType> for PageUnitType {
    fn from(val: kreuzberg::PageUnitType) -> Self {
        match val {
            kreuzberg::PageUnitType::Page => Self::Page,
            kreuzberg::PageUnitType::Slide => Self::Slide,
            kreuzberg::PageUnitType::Sheet => Self::Sheet,
        }
    }
}

impl From<kreuzberg::DiffLine> for DiffLine {
    fn from(val: kreuzberg::DiffLine) -> Self {
        match val {
            kreuzberg::DiffLine::Context(_0) => Self {
                kind: "context".to_string(),
                context: Some(_0.into()),
                ..Default::default()
            },
            kreuzberg::DiffLine::Added(_0) => Self {
                kind: "added".to_string(),
                added: Some(_0.into()),
                ..Default::default()
            },
            kreuzberg::DiffLine::Removed(_0) => Self {
                kind: "removed".to_string(),
                removed: Some(_0.into()),
                ..Default::default()
            },
        }
    }
}

impl From<DiffLine> for kreuzberg::DiffLine {
    fn from(val: DiffLine) -> Self {
        match val.kind.as_str() {
            "context" => kreuzberg::DiffLine::Context(val.context.unwrap_or_default().into()),
            "added" => kreuzberg::DiffLine::Added(val.added.unwrap_or_default().into()),
            "removed" => kreuzberg::DiffLine::Removed(val.removed.unwrap_or_default().into()),
            _ => Default::default(),
        }
    }
}

impl From<RevisionKind> for kreuzberg::RevisionKind {
    fn from(val: RevisionKind) -> Self {
        match val {
            RevisionKind::Insertion => Self::Insertion,
            RevisionKind::Deletion => Self::Deletion,
            RevisionKind::FormatChange => Self::FormatChange,
            RevisionKind::Comment => Self::Comment,
        }
    }
}

impl From<kreuzberg::RevisionKind> for RevisionKind {
    fn from(val: kreuzberg::RevisionKind) -> Self {
        match val {
            kreuzberg::RevisionKind::Insertion => Self::Insertion,
            kreuzberg::RevisionKind::Deletion => Self::Deletion,
            kreuzberg::RevisionKind::FormatChange => Self::FormatChange,
            kreuzberg::RevisionKind::Comment => Self::Comment,
        }
    }
}

impl From<RevisionAnchor> for kreuzberg::RevisionAnchor {
    fn from(val: RevisionAnchor) -> Self {
        match val {
            RevisionAnchor::Paragraph { index } => Self::Paragraph { index },
            RevisionAnchor::TableCell { row, col, table_index } => Self::TableCell { row, col, table_index },
            RevisionAnchor::Page { index } => Self::Page { index },
            RevisionAnchor::Slide { index } => Self::Slide { index },
            RevisionAnchor::Sheet { index, name } => Self::Sheet { index, name },
        }
    }
}

impl From<kreuzberg::RevisionAnchor> for RevisionAnchor {
    fn from(val: kreuzberg::RevisionAnchor) -> Self {
        match val {
            kreuzberg::RevisionAnchor::Paragraph { index } => Self::Paragraph { index },
            kreuzberg::RevisionAnchor::TableCell { row, col, table_index } => Self::TableCell { row, col, table_index },
            kreuzberg::RevisionAnchor::Page { index } => Self::Page { index },
            kreuzberg::RevisionAnchor::Slide { index } => Self::Slide { index },
            kreuzberg::RevisionAnchor::Sheet { index, name } => Self::Sheet { index, name },
        }
    }
}

impl From<UriKind> for kreuzberg::UriKind {
    fn from(val: UriKind) -> Self {
        match val {
            UriKind::Hyperlink => Self::Hyperlink,
            UriKind::Image => Self::Image,
            UriKind::Anchor => Self::Anchor,
            UriKind::Citation => Self::Citation,
            UriKind::Reference => Self::Reference,
            UriKind::Email => Self::Email,
        }
    }
}

impl From<kreuzberg::UriKind> for UriKind {
    fn from(val: kreuzberg::UriKind) -> Self {
        match val {
            kreuzberg::UriKind::Hyperlink => Self::Hyperlink,
            kreuzberg::UriKind::Image => Self::Image,
            kreuzberg::UriKind::Anchor => Self::Anchor,
            kreuzberg::UriKind::Citation => Self::Citation,
            kreuzberg::UriKind::Reference => Self::Reference,
            kreuzberg::UriKind::Email => Self::Email,
        }
    }
}

impl From<KeywordAlgorithm> for kreuzberg::KeywordAlgorithm {
    fn from(val: KeywordAlgorithm) -> Self {
        match val {
            KeywordAlgorithm::Yake => Self::Yake,
            KeywordAlgorithm::Rake => Self::Rake,
        }
    }
}

impl From<kreuzberg::KeywordAlgorithm> for KeywordAlgorithm {
    fn from(val: kreuzberg::KeywordAlgorithm) -> Self {
        match val {
            kreuzberg::KeywordAlgorithm::Yake => Self::Yake,
            kreuzberg::KeywordAlgorithm::Rake => Self::Rake,
        }
    }
}

impl From<kreuzberg::PSMMode> for PSMMode {
    fn from(val: kreuzberg::PSMMode) -> Self {
        match val {
            kreuzberg::PSMMode::OsdOnly => Self::OsdOnly,
            kreuzberg::PSMMode::AutoOsd => Self::AutoOsd,
            kreuzberg::PSMMode::AutoOnly => Self::AutoOnly,
            kreuzberg::PSMMode::Auto => Self::Auto,
            kreuzberg::PSMMode::SingleColumn => Self::SingleColumn,
            kreuzberg::PSMMode::SingleBlockVertical => Self::SingleBlockVertical,
            kreuzberg::PSMMode::SingleBlock => Self::SingleBlock,
            kreuzberg::PSMMode::SingleLine => Self::SingleLine,
            kreuzberg::PSMMode::SingleWord => Self::SingleWord,
            kreuzberg::PSMMode::CircleWord => Self::CircleWord,
            kreuzberg::PSMMode::SingleChar => Self::SingleChar,
        }
    }
}

impl From<kreuzberg::PaddleLanguage> for PaddleLanguage {
    fn from(val: kreuzberg::PaddleLanguage) -> Self {
        match val {
            kreuzberg::PaddleLanguage::English => Self::English,
            kreuzberg::PaddleLanguage::Chinese => Self::Chinese,
            kreuzberg::PaddleLanguage::Japanese => Self::Japanese,
            kreuzberg::PaddleLanguage::Korean => Self::Korean,
            kreuzberg::PaddleLanguage::German => Self::German,
            kreuzberg::PaddleLanguage::French => Self::French,
            kreuzberg::PaddleLanguage::Latin => Self::Latin,
            kreuzberg::PaddleLanguage::Cyrillic => Self::Cyrillic,
            kreuzberg::PaddleLanguage::TraditionalChinese => Self::TraditionalChinese,
            kreuzberg::PaddleLanguage::Thai => Self::Thai,
            kreuzberg::PaddleLanguage::Greek => Self::Greek,
            kreuzberg::PaddleLanguage::EastSlavic => Self::EastSlavic,
            kreuzberg::PaddleLanguage::Arabic => Self::Arabic,
            kreuzberg::PaddleLanguage::Devanagari => Self::Devanagari,
            kreuzberg::PaddleLanguage::Tamil => Self::Tamil,
            kreuzberg::PaddleLanguage::Telugu => Self::Telugu,
        }
    }
}

impl From<kreuzberg::LayoutClass> for LayoutClass {
    fn from(val: kreuzberg::LayoutClass) -> Self {
        match val {
            kreuzberg::LayoutClass::Caption => Self::Caption,
            kreuzberg::LayoutClass::Footnote => Self::Footnote,
            kreuzberg::LayoutClass::Formula => Self::Formula,
            kreuzberg::LayoutClass::ListItem => Self::ListItem,
            kreuzberg::LayoutClass::PageFooter => Self::PageFooter,
            kreuzberg::LayoutClass::PageHeader => Self::PageHeader,
            kreuzberg::LayoutClass::Picture => Self::Picture,
            kreuzberg::LayoutClass::SectionHeader => Self::SectionHeader,
            kreuzberg::LayoutClass::Table => Self::Table,
            kreuzberg::LayoutClass::Text => Self::Text,
            kreuzberg::LayoutClass::Title => Self::Title,
            kreuzberg::LayoutClass::DocumentIndex => Self::DocumentIndex,
            kreuzberg::LayoutClass::Code => Self::Code,
            kreuzberg::LayoutClass::CheckboxSelected => Self::CheckboxSelected,
            kreuzberg::LayoutClass::CheckboxUnselected => Self::CheckboxUnselected,
            kreuzberg::LayoutClass::Form => Self::Form,
            kreuzberg::LayoutClass::KeyValueRegion => Self::KeyValueRegion,
        }
    }
}

/// Convert a `kreuzberg::error::KreuzbergError` error to a Rustler error string.
#[allow(dead_code)]
fn kreuzberg_error_to_rustler_err(e: kreuzberg::error::KreuzbergError) -> String {
    e.to_string()
}

#[rustler::nif]
pub fn acceleration_config_from_json(json: String) -> Result<AccelerationConfig, String> {
    serde_json::from_str::<kreuzberg::AccelerationConfig>(&json)
        .map(AccelerationConfig::from)
        .map_err(|e| e.to_string())
}

#[rustler::nif]
pub fn content_filter_config_from_json(json: String) -> Result<ContentFilterConfig, String> {
    serde_json::from_str::<kreuzberg::ContentFilterConfig>(&json)
        .map(ContentFilterConfig::from)
        .map_err(|e| e.to_string())
}

#[rustler::nif]
pub fn email_config_from_json(json: String) -> Result<EmailConfig, String> {
    serde_json::from_str::<kreuzberg::EmailConfig>(&json)
        .map(EmailConfig::from)
        .map_err(|e| e.to_string())
}

#[rustler::nif]
pub fn extraction_config_from_json(json: String) -> Result<ExtractionConfig, String> {
    serde_json::from_str::<kreuzberg::ExtractionConfig>(&json)
        .map(ExtractionConfig::from)
        .map_err(|e| e.to_string())
}

#[rustler::nif]
pub fn file_extraction_config_from_json(json: String) -> Result<FileExtractionConfig, String> {
    serde_json::from_str::<kreuzberg::FileExtractionConfig>(&json)
        .map(FileExtractionConfig::from)
        .map_err(|e| e.to_string())
}

#[rustler::nif]
pub fn batch_bytes_item_from_json(json: String) -> Result<BatchBytesItem, String> {
    serde_json::from_str::<kreuzberg::BatchBytesItem>(&json)
        .map(BatchBytesItem::from)
        .map_err(|e| e.to_string())
}

#[rustler::nif]
pub fn batch_file_item_from_json(json: String) -> Result<BatchFileItem, String> {
    serde_json::from_str::<kreuzberg::BatchFileItem>(&json)
        .map(BatchFileItem::from)
        .map_err(|e| e.to_string())
}

#[rustler::nif]
pub fn image_extraction_config_from_json(json: String) -> Result<ImageExtractionConfig, String> {
    serde_json::from_str::<kreuzberg::ImageExtractionConfig>(&json)
        .map(ImageExtractionConfig::from)
        .map_err(|e| e.to_string())
}

#[rustler::nif]
pub fn token_reduction_options_from_json(json: String) -> Result<TokenReductionOptions, String> {
    serde_json::from_str::<kreuzberg::TokenReductionOptions>(&json)
        .map(TokenReductionOptions::from)
        .map_err(|e| e.to_string())
}

#[rustler::nif]
pub fn language_detection_config_from_json(json: String) -> Result<LanguageDetectionConfig, String> {
    serde_json::from_str::<kreuzberg::LanguageDetectionConfig>(&json)
        .map(LanguageDetectionConfig::from)
        .map_err(|e| e.to_string())
}

#[rustler::nif]
pub fn html_output_config_from_json(json: String) -> Result<HtmlOutputConfig, String> {
    serde_json::from_str::<kreuzberg::HtmlOutputConfig>(&json)
        .map(HtmlOutputConfig::from)
        .map_err(|e| e.to_string())
}

#[rustler::nif]
pub fn layout_detection_config_from_json(json: String) -> Result<LayoutDetectionConfig, String> {
    serde_json::from_str::<kreuzberg::LayoutDetectionConfig>(&json)
        .map(LayoutDetectionConfig::from)
        .map_err(|e| e.to_string())
}

#[rustler::nif]
pub fn llm_config_from_json(json: String) -> Result<LlmConfig, String> {
    serde_json::from_str::<kreuzberg::LlmConfig>(&json)
        .map(LlmConfig::from)
        .map_err(|e| e.to_string())
}

#[rustler::nif]
pub fn structured_extraction_config_from_json(json: String) -> Result<StructuredExtractionConfig, String> {
    serde_json::from_str::<kreuzberg::StructuredExtractionConfig>(&json)
        .map(StructuredExtractionConfig::from)
        .map_err(|e| e.to_string())
}

#[rustler::nif]
pub fn ocr_quality_thresholds_from_json(json: String) -> Result<OcrQualityThresholds, String> {
    serde_json::from_str::<kreuzberg::OcrQualityThresholds>(&json)
        .map(OcrQualityThresholds::from)
        .map_err(|e| e.to_string())
}

#[rustler::nif]
pub fn ocr_pipeline_stage_from_json(json: String) -> Result<OcrPipelineStage, String> {
    serde_json::from_str::<kreuzberg::OcrPipelineStage>(&json)
        .map(OcrPipelineStage::from)
        .map_err(|e| e.to_string())
}

#[rustler::nif]
pub fn ocr_pipeline_config_from_json(json: String) -> Result<OcrPipelineConfig, String> {
    serde_json::from_str::<kreuzberg::OcrPipelineConfig>(&json)
        .map(OcrPipelineConfig::from)
        .map_err(|e| e.to_string())
}

#[rustler::nif]
pub fn ocr_config_from_json(json: String) -> Result<OcrConfig, String> {
    serde_json::from_str::<kreuzberg::OcrConfig>(&json)
        .map(OcrConfig::from)
        .map_err(|e| e.to_string())
}

#[rustler::nif]
pub fn page_config_from_json(json: String) -> Result<PageConfig, String> {
    serde_json::from_str::<kreuzberg::PageConfig>(&json)
        .map(PageConfig::from)
        .map_err(|e| e.to_string())
}

#[rustler::nif]
pub fn pdf_config_from_json(json: String) -> Result<PdfConfig, String> {
    serde_json::from_str::<kreuzberg::PdfConfig>(&json)
        .map(PdfConfig::from)
        .map_err(|e| e.to_string())
}

#[rustler::nif]
pub fn hierarchy_config_from_json(json: String) -> Result<HierarchyConfig, String> {
    serde_json::from_str::<kreuzberg::HierarchyConfig>(&json)
        .map(HierarchyConfig::from)
        .map_err(|e| e.to_string())
}

#[rustler::nif]
pub fn post_processor_config_from_json(json: String) -> Result<PostProcessorConfig, String> {
    serde_json::from_str::<kreuzberg::PostProcessorConfig>(&json)
        .map(PostProcessorConfig::from)
        .map_err(|e| e.to_string())
}

#[rustler::nif]
pub fn chunking_config_from_json(json: String) -> Result<ChunkingConfig, String> {
    serde_json::from_str::<kreuzberg::ChunkingConfig>(&json)
        .map(ChunkingConfig::from)
        .map_err(|e| e.to_string())
}

#[rustler::nif]
pub fn embedding_config_from_json(json: String) -> Result<EmbeddingConfig, String> {
    serde_json::from_str::<kreuzberg::EmbeddingConfig>(&json)
        .map(EmbeddingConfig::from)
        .map_err(|e| e.to_string())
}

#[rustler::nif]
pub fn tree_sitter_config_from_json(json: String) -> Result<TreeSitterConfig, String> {
    serde_json::from_str::<kreuzberg::TreeSitterConfig>(&json)
        .map(TreeSitterConfig::from)
        .map_err(|e| e.to_string())
}

#[rustler::nif]
pub fn tree_sitter_process_config_from_json(json: String) -> Result<TreeSitterProcessConfig, String> {
    serde_json::from_str::<kreuzberg::TreeSitterProcessConfig>(&json)
        .map(TreeSitterProcessConfig::from)
        .map_err(|e| e.to_string())
}

#[rustler::nif]
pub fn server_config_from_json(json: String) -> Result<ServerConfig, String> {
    serde_json::from_str::<kreuzberg::ServerConfig>(&json)
        .map(ServerConfig::from)
        .map_err(|e| e.to_string())
}

#[rustler::nif]
pub fn docx_app_properties_from_json(json: String) -> Result<DocxAppProperties, String> {
    serde_json::from_str::<kreuzberg::DocxAppProperties>(&json)
        .map(DocxAppProperties::from)
        .map_err(|e| e.to_string())
}

#[rustler::nif]
pub fn core_properties_from_json(json: String) -> Result<CoreProperties, String> {
    serde_json::from_str::<kreuzberg::CoreProperties>(&json)
        .map(CoreProperties::from)
        .map_err(|e| e.to_string())
}

#[rustler::nif]
pub fn security_limits_from_json(json: String) -> Result<SecurityLimits, String> {
    serde_json::from_str::<kreuzberg::SecurityLimits>(&json)
        .map(SecurityLimits::from)
        .map_err(|e| e.to_string())
}

#[rustler::nif]
pub fn token_reduction_config_from_json(json: String) -> Result<TokenReductionConfig, String> {
    serde_json::from_str::<kreuzberg::TokenReductionConfig>(&json)
        .map(TokenReductionConfig::from)
        .map_err(|e| e.to_string())
}

#[rustler::nif]
pub fn pdf_annotation_from_json(json: String) -> Result<PdfAnnotation, String> {
    serde_json::from_str::<kreuzberg::PdfAnnotation>(&json)
        .map(PdfAnnotation::from)
        .map_err(|e| e.to_string())
}

#[rustler::nif]
pub fn djot_content_from_json(json: String) -> Result<DjotContent, String> {
    serde_json::from_str::<kreuzberg::DjotContent>(&json)
        .map(DjotContent::from)
        .map_err(|e| e.to_string())
}

#[rustler::nif]
pub fn formatted_block_from_json(json: String) -> Result<FormattedBlock, String> {
    serde_json::from_str::<kreuzberg::FormattedBlock>(&json)
        .map(FormattedBlock::from)
        .map_err(|e| e.to_string())
}

#[rustler::nif]
pub fn inline_element_from_json(json: String) -> Result<InlineElement, String> {
    serde_json::from_str::<kreuzberg::InlineElement>(&json)
        .map(InlineElement::from)
        .map_err(|e| e.to_string())
}

#[rustler::nif]
pub fn djot_image_from_json(json: String) -> Result<DjotImage, String> {
    serde_json::from_str::<kreuzberg::DjotImage>(&json)
        .map(DjotImage::from)
        .map_err(|e| e.to_string())
}

#[rustler::nif]
pub fn djot_link_from_json(json: String) -> Result<DjotLink, String> {
    serde_json::from_str::<kreuzberg::DjotLink>(&json)
        .map(DjotLink::from)
        .map_err(|e| e.to_string())
}

#[rustler::nif]
pub fn footnote_from_json(json: String) -> Result<Footnote, String> {
    serde_json::from_str::<kreuzberg::Footnote>(&json)
        .map(Footnote::from)
        .map_err(|e| e.to_string())
}

#[rustler::nif]
pub fn document_structure_from_json(json: String) -> Result<DocumentStructure, String> {
    serde_json::from_str::<kreuzberg::DocumentStructure>(&json)
        .map(DocumentStructure::from)
        .map_err(|e| e.to_string())
}

#[rustler::nif]
pub fn document_relationship_from_json(json: String) -> Result<DocumentRelationship, String> {
    serde_json::from_str::<kreuzberg::DocumentRelationship>(&json)
        .map(DocumentRelationship::from)
        .map_err(|e| e.to_string())
}

#[rustler::nif]
pub fn document_node_from_json(json: String) -> Result<DocumentNode, String> {
    serde_json::from_str::<kreuzberg::DocumentNode>(&json)
        .map(DocumentNode::from)
        .map_err(|e| e.to_string())
}

#[rustler::nif]
pub fn table_grid_from_json(json: String) -> Result<TableGrid, String> {
    serde_json::from_str::<kreuzberg::TableGrid>(&json)
        .map(TableGrid::from)
        .map_err(|e| e.to_string())
}

#[rustler::nif]
pub fn grid_cell_from_json(json: String) -> Result<GridCell, String> {
    serde_json::from_str::<kreuzberg::GridCell>(&json)
        .map(GridCell::from)
        .map_err(|e| e.to_string())
}

#[rustler::nif]
pub fn text_annotation_from_json(json: String) -> Result<TextAnnotation, String> {
    serde_json::from_str::<kreuzberg::TextAnnotation>(&json)
        .map(TextAnnotation::from)
        .map_err(|e| e.to_string())
}

#[rustler::nif]
pub fn extraction_result_from_json(json: String) -> Result<ExtractionResult, String> {
    serde_json::from_str::<kreuzberg::ExtractionResult>(&json)
        .map(ExtractionResult::from)
        .map_err(|e| e.to_string())
}

#[rustler::nif]
pub fn archive_entry_from_json(json: String) -> Result<ArchiveEntry, String> {
    serde_json::from_str::<kreuzberg::ArchiveEntry>(&json)
        .map(ArchiveEntry::from)
        .map_err(|e| e.to_string())
}

#[rustler::nif]
pub fn processing_warning_from_json(json: String) -> Result<ProcessingWarning, String> {
    serde_json::from_str::<kreuzberg::ProcessingWarning>(&json)
        .map(ProcessingWarning::from)
        .map_err(|e| e.to_string())
}

#[rustler::nif]
pub fn llm_usage_from_json(json: String) -> Result<LlmUsage, String> {
    serde_json::from_str::<kreuzberg::LlmUsage>(&json)
        .map(LlmUsage::from)
        .map_err(|e| e.to_string())
}

#[rustler::nif]
pub fn chunk_from_json(json: String) -> Result<Chunk, String> {
    serde_json::from_str::<kreuzberg::Chunk>(&json)
        .map(Chunk::from)
        .map_err(|e| e.to_string())
}

#[rustler::nif]
pub fn heading_context_from_json(json: String) -> Result<HeadingContext, String> {
    serde_json::from_str::<kreuzberg::HeadingContext>(&json)
        .map(HeadingContext::from)
        .map_err(|e| e.to_string())
}

#[rustler::nif]
pub fn heading_level_from_json(json: String) -> Result<HeadingLevel, String> {
    serde_json::from_str::<kreuzberg::HeadingLevel>(&json)
        .map(HeadingLevel::from)
        .map_err(|e| e.to_string())
}

#[rustler::nif]
pub fn chunk_metadata_from_json(json: String) -> Result<ChunkMetadata, String> {
    serde_json::from_str::<kreuzberg::ChunkMetadata>(&json)
        .map(ChunkMetadata::from)
        .map_err(|e| e.to_string())
}

#[rustler::nif]
pub fn extracted_image_from_json(json: String) -> Result<ExtractedImage, String> {
    serde_json::from_str::<kreuzberg::ExtractedImage>(&json)
        .map(ExtractedImage::from)
        .map_err(|e| e.to_string())
}

#[rustler::nif]
pub fn bounding_box_from_json(json: String) -> Result<BoundingBox, String> {
    serde_json::from_str::<kreuzberg::BoundingBox>(&json)
        .map(BoundingBox::from)
        .map_err(|e| e.to_string())
}

#[rustler::nif]
pub fn element_metadata_from_json(json: String) -> Result<ElementMetadata, String> {
    serde_json::from_str::<kreuzberg::ElementMetadata>(&json)
        .map(ElementMetadata::from)
        .map_err(|e| e.to_string())
}

#[rustler::nif]
pub fn element_from_json(json: String) -> Result<Element, String> {
    serde_json::from_str::<kreuzberg::Element>(&json)
        .map(Element::from)
        .map_err(|e| e.to_string())
}

#[rustler::nif]
pub fn ocr_extraction_result_from_json(json: String) -> Result<OcrExtractionResult, String> {
    serde_json::from_str::<kreuzberg::OcrExtractionResult>(&json)
        .map(OcrExtractionResult::from)
        .map_err(|e| e.to_string())
}

#[rustler::nif]
pub fn ocr_table_from_json(json: String) -> Result<OcrTable, String> {
    serde_json::from_str::<kreuzberg::OcrTable>(&json)
        .map(OcrTable::from)
        .map_err(|e| e.to_string())
}

#[rustler::nif]
pub fn ocr_table_bounding_box_from_json(json: String) -> Result<OcrTableBoundingBox, String> {
    serde_json::from_str::<kreuzberg::OcrTableBoundingBox>(&json)
        .map(OcrTableBoundingBox::from)
        .map_err(|e| e.to_string())
}

#[rustler::nif]
pub fn image_preprocessing_config_from_json(json: String) -> Result<ImagePreprocessingConfig, String> {
    serde_json::from_str::<kreuzberg::ImagePreprocessingConfig>(&json)
        .map(ImagePreprocessingConfig::from)
        .map_err(|e| e.to_string())
}

#[rustler::nif]
pub fn tesseract_config_from_json(json: String) -> Result<TesseractConfig, String> {
    serde_json::from_str::<kreuzberg::TesseractConfig>(&json)
        .map(TesseractConfig::from)
        .map_err(|e| e.to_string())
}

#[rustler::nif]
pub fn image_preprocessing_metadata_from_json(json: String) -> Result<ImagePreprocessingMetadata, String> {
    serde_json::from_str::<kreuzberg::ImagePreprocessingMetadata>(&json)
        .map(ImagePreprocessingMetadata::from)
        .map_err(|e| e.to_string())
}

#[rustler::nif]
pub fn metadata_from_json(json: String) -> Result<Metadata, String> {
    serde_json::from_str::<kreuzberg::Metadata>(&json)
        .map(Metadata::from)
        .map_err(|e| e.to_string())
}

#[rustler::nif]
pub fn excel_metadata_from_json(json: String) -> Result<ExcelMetadata, String> {
    serde_json::from_str::<kreuzberg::ExcelMetadata>(&json)
        .map(ExcelMetadata::from)
        .map_err(|e| e.to_string())
}

#[rustler::nif]
pub fn email_metadata_from_json(json: String) -> Result<EmailMetadata, String> {
    serde_json::from_str::<kreuzberg::EmailMetadata>(&json)
        .map(EmailMetadata::from)
        .map_err(|e| e.to_string())
}

#[rustler::nif]
pub fn archive_metadata_from_json(json: String) -> Result<ArchiveMetadata, String> {
    serde_json::from_str::<kreuzberg::ArchiveMetadata>(&json)
        .map(ArchiveMetadata::from)
        .map_err(|e| e.to_string())
}

#[rustler::nif]
pub fn image_metadata_from_json(json: String) -> Result<ImageMetadata, String> {
    serde_json::from_str::<kreuzberg::ImageMetadata>(&json)
        .map(ImageMetadata::from)
        .map_err(|e| e.to_string())
}

#[rustler::nif]
pub fn xml_metadata_from_json(json: String) -> Result<XmlMetadata, String> {
    serde_json::from_str::<kreuzberg::XmlMetadata>(&json)
        .map(XmlMetadata::from)
        .map_err(|e| e.to_string())
}

#[rustler::nif]
pub fn text_metadata_from_json(json: String) -> Result<TextMetadata, String> {
    serde_json::from_str::<kreuzberg::TextMetadata>(&json)
        .map(TextMetadata::from)
        .map_err(|e| e.to_string())
}

#[rustler::nif]
pub fn header_metadata_from_json(json: String) -> Result<HeaderMetadata, String> {
    serde_json::from_str::<kreuzberg::HeaderMetadata>(&json)
        .map(HeaderMetadata::from)
        .map_err(|e| e.to_string())
}

#[rustler::nif]
pub fn link_metadata_from_json(json: String) -> Result<LinkMetadata, String> {
    serde_json::from_str::<kreuzberg::LinkMetadata>(&json)
        .map(LinkMetadata::from)
        .map_err(|e| e.to_string())
}

#[rustler::nif]
pub fn image_metadata_type_from_json(json: String) -> Result<ImageMetadataType, String> {
    serde_json::from_str::<kreuzberg::ImageMetadataType>(&json)
        .map(ImageMetadataType::from)
        .map_err(|e| e.to_string())
}

#[rustler::nif]
pub fn structured_data_from_json(json: String) -> Result<StructuredData, String> {
    serde_json::from_str::<kreuzberg::StructuredData>(&json)
        .map(StructuredData::from)
        .map_err(|e| e.to_string())
}

#[rustler::nif]
pub fn html_metadata_from_json(json: String) -> Result<HtmlMetadata, String> {
    serde_json::from_str::<kreuzberg::HtmlMetadata>(&json)
        .map(HtmlMetadata::from)
        .map_err(|e| e.to_string())
}

#[rustler::nif]
pub fn ocr_metadata_from_json(json: String) -> Result<OcrMetadata, String> {
    serde_json::from_str::<kreuzberg::OcrMetadata>(&json)
        .map(OcrMetadata::from)
        .map_err(|e| e.to_string())
}

#[rustler::nif]
pub fn error_metadata_from_json(json: String) -> Result<ErrorMetadata, String> {
    serde_json::from_str::<kreuzberg::ErrorMetadata>(&json)
        .map(ErrorMetadata::from)
        .map_err(|e| e.to_string())
}

#[rustler::nif]
pub fn pptx_metadata_from_json(json: String) -> Result<PptxMetadata, String> {
    serde_json::from_str::<kreuzberg::PptxMetadata>(&json)
        .map(PptxMetadata::from)
        .map_err(|e| e.to_string())
}

#[rustler::nif]
pub fn docx_metadata_from_json(json: String) -> Result<DocxMetadata, String> {
    serde_json::from_str::<kreuzberg::DocxMetadata>(&json)
        .map(DocxMetadata::from)
        .map_err(|e| e.to_string())
}

#[rustler::nif]
pub fn csv_metadata_from_json(json: String) -> Result<CsvMetadata, String> {
    serde_json::from_str::<kreuzberg::CsvMetadata>(&json)
        .map(CsvMetadata::from)
        .map_err(|e| e.to_string())
}

#[rustler::nif]
pub fn bibtex_metadata_from_json(json: String) -> Result<BibtexMetadata, String> {
    serde_json::from_str::<kreuzberg::BibtexMetadata>(&json)
        .map(BibtexMetadata::from)
        .map_err(|e| e.to_string())
}

#[rustler::nif]
pub fn citation_metadata_from_json(json: String) -> Result<CitationMetadata, String> {
    serde_json::from_str::<kreuzberg::CitationMetadata>(&json)
        .map(CitationMetadata::from)
        .map_err(|e| e.to_string())
}

#[rustler::nif]
pub fn year_range_from_json(json: String) -> Result<YearRange, String> {
    serde_json::from_str::<kreuzberg::YearRange>(&json)
        .map(YearRange::from)
        .map_err(|e| e.to_string())
}

#[rustler::nif]
pub fn fiction_book_metadata_from_json(json: String) -> Result<FictionBookMetadata, String> {
    serde_json::from_str::<kreuzberg::FictionBookMetadata>(&json)
        .map(FictionBookMetadata::from)
        .map_err(|e| e.to_string())
}

#[rustler::nif]
pub fn dbf_metadata_from_json(json: String) -> Result<DbfMetadata, String> {
    serde_json::from_str::<kreuzberg::DbfMetadata>(&json)
        .map(DbfMetadata::from)
        .map_err(|e| e.to_string())
}

#[rustler::nif]
pub fn dbf_field_info_from_json(json: String) -> Result<DbfFieldInfo, String> {
    serde_json::from_str::<kreuzberg::DbfFieldInfo>(&json)
        .map(DbfFieldInfo::from)
        .map_err(|e| e.to_string())
}

#[rustler::nif]
pub fn jats_metadata_from_json(json: String) -> Result<JatsMetadata, String> {
    serde_json::from_str::<kreuzberg::JatsMetadata>(&json)
        .map(JatsMetadata::from)
        .map_err(|e| e.to_string())
}

#[rustler::nif]
pub fn contributor_role_from_json(json: String) -> Result<ContributorRole, String> {
    serde_json::from_str::<kreuzberg::ContributorRole>(&json)
        .map(ContributorRole::from)
        .map_err(|e| e.to_string())
}

#[rustler::nif]
pub fn epub_metadata_from_json(json: String) -> Result<EpubMetadata, String> {
    serde_json::from_str::<kreuzberg::EpubMetadata>(&json)
        .map(EpubMetadata::from)
        .map_err(|e| e.to_string())
}

#[rustler::nif]
pub fn pst_metadata_from_json(json: String) -> Result<PstMetadata, String> {
    serde_json::from_str::<kreuzberg::PstMetadata>(&json)
        .map(PstMetadata::from)
        .map_err(|e| e.to_string())
}

#[rustler::nif]
pub fn ocr_confidence_from_json(json: String) -> Result<OcrConfidence, String> {
    serde_json::from_str::<kreuzberg::OcrConfidence>(&json)
        .map(OcrConfidence::from)
        .map_err(|e| e.to_string())
}

#[rustler::nif]
pub fn ocr_rotation_from_json(json: String) -> Result<OcrRotation, String> {
    serde_json::from_str::<kreuzberg::OcrRotation>(&json)
        .map(OcrRotation::from)
        .map_err(|e| e.to_string())
}

#[rustler::nif]
pub fn ocr_element_from_json(json: String) -> Result<OcrElement, String> {
    serde_json::from_str::<kreuzberg::OcrElement>(&json)
        .map(OcrElement::from)
        .map_err(|e| e.to_string())
}

#[rustler::nif]
pub fn ocr_element_config_from_json(json: String) -> Result<OcrElementConfig, String> {
    serde_json::from_str::<kreuzberg::OcrElementConfig>(&json)
        .map(OcrElementConfig::from)
        .map_err(|e| e.to_string())
}

#[rustler::nif]
pub fn page_structure_from_json(json: String) -> Result<PageStructure, String> {
    serde_json::from_str::<kreuzberg::PageStructure>(&json)
        .map(PageStructure::from)
        .map_err(|e| e.to_string())
}

#[rustler::nif]
pub fn page_boundary_from_json(json: String) -> Result<PageBoundary, String> {
    serde_json::from_str::<kreuzberg::PageBoundary>(&json)
        .map(PageBoundary::from)
        .map_err(|e| e.to_string())
}

#[rustler::nif]
pub fn page_info_from_json(json: String) -> Result<PageInfo, String> {
    serde_json::from_str::<kreuzberg::PageInfo>(&json)
        .map(PageInfo::from)
        .map_err(|e| e.to_string())
}

#[rustler::nif]
pub fn page_content_from_json(json: String) -> Result<PageContent, String> {
    serde_json::from_str::<kreuzberg::PageContent>(&json)
        .map(PageContent::from)
        .map_err(|e| e.to_string())
}

#[rustler::nif]
pub fn layout_region_from_json(json: String) -> Result<LayoutRegion, String> {
    serde_json::from_str::<kreuzberg::LayoutRegion>(&json)
        .map(LayoutRegion::from)
        .map_err(|e| e.to_string())
}

#[rustler::nif]
pub fn page_hierarchy_from_json(json: String) -> Result<PageHierarchy, String> {
    serde_json::from_str::<kreuzberg::PageHierarchy>(&json)
        .map(PageHierarchy::from)
        .map_err(|e| e.to_string())
}

#[rustler::nif]
pub fn hierarchical_block_from_json(json: String) -> Result<HierarchicalBlock, String> {
    serde_json::from_str::<kreuzberg::HierarchicalBlock>(&json)
        .map(HierarchicalBlock::from)
        .map_err(|e| e.to_string())
}

#[rustler::nif]
pub fn cell_change_from_json(json: String) -> Result<CellChange, String> {
    serde_json::from_str::<kreuzberg::CellChange>(&json)
        .map(CellChange::from)
        .map_err(|e| e.to_string())
}

#[rustler::nif]
pub fn document_revision_from_json(json: String) -> Result<DocumentRevision, String> {
    serde_json::from_str::<kreuzberg::DocumentRevision>(&json)
        .map(DocumentRevision::from)
        .map_err(|e| e.to_string())
}

#[rustler::nif]
pub fn revision_delta_from_json(json: String) -> Result<RevisionDelta, String> {
    serde_json::from_str::<kreuzberg::RevisionDelta>(&json)
        .map(RevisionDelta::from)
        .map_err(|e| e.to_string())
}

#[rustler::nif]
pub fn table_from_json(json: String) -> Result<Table, String> {
    serde_json::from_str::<kreuzberg::Table>(&json)
        .map(Table::from)
        .map_err(|e| e.to_string())
}

#[rustler::nif]
pub fn extracted_uri_from_json(json: String) -> Result<ExtractedUri, String> {
    serde_json::from_str::<kreuzberg::ExtractedUri>(&json)
        .map(ExtractedUri::from)
        .map_err(|e| e.to_string())
}

#[rustler::nif]
pub fn diff_options_from_json(json: String) -> Result<DiffOptions, String> {
    serde_json::from_str::<kreuzberg::DiffOptions>(&json)
        .map(DiffOptions::from)
        .map_err(|e| e.to_string())
}

#[rustler::nif]
pub fn extraction_diff_from_json(json: String) -> Result<ExtractionDiff, String> {
    serde_json::from_str::<kreuzberg::ExtractionDiff>(&json)
        .map(ExtractionDiff::from)
        .map_err(|e| e.to_string())
}

#[rustler::nif]
pub fn diff_hunk_from_json(json: String) -> Result<DiffHunk, String> {
    serde_json::from_str::<kreuzberg::DiffHunk>(&json)
        .map(DiffHunk::from)
        .map_err(|e| e.to_string())
}

#[rustler::nif]
pub fn table_diff_from_json(json: String) -> Result<TableDiff, String> {
    serde_json::from_str::<kreuzberg::TableDiff>(&json)
        .map(TableDiff::from)
        .map_err(|e| e.to_string())
}

#[rustler::nif]
pub fn embedded_changes_from_json(json: String) -> Result<EmbeddedChanges, String> {
    serde_json::from_str::<kreuzberg::EmbeddedChanges>(&json)
        .map(EmbeddedChanges::from)
        .map_err(|e| e.to_string())
}

#[rustler::nif]
pub fn embedded_diff_from_json(json: String) -> Result<EmbeddedDiff, String> {
    serde_json::from_str::<kreuzberg::EmbeddedDiff>(&json)
        .map(EmbeddedDiff::from)
        .map_err(|e| e.to_string())
}

#[rustler::nif]
pub fn embedding_preset_from_json(json: String) -> Result<EmbeddingPreset, String> {
    serde_json::from_str::<kreuzberg::EmbeddingPreset>(&json)
        .map(EmbeddingPreset::from)
        .map_err(|e| e.to_string())
}

#[rustler::nif]
pub fn yake_params_from_json(json: String) -> Result<YakeParams, String> {
    serde_json::from_str::<kreuzberg::YakeParams>(&json)
        .map(YakeParams::from)
        .map_err(|e| e.to_string())
}

#[rustler::nif]
pub fn rake_params_from_json(json: String) -> Result<RakeParams, String> {
    serde_json::from_str::<kreuzberg::RakeParams>(&json)
        .map(RakeParams::from)
        .map_err(|e| e.to_string())
}

#[rustler::nif]
pub fn keyword_config_from_json(json: String) -> Result<KeywordConfig, String> {
    serde_json::from_str::<kreuzberg::KeywordConfig>(&json)
        .map(KeywordConfig::from)
        .map_err(|e| e.to_string())
}

#[rustler::nif]
pub fn keyword_from_json(json: String) -> Result<Keyword, String> {
    serde_json::from_str::<kreuzberg::Keyword>(&json)
        .map(Keyword::from)
        .map_err(|e| e.to_string())
}

#[rustler::nif]
pub fn paddle_ocr_config_from_json(json: String) -> Result<PaddleOcrConfig, String> {
    serde_json::from_str::<kreuzberg::PaddleOcrConfig>(&json)
        .map(PaddleOcrConfig::from)
        .map_err(|e| e.to_string())
}

#[rustler::nif]
pub fn pdf_metadata_from_json(json: String) -> Result<PdfMetadata, String> {
    serde_json::from_str::<kreuzberg::pdf::metadata::PdfMetadata>(&json)
        .map(PdfMetadata::from)
        .map_err(|e| e.to_string())
}

rustler::init!("Elixir.Kreuzberg.Native");
