pdfaccessguru 1.0.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (294) hide show
  1. pdfaccessguru/LLM/__init__.py +0 -0
  2. pdfaccessguru/LLM/client.py +219 -0
  3. pdfaccessguru/LLM/config.py +27 -0
  4. pdfaccessguru/LLM/fallback.py +109 -0
  5. pdfaccessguru/LLM/material/__init__.py +11 -0
  6. pdfaccessguru/LLM/material/availableLocales.json +773 -0
  7. pdfaccessguru/LLM/output_checks.py +41 -0
  8. pdfaccessguru/LLM/prompts/semantic_detection.txt +0 -0
  9. pdfaccessguru/LLM/providers/openrouter_provider.py +14 -0
  10. pdfaccessguru/__init__.py +12 -0
  11. pdfaccessguru/app.py +20 -0
  12. pdfaccessguru/cli.py +387 -0
  13. pdfaccessguru/exceptions/__init__.py +3 -0
  14. pdfaccessguru/exceptions/corrector_exceptions/__init__.py +5 -0
  15. pdfaccessguru/exceptions/corrector_exceptions/could_not_correct_detected_violation_exception.py +8 -0
  16. pdfaccessguru/exceptions/corrector_exceptions/could_not_extract_children_exception.py +12 -0
  17. pdfaccessguru/exceptions/corrector_exceptions/no_corrector_implemented_exception.py +9 -0
  18. pdfaccessguru/exceptions/invalid_pdf_path_exception.py +11 -0
  19. pdfaccessguru/exceptions/llm_exceptions/__init__.py +3 -0
  20. pdfaccessguru/exceptions/llm_exceptions/open_router_connection_failed_exception.py +9 -0
  21. pdfaccessguru/exceptions/mapping_exceptions/__init__.py +6 -0
  22. pdfaccessguru/exceptions/mapping_exceptions/could_not_find_affected_element_exception.py +10 -0
  23. pdfaccessguru/exceptions/mapping_exceptions/resolve_context_returns_whole_pdf_exception.py +17 -0
  24. pdfaccessguru/exceptions/mapping_exceptions/step_function_traversal_exception.py +12 -0
  25. pdfaccessguru/exceptions/verapdf_exceptions/__init__.py +5 -0
  26. pdfaccessguru/exceptions/verapdf_exceptions/oscli_exceptions/__init__.py +5 -0
  27. pdfaccessguru/exceptions/verapdf_exceptions/oscli_exceptions/cli_execution_failed_exception.py +8 -0
  28. pdfaccessguru/exceptions/verapdf_exceptions/verapdf_not_installed_exception.py +7 -0
  29. pdfaccessguru/exceptions/verapdf_exceptions/verapdf_parse_error_exception.py +10 -0
  30. pdfaccessguru/exceptions/verapdf_exceptions/verapdf_produced_no_output_exception.py +6 -0
  31. pdfaccessguru/exceptions/verapdf_exceptions/verapdf_wrong_version.py +7 -0
  32. pdfaccessguru/external_tools.py +105 -0
  33. pdfaccessguru/model/LLM/image.py +30 -0
  34. pdfaccessguru/model/__init__.py +5 -0
  35. pdfaccessguru/model/detected_violation.py +24 -0
  36. pdfaccessguru/model/map_violation/__init__.py +5 -0
  37. pdfaccessguru/model/map_violation/taxonomy_data.py +14 -0
  38. pdfaccessguru/model/pipeline_report/__init__.py +31 -0
  39. pdfaccessguru/model/pipeline_report/correction_run.py +25 -0
  40. pdfaccessguru/model/pipeline_report/mapping_run.py +22 -0
  41. pdfaccessguru/model/pipeline_report/phase_summary.py +54 -0
  42. pdfaccessguru/model/pipeline_report/pipeline_report.py +130 -0
  43. pdfaccessguru/model/pipeline_report/report_level.py +19 -0
  44. pdfaccessguru/model/pipeline_report/report_line.py +29 -0
  45. pdfaccessguru/model/pipeline_report/report_phase.py +54 -0
  46. pdfaccessguru/model/supplementary_information/__init__.py +8 -0
  47. pdfaccessguru/model/supplementary_information/supplementary_information.py +79 -0
  48. pdfaccessguru/model/supplementary_information/table/bounding_box.py +11 -0
  49. pdfaccessguru/model/supplementary_information/table/grid.py +76 -0
  50. pdfaccessguru/model/verapdf_violation.py +22 -0
  51. pdfaccessguru/model/violation.py +20 -0
  52. pdfaccessguru/model/violation_category.py +9 -0
  53. pdfaccessguru/pipeline/PDFAccessGuruCorrect/__init__.py +3 -0
  54. pdfaccessguru/pipeline/PDFAccessGuruCorrect/correct.py +86 -0
  55. pdfaccessguru/pipeline/PDFAccessGuruCorrect/correctors/__init__.py +6 -0
  56. pdfaccessguru/pipeline/PDFAccessGuruCorrect/correctors/corrector.py +74 -0
  57. pdfaccessguru/pipeline/PDFAccessGuruCorrect/correctors/descriptions/__init__.py +5 -0
  58. pdfaccessguru/pipeline/PDFAccessGuruCorrect/correctors/descriptions/annotations.py +119 -0
  59. pdfaccessguru/pipeline/PDFAccessGuruCorrect/correctors/descriptions/links.py +188 -0
  60. pdfaccessguru/pipeline/PDFAccessGuruCorrect/correctors/descriptions/pictures.py +80 -0
  61. pdfaccessguru/pipeline/PDFAccessGuruCorrect/correctors/font_repair/__init__.py +8 -0
  62. pdfaccessguru/pipeline/PDFAccessGuruCorrect/correctors/font_repair/embed.py +220 -0
  63. pdfaccessguru/pipeline/PDFAccessGuruCorrect/correctors/font_repair/fonts.py +300 -0
  64. pdfaccessguru/pipeline/PDFAccessGuruCorrect/correctors/font_repair/programs.py +278 -0
  65. pdfaccessguru/pipeline/PDFAccessGuruCorrect/correctors/font_repair/sources.py +162 -0
  66. pdfaccessguru/pipeline/PDFAccessGuruCorrect/correctors/structure_repair/__init__.py +9 -0
  67. pdfaccessguru/pipeline/PDFAccessGuruCorrect/correctors/structure_repair/annotation_tags.py +124 -0
  68. pdfaccessguru/pipeline/PDFAccessGuruCorrect/correctors/structure_repair/artifacts.py +184 -0
  69. pdfaccessguru/pipeline/PDFAccessGuruCorrect/correctors/structure_repair/layout.py +168 -0
  70. pdfaccessguru/pipeline/PDFAccessGuruCorrect/correctors/structure_repair/lists.py +230 -0
  71. pdfaccessguru/pipeline/PDFAccessGuruCorrect/correctors/structure_repair/roles.py +174 -0
  72. pdfaccessguru/pipeline/PDFAccessGuruCorrect/correctors/structure_repair/tables.py +300 -0
  73. pdfaccessguru/pipeline/PDFAccessGuruCorrect/correctors/structure_repair/toc_and_headings.py +129 -0
  74. pdfaccessguru/pipeline/PDFAccessGuruCorrect/correctors/structure_repair/tree.py +206 -0
  75. pdfaccessguru/pipeline/PDFAccessGuruCorrect/correctors/syntax/__init__.py +4 -0
  76. pdfaccessguru/pipeline/PDFAccessGuruCorrect/correctors/syntax/syntax_correctors_1_aran_moradi/5_1_id_scheme_missing.py +21 -0
  77. pdfaccessguru/pipeline/PDFAccessGuruCorrect/correctors/syntax/syntax_correctors_1_aran_moradi/5_2_invalid_part_property.py +21 -0
  78. pdfaccessguru/pipeline/PDFAccessGuruCorrect/correctors/syntax/syntax_correctors_1_aran_moradi/5_3_invalid_part_namespace_prefix.py +21 -0
  79. pdfaccessguru/pipeline/PDFAccessGuruCorrect/correctors/syntax/syntax_correctors_1_aran_moradi/5_4_invalid_amd_property.py +21 -0
  80. pdfaccessguru/pipeline/PDFAccessGuruCorrect/correctors/syntax/syntax_correctors_1_aran_moradi/5_5_invalid_corr_property.py +21 -0
  81. pdfaccessguru/pipeline/PDFAccessGuruCorrect/correctors/syntax/syntax_correctors_1_aran_moradi/6_1_1_invalid_pdf_version.py +63 -0
  82. pdfaccessguru/pipeline/PDFAccessGuruCorrect/correctors/syntax/syntax_correctors_1_aran_moradi/6_2_1_markinfo_dictionary_missing.py +47 -0
  83. pdfaccessguru/pipeline/PDFAccessGuruCorrect/correctors/syntax/syntax_correctors_1_aran_moradi/7_1_1_artifact_present_inside_tagged_content.py +175 -0
  84. pdfaccessguru/pipeline/PDFAccessGuruCorrect/correctors/syntax/syntax_correctors_1_aran_moradi/7_1_2_tagged_content_inside_artifact.py +38 -0
  85. pdfaccessguru/pipeline/PDFAccessGuruCorrect/correctors/syntax/syntax_correctors_1_aran_moradi/7_1_3_content_neither_artifact_nor_tagged_content.py +549 -0
  86. pdfaccessguru/pipeline/PDFAccessGuruCorrect/correctors/syntax/syntax_correctors_1_aran_moradi/7_1_4_suspects_entry_true.py +28 -0
  87. pdfaccessguru/pipeline/PDFAccessGuruCorrect/correctors/syntax/syntax_correctors_1_aran_moradi/7_1_5_not_mapped_to_standard_type.py +23 -0
  88. pdfaccessguru/pipeline/PDFAccessGuruCorrect/correctors/syntax/syntax_correctors_1_aran_moradi/7_1_6_circular_mapping_exists.py +21 -0
  89. pdfaccessguru/pipeline/PDFAccessGuruCorrect/correctors/syntax/syntax_correctors_1_aran_moradi/__init__.py +168 -0
  90. pdfaccessguru/pipeline/PDFAccessGuruCorrect/correctors/syntax/syntax_correctors_1_aran_moradi/constants.py +12 -0
  91. pdfaccessguru/pipeline/PDFAccessGuruCorrect/correctors/syntax/syntax_correctors_1_aran_moradi/pdfua_identification.py +58 -0
  92. pdfaccessguru/pipeline/PDFAccessGuruCorrect/correctors/syntax/syntax_correctors_2_siju_li/7_1_t07_standard_type_remapped.py +59 -0
  93. pdfaccessguru/pipeline/PDFAccessGuruCorrect/correctors/syntax/syntax_correctors_2_siju_li/7_1_t08_metadata_key_missing.py +37 -0
  94. pdfaccessguru/pipeline/PDFAccessGuruCorrect/correctors/syntax/syntax_correctors_2_siju_li/7_1_t09_missing_metadata_stream_dc_title.py +360 -0
  95. pdfaccessguru/pipeline/PDFAccessGuruCorrect/correctors/syntax/syntax_correctors_2_siju_li/7_1_t10_display_doc_title_false.py +23 -0
  96. pdfaccessguru/pipeline/PDFAccessGuruCorrect/correctors/syntax/syntax_correctors_2_siju_li/7_1_t11_missing_struct_tree_root_entry.py +26 -0
  97. pdfaccessguru/pipeline/PDFAccessGuruCorrect/correctors/syntax/syntax_correctors_2_siju_li/7_1_t12_parent_entry_missing.py +43 -0
  98. pdfaccessguru/pipeline/PDFAccessGuruCorrect/correctors/syntax/syntax_correctors_2_siju_li/7_2_t02_natural_language_undefined.py +55 -0
  99. pdfaccessguru/pipeline/PDFAccessGuruCorrect/correctors/syntax/syntax_correctors_2_siju_li/7_2_t03_table_specific_elements_missing.py +32 -0
  100. pdfaccessguru/pipeline/PDFAccessGuruCorrect/correctors/syntax/syntax_correctors_2_siju_li/7_2_t04_tr_element_missing.py +77 -0
  101. pdfaccessguru/pipeline/PDFAccessGuruCorrect/correctors/syntax/syntax_correctors_2_siju_li/7_2_t05_thead_element_missing.py +13 -0
  102. pdfaccessguru/pipeline/PDFAccessGuruCorrect/correctors/syntax/syntax_correctors_2_siju_li/7_2_t06_tbody_element_missing.py +13 -0
  103. pdfaccessguru/pipeline/PDFAccessGuruCorrect/correctors/syntax/syntax_correctors_2_siju_li/7_2_t07_tfoot_element_missing.py +22 -0
  104. pdfaccessguru/pipeline/PDFAccessGuruCorrect/correctors/syntax/syntax_correctors_2_siju_li/7_2_t08_th_not_contained_tr.py +28 -0
  105. pdfaccessguru/pipeline/PDFAccessGuruCorrect/correctors/syntax/syntax_correctors_2_siju_li/__init__.py +170 -0
  106. pdfaccessguru/pipeline/PDFAccessGuruCorrect/correctors/syntax/syntax_correctors_2_siju_li/language_utils.py +244 -0
  107. pdfaccessguru/pipeline/PDFAccessGuruCorrect/correctors/syntax/syntax_correctors_2_siju_li/utils.py +137 -0
  108. pdfaccessguru/pipeline/PDFAccessGuruCorrect/correctors/syntax/syntax_correctors_3_robin_hart/7_2_t09_td_not_contained_tr.py +177 -0
  109. pdfaccessguru/pipeline/PDFAccessGuruCorrect/correctors/syntax/syntax_correctors_3_robin_hart/7_2_t10_th_td_missing.py +27 -0
  110. pdfaccessguru/pipeline/PDFAccessGuruCorrect/correctors/syntax/syntax_correctors_3_robin_hart/7_2_t11_table_more_one_thead_kid.py +93 -0
  111. pdfaccessguru/pipeline/PDFAccessGuruCorrect/correctors/syntax/syntax_correctors_3_robin_hart/7_2_t12_table_more_one_tfoot_kid.py +93 -0
  112. pdfaccessguru/pipeline/PDFAccessGuruCorrect/correctors/syntax/syntax_correctors_3_robin_hart/7_2_t13_table_tfoot_kid_not_tbody_kids.py +84 -0
  113. pdfaccessguru/pipeline/PDFAccessGuruCorrect/correctors/syntax/syntax_correctors_3_robin_hart/7_2_t14_table_thead_kid_not_tbody_kids.py +84 -0
  114. pdfaccessguru/pipeline/PDFAccessGuruCorrect/correctors/syntax/syntax_correctors_3_robin_hart/7_2_t15_table_cell_intersection_other_cells.py +114 -0
  115. pdfaccessguru/pipeline/PDFAccessGuruCorrect/correctors/syntax/syntax_correctors_3_robin_hart/7_2_t16_table_caption_child.py +63 -0
  116. pdfaccessguru/pipeline/PDFAccessGuruCorrect/correctors/syntax/syntax_correctors_3_robin_hart/7_2_t17_li_not_contained_l.py +33 -0
  117. pdfaccessguru/pipeline/PDFAccessGuruCorrect/correctors/syntax/syntax_correctors_3_robin_hart/7_2_t18_lbody_not_contained_li.py +34 -0
  118. pdfaccessguru/pipeline/PDFAccessGuruCorrect/correctors/syntax/syntax_correctors_3_robin_hart/7_2_t19_invalid_elements_l.py +26 -0
  119. pdfaccessguru/pipeline/PDFAccessGuruCorrect/correctors/syntax/syntax_correctors_3_robin_hart/7_2_t20_invalid_elements_li.py +25 -0
  120. pdfaccessguru/pipeline/PDFAccessGuruCorrect/correctors/syntax/syntax_correctors_3_robin_hart/7_2_t21_natural_language_actual_text_attribute_undefined.py +140 -0
  121. pdfaccessguru/pipeline/PDFAccessGuruCorrect/correctors/syntax/syntax_correctors_3_robin_hart/__init__.py +175 -0
  122. pdfaccessguru/pipeline/PDFAccessGuruCorrect/correctors/syntax/syntax_correctors_3_robin_hart/prompts/__init__.py +5 -0
  123. pdfaccessguru/pipeline/PDFAccessGuruCorrect/correctors/syntax/syntax_correctors_3_robin_hart/prompts/determine_lang_tag_system_prompt.txt +23 -0
  124. pdfaccessguru/pipeline/PDFAccessGuruCorrect/correctors/syntax/syntax_correctors_3_robin_hart/prompts/determine_lang_tag_user_prompt.txt +1 -0
  125. pdfaccessguru/pipeline/PDFAccessGuruCorrect/correctors/syntax/syntax_correctors_3_robin_hart/prompts/td_not_containd_tr_system_prompt.txt +104 -0
  126. pdfaccessguru/pipeline/PDFAccessGuruCorrect/correctors/syntax/syntax_correctors_3_robin_hart/prompts/td_not_containd_tr_user_prompt.txt +8 -0
  127. pdfaccessguru/pipeline/PDFAccessGuruCorrect/correctors/syntax/syntax_correctors_3_robin_hart/utils.py +31 -0
  128. pdfaccessguru/pipeline/PDFAccessGuruCorrect/correctors/syntax/syntax_correctors_4_riard_qupevaj/7_2_t22_natural_language_alt_attribute_undefined.py +24 -0
  129. pdfaccessguru/pipeline/PDFAccessGuruCorrect/correctors/syntax/syntax_correctors_4_riard_qupevaj/7_2_t23_natural_language_e_attribute_undefined.py +24 -0
  130. pdfaccessguru/pipeline/PDFAccessGuruCorrect/correctors/syntax/syntax_correctors_4_riard_qupevaj/7_2_t24_natural_language_annotations_undefined.py +24 -0
  131. pdfaccessguru/pipeline/PDFAccessGuruCorrect/correctors/syntax/syntax_correctors_4_riard_qupevaj/7_2_t25_natural_language_tu_form_fields_undefined.py +24 -0
  132. pdfaccessguru/pipeline/PDFAccessGuruCorrect/correctors/syntax/syntax_correctors_4_riard_qupevaj/7_2_t26_toci_missing_from_toc.py +28 -0
  133. pdfaccessguru/pipeline/PDFAccessGuruCorrect/correctors/syntax/syntax_correctors_4_riard_qupevaj/7_2_t27_toc_contains_invalid_element.py +26 -0
  134. pdfaccessguru/pipeline/PDFAccessGuruCorrect/correctors/syntax/syntax_correctors_4_riard_qupevaj/7_2_t28_toc_caption_child.py +43 -0
  135. pdfaccessguru/pipeline/PDFAccessGuruCorrect/correctors/syntax/syntax_correctors_4_riard_qupevaj/7_2_t29_invalid_lang_entry.py +11 -0
  136. pdfaccessguru/pipeline/PDFAccessGuruCorrect/correctors/syntax/syntax_correctors_4_riard_qupevaj/7_2_t30_natural_language_actualtext_span_undetermined.py +25 -0
  137. pdfaccessguru/pipeline/PDFAccessGuruCorrect/correctors/syntax/syntax_correctors_4_riard_qupevaj/7_2_t31_natural_language_alt_attribute_span_undetermined.py +25 -0
  138. pdfaccessguru/pipeline/PDFAccessGuruCorrect/correctors/syntax/syntax_correctors_4_riard_qupevaj/7_2_t32_natural_language_e_attribute_span_undetermined.py +25 -0
  139. pdfaccessguru/pipeline/PDFAccessGuruCorrect/correctors/syntax/syntax_correctors_4_riard_qupevaj/7_2_t33_natural_language_metadata_undetermined.py +24 -0
  140. pdfaccessguru/pipeline/PDFAccessGuruCorrect/correctors/syntax/syntax_correctors_4_riard_qupevaj/7_2_t34_natural_language_text_page_undetermined.py +25 -0
  141. pdfaccessguru/pipeline/PDFAccessGuruCorrect/correctors/syntax/syntax_correctors_4_riard_qupevaj/__init__.py +82 -0
  142. pdfaccessguru/pipeline/PDFAccessGuruCorrect/correctors/syntax/syntax_correctors_4_riard_qupevaj/language_utils.py +181 -0
  143. pdfaccessguru/pipeline/PDFAccessGuruCorrect/correctors/syntax/syntax_correctors_4_riard_qupevaj/utils.py +153 -0
  144. pdfaccessguru/pipeline/PDFAccessGuruCorrect/correctors/syntax/syntax_correctors_5_brian_karle/7_2_t36_thead_invalid_elements.py +30 -0
  145. pdfaccessguru/pipeline/PDFAccessGuruCorrect/correctors/syntax/syntax_correctors_5_brian_karle/7_2_t37_tbody_invalid_elements.py +30 -0
  146. pdfaccessguru/pipeline/PDFAccessGuruCorrect/correctors/syntax/syntax_correctors_5_brian_karle/7_2_t38_tfoot_invalid_elements.py +30 -0
  147. pdfaccessguru/pipeline/PDFAccessGuruCorrect/correctors/syntax/syntax_correctors_5_brian_karle/7_2_t39_table_several_captions.py +829 -0
  148. pdfaccessguru/pipeline/PDFAccessGuruCorrect/correctors/syntax/syntax_correctors_5_brian_karle/7_2_t40_l_caption_child_not_first.py +63 -0
  149. pdfaccessguru/pipeline/PDFAccessGuruCorrect/correctors/syntax/syntax_correctors_5_brian_karle/7_2_t41_table_columns_different_number_rows.py +446 -0
  150. pdfaccessguru/pipeline/PDFAccessGuruCorrect/correctors/syntax/syntax_correctors_5_brian_karle/7_2_t42_table_rows_span_different_number_columns.py +488 -0
  151. pdfaccessguru/pipeline/PDFAccessGuruCorrect/correctors/syntax/syntax_correctors_5_brian_karle/7_2_t43_table_rows_span_different_number_columns_02.py +41 -0
  152. pdfaccessguru/pipeline/PDFAccessGuruCorrect/correctors/syntax/syntax_correctors_5_brian_karle/7_3_t01_figure_structure_alternate_description_missing.py +611 -0
  153. pdfaccessguru/pipeline/PDFAccessGuruCorrect/correctors/syntax/syntax_correctors_5_brian_karle/7_4_2_t01_heading_level_skipped.py +141 -0
  154. pdfaccessguru/pipeline/PDFAccessGuruCorrect/correctors/syntax/syntax_correctors_5_brian_karle/7_4_4_t01_node_more_one_h_tag.py +29 -0
  155. pdfaccessguru/pipeline/PDFAccessGuruCorrect/correctors/syntax/syntax_correctors_5_brian_karle/7_4_4_t02_document_uses_h_h_sharp_tags.py +359 -0
  156. pdfaccessguru/pipeline/PDFAccessGuruCorrect/correctors/syntax/syntax_correctors_5_brian_karle/7_4_4_t03_document_uses_hn_h_tags.py +40 -0
  157. pdfaccessguru/pipeline/PDFAccessGuruCorrect/correctors/syntax/syntax_correctors_5_brian_karle/__init__.py +82 -0
  158. pdfaccessguru/pipeline/PDFAccessGuruCorrect/correctors/syntax/syntax_correctors_5_brian_karle/heading_context_utils.py +234 -0
  159. pdfaccessguru/pipeline/PDFAccessGuruCorrect/correctors/syntax/syntax_correctors_6_florian_walcher/__init__.py +18 -0
  160. pdfaccessguru/pipeline/PDFAccessGuruCorrect/correctors/syntax/syntax_correctors_6_florian_walcher/correct_annotation_artifact_not_nested.py +29 -0
  161. pdfaccessguru/pipeline/PDFAccessGuruCorrect/correctors/syntax/syntax_correctors_6_florian_walcher/correct_dynamic_xfa_forms_present.py +29 -0
  162. pdfaccessguru/pipeline/PDFAccessGuruCorrect/correctors/syntax/syntax_correctors_6_florian_walcher/correct_encrypted_file_p_missing.py +29 -0
  163. pdfaccessguru/pipeline/PDFAccessGuruCorrect/correctors/syntax/syntax_correctors_6_florian_walcher/correct_f_uf_key_invalid.py +54 -0
  164. pdfaccessguru/pipeline/PDFAccessGuruCorrect/correctors/syntax/syntax_correctors_6_florian_walcher/correct_form_field_missing_alt_entry.py +94 -0
  165. pdfaccessguru/pipeline/PDFAccessGuruCorrect/correctors/syntax/syntax_correctors_6_florian_walcher/correct_formula_structure_no_alternative_description.py +166 -0
  166. pdfaccessguru/pipeline/PDFAccessGuruCorrect/correctors/syntax/syntax_correctors_6_florian_walcher/correct_id_note_tag_missing.py +27 -0
  167. pdfaccessguru/pipeline/PDFAccessGuruCorrect/correctors/syntax/syntax_correctors_6_florian_walcher/correct_id_note_tag_non_unique.py +152 -0
  168. pdfaccessguru/pipeline/PDFAccessGuruCorrect/correctors/syntax/syntax_correctors_6_florian_walcher/correct_invalid_annotation.py +28 -0
  169. pdfaccessguru/pipeline/PDFAccessGuruCorrect/correctors/syntax/syntax_correctors_6_florian_walcher/correct_missing_empty_name.py +60 -0
  170. pdfaccessguru/pipeline/PDFAccessGuruCorrect/correctors/syntax/syntax_correctors_6_florian_walcher/correct_present_optional_content_configuration.py +26 -0
  171. pdfaccessguru/pipeline/PDFAccessGuruCorrect/correctors/syntax/syntax_correctors_6_florian_walcher/correct_td_headers_missing.py +44 -0
  172. pdfaccessguru/pipeline/PDFAccessGuruCorrect/correctors/syntax/syntax_correctors_6_florian_walcher/correct_td_undefined_headers.py +55 -0
  173. pdfaccessguru/pipeline/PDFAccessGuruCorrect/correctors/syntax/syntax_correctors_6_florian_walcher/table_headers.py +112 -0
  174. pdfaccessguru/pipeline/PDFAccessGuruCorrect/correctors/syntax/syntax_correctors_7_marwa_ayache/__init__.py +36 -0
  175. pdfaccessguru/pipeline/PDFAccessGuruCorrect/correctors/syntax/syntax_correctors_7_marwa_ayache/alt_missing_media_clip_data.py +331 -0
  176. pdfaccessguru/pipeline/PDFAccessGuruCorrect/correctors/syntax/syntax_correctors_7_marwa_ayache/annotation_subtype_trapnet_exists.py +38 -0
  177. pdfaccessguru/pipeline/PDFAccessGuruCorrect/correctors/syntax/syntax_correctors_7_marwa_ayache/common.py +32 -0
  178. pdfaccessguru/pipeline/PDFAccessGuruCorrect/correctors/syntax/syntax_correctors_7_marwa_ayache/ct_missing_media_clip_data.py +95 -0
  179. pdfaccessguru/pipeline/PDFAccessGuruCorrect/correctors/syntax/syntax_correctors_7_marwa_ayache/document_reference_xobject.py +26 -0
  180. pdfaccessguru/pipeline/PDFAccessGuruCorrect/correctors/syntax/syntax_correctors_7_marwa_ayache/form_multiple_children.py +46 -0
  181. pdfaccessguru/pipeline/PDFAccessGuruCorrect/correctors/syntax/syntax_correctors_7_marwa_ayache/form_xobject_mcids_multi_referenced.py +86 -0
  182. pdfaccessguru/pipeline/PDFAccessGuruCorrect/correctors/syntax/syntax_correctors_7_marwa_ayache/incompatible_CID_entries.py +43 -0
  183. pdfaccessguru/pipeline/PDFAccessGuruCorrect/correctors/syntax/syntax_correctors_7_marwa_ayache/invalid_type_2_cidfont.py +21 -0
  184. pdfaccessguru/pipeline/PDFAccessGuruCorrect/correctors/syntax/syntax_correctors_7_marwa_ayache/link_annotation_artifact_not_nested_tag_link.py +297 -0
  185. pdfaccessguru/pipeline/PDFAccessGuruCorrect/correctors/syntax/syntax_correctors_7_marwa_ayache/link_annotation_missing_alternate_description_contents.py +114 -0
  186. pdfaccessguru/pipeline/PDFAccessGuruCorrect/correctors/syntax/syntax_correctors_7_marwa_ayache/page_annotation_tabs_s.py +22 -0
  187. pdfaccessguru/pipeline/PDFAccessGuruCorrect/correctors/syntax/syntax_correctors_7_marwa_ayache/printermark_annotation_logical_structure.py +73 -0
  188. pdfaccessguru/pipeline/PDFAccessGuruCorrect/correctors/syntax/syntax_correctors_7_marwa_ayache/prompts/__init__.py +3 -0
  189. pdfaccessguru/pipeline/PDFAccessGuruCorrect/correctors/syntax/syntax_correctors_7_marwa_ayache/prompts/link_annotation_contents_system_prompt.txt +6 -0
  190. pdfaccessguru/pipeline/PDFAccessGuruCorrect/correctors/syntax/syntax_correctors_7_marwa_ayache/prompts/link_annotation_contents_user_prompt.txt +11 -0
  191. pdfaccessguru/pipeline/PDFAccessGuruCorrect/correctors/syntax/syntax_correctors_7_marwa_ayache/prompts/media_clip_alt_system_prompt.txt +7 -0
  192. pdfaccessguru/pipeline/PDFAccessGuruCorrect/correctors/syntax/syntax_correctors_7_marwa_ayache/prompts/media_clip_alt_user_prompt.txt +14 -0
  193. pdfaccessguru/pipeline/PDFAccessGuruCorrect/correctors/syntax/syntax_correctors_7_marwa_ayache/widget_annotation_artifact_nested_tag_form.py +80 -0
  194. pdfaccessguru/pipeline/PDFAccessGuruCorrect/correctors/syntax/syntax_correctors_8_babett_ludwig/7_21_3_3_t01_non_standard_cmap_embedded.py +79 -0
  195. pdfaccessguru/pipeline/PDFAccessGuruCorrect/correctors/syntax/syntax_correctors_8_babett_ludwig/7_21_3_3_t02_wmode_embedded_cmap_not_identical.py +55 -0
  196. pdfaccessguru/pipeline/PDFAccessGuruCorrect/correctors/syntax/syntax_correctors_8_babett_ludwig/7_21_3_3_t03_cmap_references_another_non_standard_cmap.py +60 -0
  197. pdfaccessguru/pipeline/PDFAccessGuruCorrect/correctors/syntax/syntax_correctors_8_babett_ludwig/7_21_4_1_t01_font_program_embedded.py +34 -0
  198. pdfaccessguru/pipeline/PDFAccessGuruCorrect/correctors/syntax/syntax_correctors_8_babett_ludwig/7_21_4_1_t02_referenced_glyphs_missing.py +501 -0
  199. pdfaccessguru/pipeline/PDFAccessGuruCorrect/correctors/syntax/syntax_correctors_8_babett_ludwig/7_21_4_2_t01_charset_incorrectly_lists_glyphs.py +35 -0
  200. pdfaccessguru/pipeline/PDFAccessGuruCorrect/correctors/syntax/syntax_correctors_8_babett_ludwig/7_21_4_2_t02_cid_incorrectly_identifies_glpyhs.py +16 -0
  201. pdfaccessguru/pipeline/PDFAccessGuruCorrect/correctors/syntax/syntax_correctors_8_babett_ludwig/7_21_5_t01_glyph_width_inconsistent.py +27 -0
  202. pdfaccessguru/pipeline/PDFAccessGuruCorrect/correctors/syntax/syntax_correctors_8_babett_ludwig/7_21_6_t01_non_symbolic_cmap_missing.py +26 -0
  203. pdfaccessguru/pipeline/PDFAccessGuruCorrect/correctors/syntax/syntax_correctors_8_babett_ludwig/7_21_6_t02_invalid_non_symbolic_font_Adobe_mapping.py +43 -0
  204. pdfaccessguru/pipeline/PDFAccessGuruCorrect/correctors/syntax/syntax_correctors_8_babett_ludwig/7_21_6_t03_symbolic_true_type_font_contains_encoding.py +32 -0
  205. pdfaccessguru/pipeline/PDFAccessGuruCorrect/correctors/syntax/syntax_correctors_8_babett_ludwig/7_21_6_t04_embedded_font_program_multiple_cmap_subtables.py +26 -0
  206. pdfaccessguru/pipeline/PDFAccessGuruCorrect/correctors/syntax/syntax_correctors_8_babett_ludwig/7_21_7_t01_glyph_cannot_mapped_unicode.py +598 -0
  207. pdfaccessguru/pipeline/PDFAccessGuruCorrect/correctors/syntax/syntax_correctors_8_babett_ludwig/7_21_7_t02_glyph_invalid_unicode.py +206 -0
  208. pdfaccessguru/pipeline/PDFAccessGuruCorrect/correctors/syntax/syntax_correctors_8_babett_ludwig/7_21_8t01_document_reference_notdef_glyph.py +146 -0
  209. pdfaccessguru/pipeline/PDFAccessGuruCorrect/correctors/syntax/syntax_correctors_8_babett_ludwig/__init__.py +72 -0
  210. pdfaccessguru/pipeline/PDFAccessGuruCorrect/correctors/syntax/syntax_correctors_8_babett_ludwig/fonts/DejaVuSans.ttf +0 -0
  211. pdfaccessguru/pipeline/PDFAccessGuruCorrect/correctors/syntax/syntax_correctors_8_babett_ludwig/fonts/LiberationSans-Regular.ttf +0 -0
  212. pdfaccessguru/pipeline/PDFAccessGuruDetect/__init__.py +3 -0
  213. pdfaccessguru/pipeline/PDFAccessGuruDetect/detect.py +135 -0
  214. pdfaccessguru/pipeline/PDFAccessGuruDetect/map_violation/__init__.py +8 -0
  215. pdfaccessguru/pipeline/PDFAccessGuruDetect/map_violation/collect_supplementary_information/__init__.py +6 -0
  216. pdfaccessguru/pipeline/PDFAccessGuruDetect/map_violation/collect_supplementary_information/collect_supplementary_information.py +45 -0
  217. pdfaccessguru/pipeline/PDFAccessGuruDetect/map_violation/collect_supplementary_information/collectors_3_robin_hart/__init__.py +3 -0
  218. pdfaccessguru/pipeline/PDFAccessGuruDetect/map_violation/collect_supplementary_information/collectors_3_robin_hart/table_grid_analysis.py +520 -0
  219. pdfaccessguru/pipeline/PDFAccessGuruDetect/map_violation/collect_supplementary_information/collectors_3_robin_hart/v7_2_t09_td_not_contained_tr.py +403 -0
  220. pdfaccessguru/pipeline/PDFAccessGuruDetect/map_violation/collect_supplementary_information/collectors_3_robin_hart/v7_2_t15_table_cell_intersection_other_cells.py +51 -0
  221. pdfaccessguru/pipeline/PDFAccessGuruDetect/map_violation/collect_supplementary_information/registry.py +890 -0
  222. pdfaccessguru/pipeline/PDFAccessGuruDetect/map_violation/collect_supplementary_information/utils/__init__.py +3 -0
  223. pdfaccessguru/pipeline/PDFAccessGuruDetect/map_violation/collect_supplementary_information/utils/structure_tree_helpers.py +57 -0
  224. pdfaccessguru/pipeline/PDFAccessGuruDetect/map_violation/detect_affected_element/__init__.py +8 -0
  225. pdfaccessguru/pipeline/PDFAccessGuruDetect/map_violation/detect_affected_element/detect_affected_element.py +83 -0
  226. pdfaccessguru/pipeline/PDFAccessGuruDetect/map_violation/detect_affected_element/registry.py +1101 -0
  227. pdfaccessguru/pipeline/PDFAccessGuruDetect/map_violation/detect_affected_element/resolve_context.py +537 -0
  228. pdfaccessguru/pipeline/PDFAccessGuruDetect/map_violation/map_violation.py +53 -0
  229. pdfaccessguru/pipeline/PDFAccessGuruDetect/map_violation/taxonomy_data_mapping/__init__.py +6 -0
  230. pdfaccessguru/pipeline/PDFAccessGuruDetect/map_violation/taxonomy_data_mapping/map_verapdf_violation_to_taxonomy_data.py +271 -0
  231. pdfaccessguru/pipeline/PDFAccessGuruDetect/verapdf/__init__.py +5 -0
  232. pdfaccessguru/pipeline/PDFAccessGuruDetect/verapdf/oscli/__init__.py +3 -0
  233. pdfaccessguru/pipeline/PDFAccessGuruDetect/verapdf/oscli/platform_prompt.py +88 -0
  234. pdfaccessguru/pipeline/PDFAccessGuruDetect/verapdf/verapdf_runner.py +120 -0
  235. pdfaccessguru/pipeline/__init__.py +10 -0
  236. pdfaccessguru/pipeline/contrast.py +380 -0
  237. pdfaccessguru/pipeline/meaning.py +370 -0
  238. pdfaccessguru/pipeline/pipeline.py +325 -0
  239. pdfaccessguru/pipeline/protection.py +63 -0
  240. pdfaccessguru/pipeline/reporting/__init__.py +32 -0
  241. pdfaccessguru/pipeline/reporting/console.py +75 -0
  242. pdfaccessguru/pipeline/reporting/context.py +59 -0
  243. pdfaccessguru/pipeline/reporting/scopes.py +94 -0
  244. pdfaccessguru/pipeline/reporting/summary.py +124 -0
  245. pdfaccessguru/pipeline/reporting/writer.py +102 -0
  246. pdfaccessguru/pipeline/stages.py +162 -0
  247. pdfaccessguru/pipeline/text_tagging.py +72 -0
  248. pdfaccessguru/pipeline/transaction.py +219 -0
  249. pdfaccessguru/reporting/__init__.py +24 -0
  250. pdfaccessguru/reporting/customer_report.py +548 -0
  251. pdfaccessguru/reporting/pdf_report.py +208 -0
  252. pdfaccessguru/reporting/plain_language.py +391 -0
  253. pdfaccessguru/reporting/report_language.py +216 -0
  254. pdfaccessguru/reporting/report_metrics.py +172 -0
  255. pdfaccessguru/reporting/review_list.py +343 -0
  256. pdfaccessguru/reporting/taxonomy_loader.py +68 -0
  257. pdfaccessguru/reporting/technical_report.py +145 -0
  258. pdfaccessguru/reporting/violation_reference.py +56 -0
  259. pdfaccessguru/resources/__init__.py +1 -0
  260. pdfaccessguru/resources/taxonomy.md +149 -0
  261. pdfaccessguru/script_evaluate_folder_and_generate_statistics.py +558 -0
  262. pdfaccessguru/setup_check.py +81 -0
  263. pdfaccessguru/shortcuts.py +104 -0
  264. pdfaccessguru/tagging/__init__.py +16 -0
  265. pdfaccessguru/tagging/builder.py +229 -0
  266. pdfaccessguru/tagging/layout.py +339 -0
  267. pdfaccessguru/tagging/marking.py +318 -0
  268. pdfaccessguru/tool_installer.py +207 -0
  269. pdfaccessguru/updates.py +171 -0
  270. pdfaccessguru/utils/__init__.py +35 -0
  271. pdfaccessguru/utils/cli_colors.py +32 -0
  272. pdfaccessguru/utils/compare_segment_parsing.py +16 -0
  273. pdfaccessguru/utils/describers.py +47 -0
  274. pdfaccessguru/utils/environment_variables.py +25 -0
  275. pdfaccessguru/utils/page_text.py +39 -0
  276. pdfaccessguru/utils/pdf_names.py +30 -0
  277. pdfaccessguru/webapp/__init__.py +1 -0
  278. pdfaccessguru/webapp/markdown_html.py +90 -0
  279. pdfaccessguru/webapp/pages.py +654 -0
  280. pdfaccessguru/webapp/preview.py +140 -0
  281. pdfaccessguru/webapp/review.py +489 -0
  282. pdfaccessguru/webapp/server.py +833 -0
  283. pdfaccessguru/webapp/static/OFL-Atkinson-Hyperlegible.txt +92 -0
  284. pdfaccessguru/webapp/static/atkinson-hyperlegible-400-latin-ext.woff2 +0 -0
  285. pdfaccessguru/webapp/static/atkinson-hyperlegible-400-latin.woff2 +0 -0
  286. pdfaccessguru/webapp/static/atkinson-hyperlegible-700-latin-ext.woff2 +0 -0
  287. pdfaccessguru/webapp/static/atkinson-hyperlegible-700-latin.woff2 +0 -0
  288. pdfaccessguru/webapp/static/icon.svg +19 -0
  289. pdfaccessguru/webapp/static/logo.svg +24 -0
  290. pdfaccessguru-1.0.0.dist-info/METADATA +72 -0
  291. pdfaccessguru-1.0.0.dist-info/RECORD +294 -0
  292. pdfaccessguru-1.0.0.dist-info/WHEEL +4 -0
  293. pdfaccessguru-1.0.0.dist-info/entry_points.txt +5 -0
  294. pdfaccessguru-1.0.0.dist-info/licenses/LICENSE +661 -0
