Coverage for src/qdrant_loader/core/conversion/config.py: 97%

63 statements  

« prev     ^ index     » next       coverage.py v7.15.0, created at 2026-07-20 10:15 +0000

1"""Engine-agnostic conversion configuration. 

2 

3Frozen dataclasses describing *our* conversion knobs — never docling types. The 

4option builder in :mod:`.options` is the single place that translates this into 

5docling objects, which keeps the config swappable and the engine behind a seam. 

6 

7The defaults below ARE the ``fast`` profile (CPU-only, born-digital corpus, 

8conversion opt-in per source); each field documents its own rationale inline. 

9""" 

10 

11from __future__ import annotations 

12 

13import dataclasses 

14from dataclasses import dataclass, field 

15from enum import StrEnum 

16from typing import Any 

17 

18 

19@dataclass(frozen=True, slots=True) 

20class OcrConfig: 

21 """OCR settings. Off in the baseline — the corpus is born-digital.""" 

22 

23 enabled: bool = False 

24 backend: str = ( 

25 "torch" # the only rapidocr backend in the slim image (no onnxruntime) 

26 ) 

27 languages: tuple[str, ...] = ("english",) # rapidocr supports english | chinese 

28 full_page: bool = False 

29 

30 

31@dataclass(frozen=True, slots=True) 

32class TableConfig: 

33 """Table-structure recognition settings.""" 

34 

35 enabled: bool = True 

36 mode: str = "fast" # FAST on CPU; the "accurate" profile flips this 

37 cell_matching: bool = True 

38 

39 

40@dataclass(frozen=True, slots=True) 

41class PictureDescriptionConfig: 

42 """API image-captioning. Off by default but fully wired (matches today).""" 

43 

44 enabled: bool = False 

45 url: str = "https://api.openai.com/v1/chat/completions" 

46 model: str = "gpt-4o-mini" 

47 api_key: str | None = None 

48 prompt: str = "Describe this image in a few sentences." 

49 timeout: float = 60.0 

50 concurrency: int = 1 

51 scale: float = 2.0 # docling's own picture-description default scale 

52 

53 

54@dataclass(frozen=True, slots=True) 

55class ExcelConfig: 

56 """Excel backend tuning. docling's defaults already handle merged cells.""" 

57 

58 gap_tolerance: int = 0 

59 singleton_as_text: bool = False 

60 sheet_names: tuple[str, ...] | None = None 

61 

62 

63class ConversionProfile(StrEnum): 

64 """Named bundles of the three cost axes (OCR / table fidelity / captioning). 

65 

66 Operators choose one profile instead of reasoning about a dozen interacting 

67 knobs. The string values are what a connector config carries in YAML, so the 

68 enum doubles as the type-safe parse target for that string. 

69 """ 

70 

71 FAST = "fast" 

72 ACCURATE = "accurate" 

73 SCANNED = "scanned" 

74 

75 

76@dataclass(frozen=True, slots=True) 

77class ConversionConfig: 

78 """The complete, immutable conversion configuration for one engine instance.""" 

79 

80 # ── policy / ops ── 

81 max_file_size: int = 50 * 1024 * 1024 

82 document_timeout: float | None = 300.0 # wall-clock; docling default is None 

83 enabled_formats: tuple[str, ...] = ("pdf", "docx", "pptx", "xlsx", "image", "csv") 

84 

85 # ── hardware ── 

86 device: str = "auto" 

87 num_threads: int = 4 

88 compile_models: bool = False # disable torch.compile warm-up cost on CPU 

89 artifacts_path: str | None = None # offline model dir; None = fetch-on-first-run 

90 

91 # ── pdf / image quality ── 

92 ocr: OcrConfig = field(default_factory=OcrConfig) 

93 table: TableConfig = field(default_factory=TableConfig) 

94 code_formula_enrichment: bool = False 

95 

96 # ── enrichment / office ── 

97 picture: PictureDescriptionConfig = field(default_factory=PictureDescriptionConfig) 

98 excel: ExcelConfig = field(default_factory=ExcelConfig) 

99 

100 @classmethod 

101 def from_profile( 

102 cls, profile: ConversionProfile, **overrides: Any 

103 ) -> ConversionConfig: 

104 """Build the baseline config for ``profile``, then apply top-level overrides. 

105 

106 Overrides replace top-level fields only; nested sub-configs (``ocr=...``, 

107 ``table=...``) are passed as whole frozen objects, not field-by-field. 

108 """ 

109 baseline = cls._baseline_for(profile) 

110 return dataclasses.replace(baseline, **overrides) if overrides else baseline 

111 

112 @classmethod 

113 def _baseline_for(cls, profile: ConversionProfile) -> ConversionConfig: 

114 """The unmodified config for each profile. Replaces the old _PROFILES dict.""" 

115 match profile: 

116 case ConversionProfile.FAST: 

117 return cls() # the schema defaults are the fast profile 

118 case ConversionProfile.ACCURATE: 

119 return cls(table=TableConfig(mode="accurate")) 

120 case ConversionProfile.SCANNED: 

121 return cls( 

122 ocr=OcrConfig(enabled=True, full_page=True), 

123 table=TableConfig(mode="accurate"), 

124 document_timeout=600.0, 

125 ) 

126 raise ValueError(f"unknown conversion profile: {profile!r}")