File without changes
@@ -0,0 +1,219 @@
1
+ """
2
+ One connection to a language model, for three kinds of service (Phase 4):
3
+
4
+ - OpenRouter (cloud, key: OPENROUTER_API_KEY)
5
+ - OpenAI (cloud, key: OPENAI_API_KEY)
6
+ - Ollama (runs on this computer, nothing leaves it; https://ollama.com)
7
+
8
+ All three speak the same "chat completions" format, so one client serves them. Which one is
9
+ used is set with PDFACCESSGURU_AI (openrouter, openai, ollama, none or auto). With "auto" (the
10
+ default) a configured key is used, otherwise a running Ollama, otherwise no AI.
11
+ PDFACCESSGURU_PRIVACY=local allows only Ollama, so no document content is ever sent away.
12
+
13
+ Without an AI every task falls back to simple rules (see LLM/fallback.py) and the report says so.
14
+ """
15
+
16
+ from __future__ import annotations
17
+
18
+ import base64
19
+ import os
20
+ from collections import Counter
21
+ from dataclasses import dataclass, field
22
+
23
+ import requests
24
+
25
+ from pdfaccessguru.exceptions.llm_exceptions.open_router_connection_failed_exception import (
26
+ OpenRouterConnectionFailedException,
27
+ )
28
+ from pdfaccessguru.utils.environment_variables import load_environment_variables
29
+
30
+ PROVIDERS = ("openrouter", "openai", "ollama")
31
+ _CLOUD_URLS = {
32
+ "openrouter": "https://openrouter.ai/api/v1/chat/completions",
33
+ "openai": "https://api.openai.com/v1/chat/completions",
34
+ }
35
+ _DEFAULT_MODELS = {
36
+ "openrouter": ("google/gemma-4-26b-a4b-it", "google/gemma-4-26b-a4b-it"),
37
+ "openai": ("gpt-4.1-mini", "gpt-4.1-mini"),
38
+ }
39
+ # Ollama models that can read pictures, best first; text tasks use the first installed model.
40
+ _OLLAMA_VISION = ("gemma3", "qwen2.5vl", "llama3.2-vision", "llava", "minicpm-v")
41
+ _OLLAMA_TEXT = ("gemma3", "qwen2.5", "llama3", "mistral")
42
+
43
+
44
+ class LLMNotConfigured(OpenRouterConnectionFailedException):
45
+ """No language model is set up (or none that can do this task). Use the fallback."""
46
+
47
+
48
+ @dataclass(frozen=True)
49
+ class LLMSettings:
50
+ provider: str # one of PROVIDERS, or "none"
51
+ url: str = ""
52
+ api_key: str = field(default="", repr=False) # never shown in logs or messages
53
+ text_model: str = ""
54
+ vision_model: str = "" # "" if the service has no model that reads pictures
55
+ reason: str = "" # why no AI is used (plain language)
56
+
57
+ @property
58
+ def available(self) -> bool:
59
+ return self.provider in PROVIDERS
60
+
61
+ @property
62
+ def local(self) -> bool:
63
+ return self.provider == "ollama"
64
+
65
+ def describe(self, language: str = "en") -> str:
66
+ """Plain-language description for reports, the doctor command and the app (English or German)."""
67
+ german = language == "de"
68
+ if not self.available:
69
+ return f"kein KI-Modell ({_reason_de(self.reason)})" if german else f"no AI model ({self.reason})"
70
+ name = {"openrouter": "OpenRouter", "openai": "OpenAI", "ollama": "Ollama"}[self.provider]
71
+ if german:
72
+ return f"{name} {'auf diesem Computer' if self.local else 'in der Cloud'}, Modell {self.text_model}"
73
+ return f"{name} {'on this computer' if self.local else 'in the cloud'}, model {self.text_model}"
74
+
75
+
76
+ _REASONS_DE = {
77
+ "Ollama is not running or has no model installed": "Ollama läuft nicht oder hat kein Modell installiert",
78
+ "AI is switched off": "KI ist ausgeschaltet",
79
+ "no AI key is set and Ollama is not running": "kein KI-Schlüssel eingerichtet und Ollama läuft nicht",
80
+ "no OpenRouter key is set": "kein OpenRouter-Schlüssel eingerichtet",
81
+ "no OpenAI key is set": "kein OpenAI-Schlüssel eingerichtet",
82
+ }
83
+
84
+
85
+ def _reason_de(reason: str) -> str:
86
+ if reason in _REASONS_DE:
87
+ return _REASONS_DE[reason]
88
+ if reason.startswith("privacy mode 'local' does not allow "):
89
+ return f"der Datenschutzmodus „lokal“ erlaubt {reason.rsplit(' ', 1)[-1]} nicht"
90
+ return reason
91
+
92
+
93
+ def _ollama_url() -> str:
94
+ return os.environ.get("OLLAMA_HOST", "http://localhost:11434").rstrip("/")
95
+
96
+
97
+ def _ollama_models(url: str) -> list[str]:
98
+ try:
99
+ response = requests.get(f"{url}/api/tags", timeout=1.5)
100
+ response.raise_for_status()
101
+ return [model["name"] for model in response.json().get("models", [])]
102
+ except (requests.RequestException, ValueError, KeyError):
103
+ return []
104
+
105
+
106
+ def _pick(installed: list[str], preferred: tuple[str, ...]) -> str:
107
+ for family in preferred:
108
+ for name in installed:
109
+ if name.split(":")[0] == family:
110
+ return name
111
+ return ""
112
+
113
+
114
+ def _ollama_settings() -> LLMSettings:
115
+ url = _ollama_url()
116
+ installed = _ollama_models(url)
117
+ if not installed:
118
+ return LLMSettings("none", reason="Ollama is not running or has no model installed")
119
+ vision = os.environ.get("PDFACCESSGURU_VISION_MODEL") or _pick(installed, _OLLAMA_VISION)
120
+ text = os.environ.get("PDFACCESSGURU_TEXT_MODEL") or _pick(installed, _OLLAMA_TEXT) or installed[0]
121
+ return LLMSettings("ollama", f"{url}/v1/chat/completions", "ollama", text, vision)
122
+
123
+
124
+ def _cloud_settings(provider: str) -> LLMSettings:
125
+ key = os.environ.get("OPENROUTER_API_KEY" if provider == "openrouter" else "OPENAI_API_KEY", "")
126
+ if not key:
127
+ return LLMSettings("none", reason=f"no {'OpenRouter' if provider == 'openrouter' else 'OpenAI'} key is set")
128
+ text, vision = _DEFAULT_MODELS[provider]
129
+ if provider == "openrouter": # the names used before Phase 4 still work
130
+ text = os.environ.get("DEFAULT_MODEL", text)
131
+ vision = os.environ.get("DEFAULT_IMAGE_MODEL", vision)
132
+ text = os.environ.get("PDFACCESSGURU_TEXT_MODEL", text)
133
+ vision = os.environ.get("PDFACCESSGURU_VISION_MODEL", vision)
134
+ return LLMSettings(provider, _CLOUD_URLS[provider], key, text, vision)
135
+
136
+
137
+ def local_model() -> str:
138
+ """The Ollama model this computer would use (pictures first), or "" if Ollama has none. For the app."""
139
+ installed = _ollama_models(_ollama_url())
140
+ return _pick(installed, _OLLAMA_VISION) or _pick(installed, _OLLAMA_TEXT) or (installed[0] if installed else "")
141
+
142
+
143
+ def online_key_service() -> str:
144
+ """The online AI service a key is set up for ("OpenRouter", "OpenAI"), or "". Never returns the key."""
145
+ load_environment_variables()
146
+ if os.environ.get("OPENROUTER_API_KEY"):
147
+ return "OpenRouter"
148
+ return "OpenAI" if os.environ.get("OPENAI_API_KEY") else ""
149
+
150
+
151
+ _settings: LLMSettings | None = None
152
+ usage: Counter = Counter() # requests per kind ("text", "picture") in this run
153
+
154
+
155
+ def settings(refresh: bool = False) -> LLMSettings:
156
+ """The language model to use, from PDFACCESSGURU_AI, PDFACCESSGURU_PRIVACY and the keys."""
157
+ global _settings
158
+ if _settings is not None and not refresh:
159
+ return _settings
160
+ load_environment_variables()
161
+ choice = os.environ.get("PDFACCESSGURU_AI", "auto").strip().lower() or "auto"
162
+ local_only = os.environ.get("PDFACCESSGURU_PRIVACY", "").strip().lower() == "local"
163
+ if choice == "none":
164
+ result = LLMSettings("none", reason="AI is switched off")
165
+ elif local_only and choice not in ("auto", "ollama"):
166
+ result = LLMSettings("none", reason=f"privacy mode 'local' does not allow {choice}")
167
+ elif choice == "ollama" or (local_only and choice == "auto"):
168
+ result = _ollama_settings()
169
+ elif choice in ("openrouter", "openai"):
170
+ result = _cloud_settings(choice)
171
+ else: # auto: a key, else a running Ollama
172
+ result = next((s for s in (_cloud_settings("openrouter"), _cloud_settings("openai")) if s.available), None) \
173
+ or _ollama_settings()
174
+ if not result.available:
175
+ result = LLMSettings("none", reason="no AI key is set and Ollama is not running")
176
+ _settings = result
177
+ return result
178
+
179
+
180
+ class LLMClient:
181
+ """
182
+ Sends prompts to the configured model. Both methods return the answer text and raise
183
+ LLMNotConfigured if no (suitable) model is set up, or OpenRouterConnectionFailedException
184
+ if the request fails. (The class keeps the name OpenRouterBaseProvider as an alias.)
185
+ """
186
+
187
+ def __init__(self, config: LLMSettings | None = None):
188
+ self.config = config or settings()
189
+
190
+ def _post(self, model: str, messages: list, json_mode: bool, kind: str) -> str:
191
+ if not self.config.available:
192
+ raise LLMNotConfigured(self.config.reason)
193
+ payload = {"model": model, "messages": messages, "temperature": 0.2}
194
+ if json_mode:
195
+ payload["response_format"] = {"type": "json_object"}
196
+ headers = {"Authorization": f"Bearer {self.config.api_key}", "Content-Type": "application/json"}
197
+ timeout = (5, 600) if self.config.local else (15, 180) # local models run on the processor
198
+ try:
199
+ response = requests.post(self.config.url, headers=headers, json=payload, timeout=timeout)
200
+ response.raise_for_status()
201
+ usage[kind] += 1
202
+ return response.json()["choices"][0]["message"]["content"] or ""
203
+ except (requests.RequestException, ValueError, KeyError, IndexError) as error:
204
+ raise OpenRouterConnectionFailedException(str(error)) from error
205
+
206
+ def send_message(self, system_prompt: str, user_prompt: str, json_mode: bool = False) -> str:
207
+ messages = [{"role": "system", "content": system_prompt}, {"role": "user", "content": user_prompt}]
208
+ return self._post(self.config.text_model, messages, json_mode, "text")
209
+
210
+ def send_message_with_image(self, system_prompt: str, user_prompt: str, image_path, json_mode: bool = False) -> str:
211
+ if self.config.available and not self.config.vision_model:
212
+ raise LLMNotConfigured("the AI model cannot read pictures")
213
+ data_url = "data:image/png;base64," + base64.b64encode(image_path.image_data).decode("ascii")
214
+ messages = [
215
+ {"role": "system", "content": system_prompt},
216
+ {"role": "user", "content": [{"type": "text", "text": user_prompt},
217
+ {"type": "image_url", "image_url": {"url": data_url}}]},
218
+ ]
219
+ return self._post(self.config.vision_model, messages, json_mode, "picture")
@@ -0,0 +1,27 @@
1
+ """
2
+ Configuration for the LLM integration.
3
+
4
+ All values are read from environment variables (or from the local .env file,
5
+ see .env.example). Never write API keys into this file or any other source file.
6
+ """
7
+ import os
8
+
9
+ from pdfaccessguru.utils.environment_variables import load_environment_variables
10
+
11
+ load_environment_variables()
12
+
13
+
14
+ # Personal OpenRouter API key. Empty means "no key configured".
15
+ OPENROUTER_API_KEY = os.environ.get("OPENROUTER_API_KEY", "")
16
+
17
+ # The OpenRouter chat completions endpoint (follows the OpenAI API format).
18
+ OPENROUTER_BASE_URL = "https://openrouter.ai/api/v1/chat/completions"
19
+
20
+ # The model used for normal requests.
21
+ DEFAULT_MODEL = os.environ.get("DEFAULT_MODEL", "google/gemma-4-26b-a4b-it")
22
+
23
+ # The vision-capable model used for prompts that include an image.
24
+ DEFAULT_IMAGE_MODEL = os.environ.get("DEFAULT_IMAGE_MODEL", "google/gemma-4-26b-a4b-it:free")
25
+
26
+ # A more capable model for tasks that need higher accuracy.
27
+ PREMIUM_MODEL = os.environ.get("PREMIUM_MODEL", DEFAULT_MODEL)
@@ -0,0 +1,109 @@
1
+ """
2
+ What the tool does when no AI model is available (Phase 4), and how it tells the user.
3
+
4
+ Every task that can use an AI also works without one. The simple method is used, and the report
5
+ says once per task, in plain words, what was done instead, so the user knows which texts to
6
+ check. Some tasks use the simple method first even with an AI, because it is reliable and fast
7
+ (e.g. the language of long text).
8
+ """
9
+
10
+ from __future__ import annotations
11
+
12
+ from functools import lru_cache
13
+
14
+ from pdfaccessguru.LLM.client import settings
15
+ from pdfaccessguru.pipeline.reporting.writer import report_info, report_warn
16
+
17
+ # What the report says when a task had to do without an AI (one sentence each).
18
+ FALLBACK_NOTES = {
19
+ "language": "The language of some short texts could not be recognised without an AI model, "
20
+ "so the language of the whole document was used for them.",
21
+ "alt_text": "Picture descriptions were written from the text in and around each picture, "
22
+ "without an AI model. Please review them.",
23
+ "link_text": "Link descriptions were made from each link's text or address, without an AI model.",
24
+ "title": "The document title was taken from its first heading or first line, without an AI model.",
25
+ "unicode": "Some characters could not be identified without an AI model; they are listed as still open.",
26
+ "form_field": "Form field descriptions were made from the field names, without an AI model. Please review them.",
27
+ "formula": "Formula descriptions were made from the text of each formula, without an AI model. Please review them.",
28
+ "table": "Some table rows could not be worked out without an AI model; they are listed as still open.",
29
+ }
30
+
31
+ _noted: set[str] = set()
32
+
33
+
34
+ def ai_available() -> bool:
35
+ return settings().available
36
+
37
+
38
+ def start_run() -> None:
39
+ """Called at the start of each run: report which AI is used, and forget earlier notes."""
40
+ _noted.clear()
41
+ config = settings(refresh=True)
42
+ if config.available:
43
+ report_info(f"Texts such as picture descriptions are written by: {config.describe()}.")
44
+ else:
45
+ report_warn(
46
+ f"No AI model is used ({config.reason}). Picture descriptions, link texts and similar "
47
+ "texts are written with simple rules instead, which can be less helpful. To use an AI, "
48
+ "install Ollama (a free AI that runs on this computer), or add a key for an online AI like "
49
+ "ChatGPT (OpenAI) or OpenRouter; "
50
+ "'pdfaccessguru doctor' shows how.")
51
+
52
+
53
+ def note_fallback(task: str) -> None:
54
+ """Say once per run that a task was done without an AI."""
55
+ if task in _noted:
56
+ return
57
+ _noted.add(task)
58
+ report_info("Without AI: " + FALLBACK_NOTES.get(task, f"{task} was done with simple rules."))
59
+
60
+
61
+ @lru_cache(maxsize=1)
62
+ def _language_identifier():
63
+ from py3langid.langid import MODEL_FILE, LanguageIdentifier
64
+ return LanguageIdentifier.from_model_file(MODEL_FILE, norm_probs=True)
65
+
66
+
67
+ def detect_language(text: str | None, min_confidence: float = 0.9) -> str | None:
68
+ """
69
+ The language of a text as a language tag ("de", "en", ...), or None if the text is too
70
+ short or the result is not clear enough. Works without an AI (word and letter statistics).
71
+ """
72
+ if not text:
73
+ return None
74
+ letters = sum(char.isalpha() for char in text)
75
+ if letters < 12:
76
+ return None
77
+ language, confidence = _language_identifier().classify(text[:5000])
78
+ return language if confidence >= min_confidence else None
79
+
80
+
81
+ # German versions of the notes above, for German reports (Phase 7a).
82
+ _NOTES_DE = {
83
+ "language": "Die Sprache einiger kurzer Texte ließ sich ohne KI-Modell nicht erkennen; für sie wurde die "
84
+ "Sprache des ganzen Dokuments verwendet.",
85
+ "alt_text": "Bildbeschreibungen wurden ohne KI-Modell aus dem Text in und um jedes Bild erstellt. Bitte sehen Sie sie durch.",
86
+ "link_text": "Linkbeschreibungen wurden ohne KI-Modell aus dem Text oder der Adresse jedes Links erstellt.",
87
+ "title": "Der Dokumenttitel wurde ohne KI-Modell aus der ersten Überschrift oder ersten Zeile übernommen.",
88
+ "unicode": "Einige Zeichen ließen sich ohne KI-Modell nicht bestimmen; sie stehen bei den offenen Problemen.",
89
+ "form_field": "Beschreibungen von Formularfeldern wurden ohne KI-Modell aus den Feldnamen erstellt. Bitte sehen Sie sie durch.",
90
+ "formula": "Formelbeschreibungen wurden ohne KI-Modell aus dem Text jeder Formel erstellt. Bitte sehen Sie sie durch.",
91
+ "table": "Einige Tabellenzeilen ließen sich ohne KI-Modell nicht bestimmen; sie stehen bei den offenen Problemen.",
92
+ }
93
+
94
+
95
+ def translate_note(message: str) -> str:
96
+ """The German version of a note or status line written by this module (else unchanged)."""
97
+ for key, english in FALLBACK_NOTES.items():
98
+ if message == english:
99
+ return _NOTES_DE[key]
100
+ if message.startswith("Texts such as picture descriptions are written by: "):
101
+ who = message[len("Texts such as picture descriptions are written by: "):].rstrip(".")
102
+ who = who.replace("on this computer", "auf diesem Computer").replace("in the cloud", "in der Cloud").replace("model", "Modell")
103
+ return f"Texte wie Bildbeschreibungen schreibt: {who}."
104
+ if message.startswith("No AI model is used"):
105
+ return ("Es wird kein KI-Modell verwendet. Bildbeschreibungen, Linktexte und ähnliche Texte werden "
106
+ "stattdessen mit einfachen Regeln erstellt, die weniger hilfreich sein können. Für eine KI "
107
+ "installieren Sie Ollama (eine kostenlose KI, die auf diesem Computer läuft) oder hinterlegen "
108
+ "einen Schlüssel für eine Online-KI wie ChatGPT (OpenAI) oder OpenRouter; 'pdfaccessguru doctor' zeigt, wie.")
109
+ return message
@@ -0,0 +1,11 @@
1
+ """
2
+ Package that hosts a collection of material for use in prompts.
3
+ Material are additional natural language based files, used to enhance prompts.
4
+
5
+ List of all material files:
6
+
7
+ - available_locales.json contains a list of all available locales,
8
+ source: https://github.com/unicode-org/cldr-json/tree/main/cldr-json/cldr-core
9
+
10
+
11
+ """