classifyre-cli 0.4.55__tar.gz → 0.4.56__tar.gz

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 (249) hide show
  1. {classifyre_cli-0.4.55 → classifyre_cli-0.4.56}/.turbo/turbo-build.log +1 -1
  2. {classifyre_cli-0.4.55 → classifyre_cli-0.4.56}/PKG-INFO +1 -1
  3. {classifyre_cli-0.4.55 → classifyre_cli-0.4.56}/package.json +1 -1
  4. {classifyre_cli-0.4.55 → classifyre_cli-0.4.56}/pyproject.toml +1 -3
  5. {classifyre_cli-0.4.55 → classifyre_cli-0.4.56}/src/detectors/custom/runners/__init__.py +0 -4
  6. {classifyre_cli-0.4.55 → classifyre_cli-0.4.56}/src/detectors/custom/runners/_factory.py +0 -5
  7. {classifyre_cli-0.4.55 → classifyre_cli-0.4.56}/src/main.py +5 -17
  8. {classifyre_cli-0.4.55 → classifyre_cli-0.4.56}/src/models/generated_detectors.py +0 -73
  9. {classifyre_cli-0.4.55 → classifyre_cli-0.4.56}/src/models/generated_single_asset_scan_results.py +17 -0
  10. {classifyre_cli-0.4.55 → classifyre_cli-0.4.56}/src/outputs/rest.py +10 -0
  11. classifyre_cli-0.4.56/src/pipeline/__init__.py +25 -0
  12. {classifyre_cli-0.4.55 → classifyre_cli-0.4.56}/src/pipeline/detector_pipeline.py +42 -11
  13. classifyre_cli-0.4.56/src/pipeline/text_artifact.py +54 -0
  14. {classifyre_cli-0.4.55 → classifyre_cli-0.4.56}/src/pipeline/worker_pool.py +0 -1
  15. {classifyre_cli-0.4.55 → classifyre_cli-0.4.56}/src/utils/file_parser.py +53 -5
  16. {classifyre_cli-0.4.55 → classifyre_cli-0.4.56}/src/utils/video_processing.py +28 -7
  17. {classifyre_cli-0.4.55 → classifyre_cli-0.4.56}/tests/detectors/custom/test_transformer_runners.py +1 -69
  18. {classifyre_cli-0.4.55 → classifyre_cli-0.4.56}/tests/detectors/test_custom_detector_examples_runtime.py +0 -2
  19. {classifyre_cli-0.4.55 → classifyre_cli-0.4.56}/tests/detectors/test_detector_catalog_commercial.py +1 -3
  20. {classifyre_cli-0.4.55 → classifyre_cli-0.4.56}/tests/pipeline/test_detector_outcomes.py +45 -0
  21. {classifyre_cli-0.4.55 → classifyre_cli-0.4.56}/tests/pipeline/test_detector_pipeline.py +3 -2
  22. {classifyre_cli-0.4.55 → classifyre_cli-0.4.56}/tests/utils/test_file_parser.py +21 -0
  23. {classifyre_cli-0.4.55 → classifyre_cli-0.4.56}/tests/utils/test_video_processing.py +45 -0
  24. {classifyre_cli-0.4.55 → classifyre_cli-0.4.56}/uv.lock +41 -47
  25. classifyre_cli-0.4.55/src/detectors/custom/runners/_feature_extraction.py +0 -165
  26. classifyre_cli-0.4.55/src/pipeline/__init__.py +0 -7
  27. {classifyre_cli-0.4.55 → classifyre_cli-0.4.56}/.gitignore +0 -0
  28. {classifyre_cli-0.4.55 → classifyre_cli-0.4.56}/.python-version +0 -0
  29. {classifyre_cli-0.4.55 → classifyre_cli-0.4.56}/README.md +0 -0
  30. {classifyre_cli-0.4.55 → classifyre_cli-0.4.56}/main.py +0 -0
  31. {classifyre_cli-0.4.55 → classifyre_cli-0.4.56}/scripts/generate_models.py +0 -0
  32. {classifyre_cli-0.4.55 → classifyre_cli-0.4.56}/src/__init__.py +0 -0
  33. {classifyre_cli-0.4.55 → classifyre_cli-0.4.56}/src/config.py +0 -0
  34. {classifyre_cli-0.4.55 → classifyre_cli-0.4.56}/src/detectors/__init__.py +0 -0
  35. {classifyre_cli-0.4.55 → classifyre_cli-0.4.56}/src/detectors/base.py +0 -0
  36. {classifyre_cli-0.4.55 → classifyre_cli-0.4.56}/src/detectors/broken_links/__init__.py +0 -0
  37. {classifyre_cli-0.4.55 → classifyre_cli-0.4.56}/src/detectors/broken_links/detector.py +0 -0
  38. {classifyre_cli-0.4.55 → classifyre_cli-0.4.56}/src/detectors/config.py +0 -0
  39. {classifyre_cli-0.4.55 → classifyre_cli-0.4.56}/src/detectors/content/__init__.py +0 -0
  40. {classifyre_cli-0.4.55 → classifyre_cli-0.4.56}/src/detectors/custom/__init__.py +0 -0
  41. {classifyre_cli-0.4.55 → classifyre_cli-0.4.56}/src/detectors/custom/detector.py +0 -0
  42. {classifyre_cli-0.4.55 → classifyre_cli-0.4.56}/src/detectors/custom/extractor.py +0 -0
  43. {classifyre_cli-0.4.55 → classifyre_cli-0.4.56}/src/detectors/custom/runners/_base.py +0 -0
  44. {classifyre_cli-0.4.55 → classifyre_cli-0.4.56}/src/detectors/custom/runners/_gliner2.py +0 -0
  45. {classifyre_cli-0.4.55 → classifyre_cli-0.4.56}/src/detectors/custom/runners/_image_classification.py +0 -0
  46. {classifyre_cli-0.4.55 → classifyre_cli-0.4.56}/src/detectors/custom/runners/_llm.py +0 -0
  47. {classifyre_cli-0.4.55 → classifyre_cli-0.4.56}/src/detectors/custom/runners/_object_detection.py +0 -0
  48. {classifyre_cli-0.4.55 → classifyre_cli-0.4.56}/src/detectors/custom/runners/_regex.py +0 -0
  49. {classifyre_cli-0.4.55 → classifyre_cli-0.4.56}/src/detectors/custom/runners/_text_classification.py +0 -0
  50. {classifyre_cli-0.4.55 → classifyre_cli-0.4.56}/src/detectors/custom/trainer.py +0 -0
  51. {classifyre_cli-0.4.55 → classifyre_cli-0.4.56}/src/detectors/dependencies.py +0 -0
  52. {classifyre_cli-0.4.55 → classifyre_cli-0.4.56}/src/detectors/pii/__init__.py +0 -0
  53. {classifyre_cli-0.4.55 → classifyre_cli-0.4.56}/src/detectors/pii/detector.py +0 -0
  54. {classifyre_cli-0.4.55 → classifyre_cli-0.4.56}/src/detectors/secrets/__init__.py +0 -0
  55. {classifyre_cli-0.4.55 → classifyre_cli-0.4.56}/src/detectors/secrets/detector.py +0 -0
  56. {classifyre_cli-0.4.55 → classifyre_cli-0.4.56}/src/detectors/threat/__init__.py +0 -0
  57. {classifyre_cli-0.4.55 → classifyre_cli-0.4.56}/src/detectors/threat/code_security_detector.py +0 -0
  58. {classifyre_cli-0.4.55 → classifyre_cli-0.4.56}/src/detectors/threat/yara_detector.py +0 -0
  59. {classifyre_cli-0.4.55 → classifyre_cli-0.4.56}/src/models/generated_input.py +0 -0
  60. {classifyre_cli-0.4.55 → classifyre_cli-0.4.56}/src/outputs/__init__.py +0 -0
  61. {classifyre_cli-0.4.55 → classifyre_cli-0.4.56}/src/outputs/base.py +0 -0
  62. {classifyre_cli-0.4.55 → classifyre_cli-0.4.56}/src/outputs/console.py +0 -0
  63. {classifyre_cli-0.4.55 → classifyre_cli-0.4.56}/src/outputs/factory.py +0 -0
  64. {classifyre_cli-0.4.55 → classifyre_cli-0.4.56}/src/outputs/file.py +0 -0
  65. {classifyre_cli-0.4.55 → classifyre_cli-0.4.56}/src/pipeline/content_provider.py +0 -0
  66. {classifyre_cli-0.4.55 → classifyre_cli-0.4.56}/src/pipeline/parsed_content_provider.py +0 -0
  67. {classifyre_cli-0.4.55 → classifyre_cli-0.4.56}/src/sandbox/__init__.py +0 -0
  68. {classifyre_cli-0.4.55 → classifyre_cli-0.4.56}/src/sandbox/runner.py +0 -0
  69. {classifyre_cli-0.4.55 → classifyre_cli-0.4.56}/src/sources/__init__.py +0 -0
  70. {classifyre_cli-0.4.55 → classifyre_cli-0.4.56}/src/sources/asset_metadata.py +0 -0
  71. {classifyre_cli-0.4.55 → classifyre_cli-0.4.56}/src/sources/atlassian_common.py +0 -0
  72. {classifyre_cli-0.4.55 → classifyre_cli-0.4.56}/src/sources/azure_blob_storage/__init__.py +0 -0
  73. {classifyre_cli-0.4.55 → classifyre_cli-0.4.56}/src/sources/azure_blob_storage/source.py +0 -0
  74. {classifyre_cli-0.4.55 → classifyre_cli-0.4.56}/src/sources/base.py +0 -0
  75. {classifyre_cli-0.4.55 → classifyre_cli-0.4.56}/src/sources/confluence/__init__.py +0 -0
  76. {classifyre_cli-0.4.55 → classifyre_cli-0.4.56}/src/sources/confluence/source.py +0 -0
  77. {classifyre_cli-0.4.55 → classifyre_cli-0.4.56}/src/sources/databricks/__init__.py +0 -0
  78. {classifyre_cli-0.4.55 → classifyre_cli-0.4.56}/src/sources/databricks/source.py +0 -0
  79. {classifyre_cli-0.4.55 → classifyre_cli-0.4.56}/src/sources/delta_lake/__init__.py +0 -0
  80. {classifyre_cli-0.4.55 → classifyre_cli-0.4.56}/src/sources/delta_lake/source.py +0 -0
  81. {classifyre_cli-0.4.55 → classifyre_cli-0.4.56}/src/sources/dependencies.py +0 -0
  82. {classifyre_cli-0.4.55 → classifyre_cli-0.4.56}/src/sources/elasticsearch/__init__.py +0 -0
  83. {classifyre_cli-0.4.55 → classifyre_cli-0.4.56}/src/sources/elasticsearch/source.py +0 -0
  84. {classifyre_cli-0.4.55 → classifyre_cli-0.4.56}/src/sources/email/__init__.py +0 -0
  85. {classifyre_cli-0.4.55 → classifyre_cli-0.4.56}/src/sources/email/source.py +0 -0
  86. {classifyre_cli-0.4.55 → classifyre_cli-0.4.56}/src/sources/google_cloud_storage/__init__.py +0 -0
  87. {classifyre_cli-0.4.55 → classifyre_cli-0.4.56}/src/sources/google_cloud_storage/source.py +0 -0
  88. {classifyre_cli-0.4.55 → classifyre_cli-0.4.56}/src/sources/google_workspace/__init__.py +0 -0
  89. {classifyre_cli-0.4.55 → classifyre_cli-0.4.56}/src/sources/google_workspace/source.py +0 -0
  90. {classifyre_cli-0.4.55 → classifyre_cli-0.4.56}/src/sources/hive/__init__.py +0 -0
  91. {classifyre_cli-0.4.55 → classifyre_cli-0.4.56}/src/sources/hive/source.py +0 -0
  92. {classifyre_cli-0.4.55 → classifyre_cli-0.4.56}/src/sources/iceberg/__init__.py +0 -0
  93. {classifyre_cli-0.4.55 → classifyre_cli-0.4.56}/src/sources/iceberg/source.py +0 -0
  94. {classifyre_cli-0.4.55 → classifyre_cli-0.4.56}/src/sources/jira/__init__.py +0 -0
  95. {classifyre_cli-0.4.55 → classifyre_cli-0.4.56}/src/sources/jira/source.py +0 -0
  96. {classifyre_cli-0.4.55 → classifyre_cli-0.4.56}/src/sources/kafka/__init__.py +0 -0
  97. {classifyre_cli-0.4.55 → classifyre_cli-0.4.56}/src/sources/kafka/source.py +0 -0
  98. {classifyre_cli-0.4.55 → classifyre_cli-0.4.56}/src/sources/lakehouse_base.py +0 -0
  99. {classifyre_cli-0.4.55 → classifyre_cli-0.4.56}/src/sources/local_folder/__init__.py +0 -0
  100. {classifyre_cli-0.4.55 → classifyre_cli-0.4.56}/src/sources/local_folder/source.py +0 -0
  101. {classifyre_cli-0.4.55 → classifyre_cli-0.4.56}/src/sources/meilisearch/__init__.py +0 -0
  102. {classifyre_cli-0.4.55 → classifyre_cli-0.4.56}/src/sources/meilisearch/source.py +0 -0
  103. {classifyre_cli-0.4.55 → classifyre_cli-0.4.56}/src/sources/microsoft_365/__init__.py +0 -0
  104. {classifyre_cli-0.4.55 → classifyre_cli-0.4.56}/src/sources/microsoft_365/source.py +0 -0
  105. {classifyre_cli-0.4.55 → classifyre_cli-0.4.56}/src/sources/mongodb/__init__.py +0 -0
  106. {classifyre_cli-0.4.55 → classifyre_cli-0.4.56}/src/sources/mongodb/source.py +0 -0
  107. {classifyre_cli-0.4.55 → classifyre_cli-0.4.56}/src/sources/mssql/__init__.py +0 -0
  108. {classifyre_cli-0.4.55 → classifyre_cli-0.4.56}/src/sources/mssql/source.py +0 -0
  109. {classifyre_cli-0.4.55 → classifyre_cli-0.4.56}/src/sources/mysql/__init__.py +0 -0
  110. {classifyre_cli-0.4.55 → classifyre_cli-0.4.56}/src/sources/mysql/source.py +0 -0
  111. {classifyre_cli-0.4.55 → classifyre_cli-0.4.56}/src/sources/neo4j/__init__.py +0 -0
  112. {classifyre_cli-0.4.55 → classifyre_cli-0.4.56}/src/sources/neo4j/source.py +0 -0
  113. {classifyre_cli-0.4.55 → classifyre_cli-0.4.56}/src/sources/notion/__init__.py +0 -0
  114. {classifyre_cli-0.4.55 → classifyre_cli-0.4.56}/src/sources/notion/client.py +0 -0
  115. {classifyre_cli-0.4.55 → classifyre_cli-0.4.56}/src/sources/notion/source.py +0 -0
  116. {classifyre_cli-0.4.55 → classifyre_cli-0.4.56}/src/sources/object_storage/base.py +0 -0
  117. {classifyre_cli-0.4.55 → classifyre_cli-0.4.56}/src/sources/opensearch/__init__.py +0 -0
  118. {classifyre_cli-0.4.55 → classifyre_cli-0.4.56}/src/sources/opensearch/source.py +0 -0
  119. {classifyre_cli-0.4.55 → classifyre_cli-0.4.56}/src/sources/oracle/__init__.py +0 -0
  120. {classifyre_cli-0.4.55 → classifyre_cli-0.4.56}/src/sources/oracle/source.py +0 -0
  121. {classifyre_cli-0.4.55 → classifyre_cli-0.4.56}/src/sources/postgresql/__init__.py +0 -0
  122. {classifyre_cli-0.4.55 → classifyre_cli-0.4.56}/src/sources/postgresql/source.py +0 -0
  123. {classifyre_cli-0.4.55 → classifyre_cli-0.4.56}/src/sources/powerbi/__init__.py +0 -0
  124. {classifyre_cli-0.4.55 → classifyre_cli-0.4.56}/src/sources/powerbi/source.py +0 -0
  125. {classifyre_cli-0.4.55 → classifyre_cli-0.4.56}/src/sources/recipe_normalizer.py +0 -0
  126. {classifyre_cli-0.4.55 → classifyre_cli-0.4.56}/src/sources/s3_client.py +0 -0
  127. {classifyre_cli-0.4.55 → classifyre_cli-0.4.56}/src/sources/s3_compatible_storage/README.md +0 -0
  128. {classifyre_cli-0.4.55 → classifyre_cli-0.4.56}/src/sources/s3_compatible_storage/__init__.py +0 -0
  129. {classifyre_cli-0.4.55 → classifyre_cli-0.4.56}/src/sources/s3_compatible_storage/source.py +0 -0
  130. {classifyre_cli-0.4.55 → classifyre_cli-0.4.56}/src/sources/search_engine_base.py +0 -0
  131. {classifyre_cli-0.4.55 → classifyre_cli-0.4.56}/src/sources/servicedesk/__init__.py +0 -0
  132. {classifyre_cli-0.4.55 → classifyre_cli-0.4.56}/src/sources/servicedesk/source.py +0 -0
  133. {classifyre_cli-0.4.55 → classifyre_cli-0.4.56}/src/sources/slack/__init__.py +0 -0
  134. {classifyre_cli-0.4.55 → classifyre_cli-0.4.56}/src/sources/slack/source.py +0 -0
  135. {classifyre_cli-0.4.55 → classifyre_cli-0.4.56}/src/sources/snowflake/__init__.py +0 -0
  136. {classifyre_cli-0.4.55 → classifyre_cli-0.4.56}/src/sources/snowflake/source.py +0 -0
  137. {classifyre_cli-0.4.55 → classifyre_cli-0.4.56}/src/sources/sqlite/__init__.py +0 -0
  138. {classifyre_cli-0.4.55 → classifyre_cli-0.4.56}/src/sources/sqlite/source.py +0 -0
  139. {classifyre_cli-0.4.55 → classifyre_cli-0.4.56}/src/sources/tableau/__init__.py +0 -0
  140. {classifyre_cli-0.4.55 → classifyre_cli-0.4.56}/src/sources/tableau/source.py +0 -0
  141. {classifyre_cli-0.4.55 → classifyre_cli-0.4.56}/src/sources/tabular_base.py +0 -0
  142. {classifyre_cli-0.4.55 → classifyre_cli-0.4.56}/src/sources/tabular_utils.py +0 -0
  143. {classifyre_cli-0.4.55 → classifyre_cli-0.4.56}/src/sources/wordpress/__init__.py +0 -0
  144. {classifyre_cli-0.4.55 → classifyre_cli-0.4.56}/src/sources/wordpress/source.py +0 -0
  145. {classifyre_cli-0.4.55 → classifyre_cli-0.4.56}/src/sources/youtube/__init__.py +0 -0
  146. {classifyre_cli-0.4.55 → classifyre_cli-0.4.56}/src/sources/youtube/source.py +0 -0
  147. {classifyre_cli-0.4.55 → classifyre_cli-0.4.56}/src/telemetry.py +0 -0
  148. {classifyre_cli-0.4.55 → classifyre_cli-0.4.56}/src/utils/__init__.py +0 -0
  149. {classifyre_cli-0.4.55 → classifyre_cli-0.4.56}/src/utils/content_extraction.py +0 -0
  150. {classifyre_cli-0.4.55 → classifyre_cli-0.4.56}/src/utils/dependency_groups.py +0 -0
  151. {classifyre_cli-0.4.55 → classifyre_cli-0.4.56}/src/utils/embedded_images.py +0 -0
  152. {classifyre_cli-0.4.55 → classifyre_cli-0.4.56}/src/utils/file_metadata.py +0 -0
  153. {classifyre_cli-0.4.55 → classifyre_cli-0.4.56}/src/utils/file_to_images.py +0 -0
  154. {classifyre_cli-0.4.55 → classifyre_cli-0.4.56}/src/utils/hashing.py +0 -0
  155. {classifyre_cli-0.4.55 → classifyre_cli-0.4.56}/src/utils/resources.py +0 -0
  156. {classifyre_cli-0.4.55 → classifyre_cli-0.4.56}/src/utils/transcription.py +0 -0
  157. {classifyre_cli-0.4.55 → classifyre_cli-0.4.56}/src/utils/uv_sync.py +0 -0
  158. {classifyre_cli-0.4.55 → classifyre_cli-0.4.56}/src/utils/validation.py +0 -0
  159. {classifyre_cli-0.4.55 → classifyre_cli-0.4.56}/tests/__init__.py +0 -0
  160. {classifyre_cli-0.4.55 → classifyre_cli-0.4.56}/tests/_lakehouse_fakes.py +0 -0
  161. {classifyre_cli-0.4.55 → classifyre_cli-0.4.56}/tests/conftest.py +0 -0
  162. {classifyre_cli-0.4.55 → classifyre_cli-0.4.56}/tests/detectors/__init__.py +0 -0
  163. {classifyre_cli-0.4.55 → classifyre_cli-0.4.56}/tests/detectors/broken_links/test_broken_links_detector.py +0 -0
  164. {classifyre_cli-0.4.55 → classifyre_cli-0.4.56}/tests/detectors/conftest.py +0 -0
  165. {classifyre_cli-0.4.55 → classifyre_cli-0.4.56}/tests/detectors/content/__init__.py +0 -0
  166. {classifyre_cli-0.4.55 → classifyre_cli-0.4.56}/tests/detectors/custom/__init__.py +0 -0
  167. {classifyre_cli-0.4.55 → classifyre_cli-0.4.56}/tests/detectors/custom/conftest.py +0 -0
  168. {classifyre_cli-0.4.55 → classifyre_cli-0.4.56}/tests/detectors/custom/test_gliner2_classification.py +0 -0
  169. {classifyre_cli-0.4.55 → classifyre_cli-0.4.56}/tests/detectors/custom/test_invoice_extraction.py +0 -0
  170. {classifyre_cli-0.4.55 → classifyre_cli-0.4.56}/tests/detectors/custom/test_llm_runner.py +0 -0
  171. {classifyre_cli-0.4.55 → classifyre_cli-0.4.56}/tests/detectors/custom/test_pipeline_integration.py +0 -0
  172. {classifyre_cli-0.4.55 → classifyre_cli-0.4.56}/tests/detectors/custom/test_regex_runner.py +0 -0
  173. {classifyre_cli-0.4.55 → classifyre_cli-0.4.56}/tests/detectors/custom/test_runner_hardening.py +0 -0
  174. {classifyre_cli-0.4.55 → classifyre_cli-0.4.56}/tests/detectors/pii/__init__.py +0 -0
  175. {classifyre_cli-0.4.55 → classifyre_cli-0.4.56}/tests/detectors/pii/conftest.py +0 -0
  176. {classifyre_cli-0.4.55 → classifyre_cli-0.4.56}/tests/detectors/pii/sample_invoice.pdf +0 -0
  177. {classifyre_cli-0.4.55 → classifyre_cli-0.4.56}/tests/detectors/pii/test_pii_chunking.py +0 -0
  178. {classifyre_cli-0.4.55 → classifyre_cli-0.4.56}/tests/detectors/pii/test_pii_detector.py +0 -0
  179. {classifyre_cli-0.4.55 → classifyre_cli-0.4.56}/tests/detectors/pii/test_pii_detector_extended.py +0 -0
  180. {classifyre_cli-0.4.55 → classifyre_cli-0.4.56}/tests/detectors/pii/test_pii_severity.py +0 -0
  181. {classifyre_cli-0.4.55 → classifyre_cli-0.4.56}/tests/detectors/secrets/__init__.py +0 -0
  182. {classifyre_cli-0.4.55 → classifyre_cli-0.4.56}/tests/detectors/secrets/test_secrets_detector.py +0 -0
  183. {classifyre_cli-0.4.55 → classifyre_cli-0.4.56}/tests/detectors/secrets/test_secrets_detector_extended.py +0 -0
  184. {classifyre_cli-0.4.55 → classifyre_cli-0.4.56}/tests/detectors/test_base_detector.py +0 -0
  185. {classifyre_cli-0.4.55 → classifyre_cli-0.4.56}/tests/detectors/test_detector_pipeline_types.py +0 -0
  186. {classifyre_cli-0.4.55 → classifyre_cli-0.4.56}/tests/detectors/test_detector_schema_examples.py +0 -0
  187. {classifyre_cli-0.4.55 → classifyre_cli-0.4.56}/tests/detectors/test_detector_types.py +0 -0
  188. {classifyre_cli-0.4.55 → classifyre_cli-0.4.56}/tests/detectors/test_phase2_detectors.py +0 -0
  189. {classifyre_cli-0.4.55 → classifyre_cli-0.4.56}/tests/detectors/test_registry.py +0 -0
  190. {classifyre_cli-0.4.55 → classifyre_cli-0.4.56}/tests/detectors/threat/__init__.py +0 -0
  191. {classifyre_cli-0.4.55 → classifyre_cli-0.4.56}/tests/detectors/threat/test_code_security_detector.py +0 -0
  192. {classifyre_cli-0.4.55 → classifyre_cli-0.4.56}/tests/detectors/threat/test_yara_detector.py +0 -0
  193. {classifyre_cli-0.4.55 → classifyre_cli-0.4.56}/tests/integration/test_wordpress_broken_links_detector.py +0 -0
  194. {classifyre_cli-0.4.55 → classifyre_cli-0.4.56}/tests/integration/test_wordpress_links_assets.py +0 -0
  195. {classifyre_cli-0.4.55 → classifyre_cli-0.4.56}/tests/pipeline/test_worker_pool.py +0 -0
  196. {classifyre_cli-0.4.55 → classifyre_cli-0.4.56}/tests/test_assets_metadata_catalog.py +0 -0
  197. {classifyre_cli-0.4.55 → classifyre_cli-0.4.56}/tests/test_azure_blob_storage_source.py +0 -0
  198. {classifyre_cli-0.4.55 → classifyre_cli-0.4.56}/tests/test_base_source_attachment.py +0 -0
  199. {classifyre_cli-0.4.55 → classifyre_cli-0.4.56}/tests/test_base_source_sampling.py +0 -0
  200. {classifyre_cli-0.4.55 → classifyre_cli-0.4.56}/tests/test_config.py +0 -0
  201. {classifyre_cli-0.4.55 → classifyre_cli-0.4.56}/tests/test_confluence_source.py +0 -0
  202. {classifyre_cli-0.4.55 → classifyre_cli-0.4.56}/tests/test_custom_extractor.py +0 -0
  203. {classifyre_cli-0.4.55 → classifyre_cli-0.4.56}/tests/test_databricks_source.py +0 -0
  204. {classifyre_cli-0.4.55 → classifyre_cli-0.4.56}/tests/test_delta_lake_source.py +0 -0
  205. {classifyre_cli-0.4.55 → classifyre_cli-0.4.56}/tests/test_dependency_groups.py +0 -0
  206. {classifyre_cli-0.4.55 → classifyre_cli-0.4.56}/tests/test_elasticsearch_source.py +0 -0
  207. {classifyre_cli-0.4.55 → classifyre_cli-0.4.56}/tests/test_email_source.py +0 -0
  208. {classifyre_cli-0.4.55 → classifyre_cli-0.4.56}/tests/test_google_cloud_storage_source.py +0 -0
  209. {classifyre_cli-0.4.55 → classifyre_cli-0.4.56}/tests/test_google_workspace_source.py +0 -0
  210. {classifyre_cli-0.4.55 → classifyre_cli-0.4.56}/tests/test_hashing.py +0 -0
  211. {classifyre_cli-0.4.55 → classifyre_cli-0.4.56}/tests/test_hive_source.py +0 -0
  212. {classifyre_cli-0.4.55 → classifyre_cli-0.4.56}/tests/test_iceberg_source.py +0 -0
  213. {classifyre_cli-0.4.55 → classifyre_cli-0.4.56}/tests/test_jira_source.py +0 -0
  214. {classifyre_cli-0.4.55 → classifyre_cli-0.4.56}/tests/test_kafka_source.py +0 -0
  215. {classifyre_cli-0.4.55 → classifyre_cli-0.4.56}/tests/test_local_folder_source.py +0 -0
  216. {classifyre_cli-0.4.55 → classifyre_cli-0.4.56}/tests/test_meilisearch_source.py +0 -0
  217. {classifyre_cli-0.4.55 → classifyre_cli-0.4.56}/tests/test_microsoft_365_source.py +0 -0
  218. {classifyre_cli-0.4.55 → classifyre_cli-0.4.56}/tests/test_mongodb_source.py +0 -0
  219. {classifyre_cli-0.4.55 → classifyre_cli-0.4.56}/tests/test_mssql_source.py +0 -0
  220. {classifyre_cli-0.4.55 → classifyre_cli-0.4.56}/tests/test_mysql_source.py +0 -0
  221. {classifyre_cli-0.4.55 → classifyre_cli-0.4.56}/tests/test_neo4j_source.py +0 -0
  222. {classifyre_cli-0.4.55 → classifyre_cli-0.4.56}/tests/test_notion_source.py +0 -0
  223. {classifyre_cli-0.4.55 → classifyre_cli-0.4.56}/tests/test_opensearch_source.py +0 -0
  224. {classifyre_cli-0.4.55 → classifyre_cli-0.4.56}/tests/test_oracle_source.py +0 -0
  225. {classifyre_cli-0.4.55 → classifyre_cli-0.4.56}/tests/test_outputs.py +0 -0
  226. {classifyre_cli-0.4.55 → classifyre_cli-0.4.56}/tests/test_postgresql_source.py +0 -0
  227. {classifyre_cli-0.4.55 → classifyre_cli-0.4.56}/tests/test_powerbi_source.py +0 -0
  228. {classifyre_cli-0.4.55 → classifyre_cli-0.4.56}/tests/test_recipe_normalizer.py +0 -0
  229. {classifyre_cli-0.4.55 → classifyre_cli-0.4.56}/tests/test_s3_compatible_storage_source.py +0 -0
  230. {classifyre_cli-0.4.55 → classifyre_cli-0.4.56}/tests/test_sampling_automatic.py +0 -0
  231. {classifyre_cli-0.4.55 → classifyre_cli-0.4.56}/tests/test_sandbox_runner.py +0 -0
  232. {classifyre_cli-0.4.55 → classifyre_cli-0.4.56}/tests/test_servicedesk_source.py +0 -0
  233. {classifyre_cli-0.4.55 → classifyre_cli-0.4.56}/tests/test_slack_source.py +0 -0
  234. {classifyre_cli-0.4.55 → classifyre_cli-0.4.56}/tests/test_snowflake_source.py +0 -0
  235. {classifyre_cli-0.4.55 → classifyre_cli-0.4.56}/tests/test_source_dependency_groups.py +0 -0
  236. {classifyre_cli-0.4.55 → classifyre_cli-0.4.56}/tests/test_source_strategy_conformance.py +0 -0
  237. {classifyre_cli-0.4.55 → classifyre_cli-0.4.56}/tests/test_sqlite_source.py +0 -0
  238. {classifyre_cli-0.4.55 → classifyre_cli-0.4.56}/tests/test_tableau_source.py +0 -0
  239. {classifyre_cli-0.4.55 → classifyre_cli-0.4.56}/tests/test_tabular_automatic_sampling.py +0 -0
  240. {classifyre_cli-0.4.55 → classifyre_cli-0.4.56}/tests/test_tabular_utils.py +0 -0
  241. {classifyre_cli-0.4.55 → classifyre_cli-0.4.56}/tests/test_uv_sync.py +0 -0
  242. {classifyre_cli-0.4.55 → classifyre_cli-0.4.56}/tests/test_wordpress_source.py +0 -0
  243. {classifyre_cli-0.4.55 → classifyre_cli-0.4.56}/tests/test_youtube_source.py +0 -0
  244. {classifyre_cli-0.4.55 → classifyre_cli-0.4.56}/tests/test_youtube_source_integration.py +0 -0
  245. {classifyre_cli-0.4.55 → classifyre_cli-0.4.56}/tests/utils/test_content_extraction.py +0 -0
  246. {classifyre_cli-0.4.55 → classifyre_cli-0.4.56}/tests/utils/test_embedded_images.py +0 -0
  247. {classifyre_cli-0.4.55 → classifyre_cli-0.4.56}/tests/utils/test_file_metadata.py +0 -0
  248. {classifyre_cli-0.4.55 → classifyre_cli-0.4.56}/tests/utils/test_file_to_images.py +0 -0
  249. {classifyre_cli-0.4.55 → classifyre_cli-0.4.56}/tests/utils/test_transcription.py +0 -0
@@ -1,3 +1,3 @@
1
1
  $ uv sync
2
- Resolved 292 packages in 12ms
2
+ Resolved 292 packages in 322ms
3
3
  Checked 51 packages in 1ms
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: classifyre-cli
3
- Version: 0.4.55
3
+ Version: 0.4.56
4
4
  Summary: Classifyre CLI — scan and classify unstructured data sources
5
5
  License: MIT
6
6
  Keywords: data,ingestion,metadata,pii,secrets,unstructured
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@classifyre/cli",
3
- "version": "0.4.55",
3
+ "version": "0.4.56",
4
4
  "private": true,
5
5
  "scripts": {
6
6
  "build": "uv sync",
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "classifyre-cli"
3
- version = "0.4.55"
3
+ version = "0.4.56"
4
4
  description = "Classifyre CLI — scan and classify unstructured data sources"
5
5
  readme = "README.md"
6
6
  requires-python = ">=3.12"
@@ -83,7 +83,6 @@ content = [
83
83
  ]
84
84
  quality = [
85
85
  "datasketch>=1.10.0",
86
- "sentence-transformers>=5.5.0",
87
86
  "textstat>=0.7.13",
88
87
  "transformers>=4.50.0,<6.0.0",
89
88
  "torch>=2.12.0",
@@ -101,7 +100,6 @@ custom = [
101
100
  "datasets>=4.8.5",
102
101
  "scikit-learn>=1.7.2",
103
102
  "gliner2>=1.3.1",
104
- "sentence-transformers>=5.5.0",
105
103
  # Direct declaration so the [tool.uv.sources] cpu-index pin applies (timm
106
104
  # pulls torchvision transitively; see the ocr group note).
107
105
  "torchvision>=0.27.0",
@@ -14,7 +14,6 @@ from ._base import (
14
14
  _resolve_pipeline_severity,
15
15
  )
16
16
  from ._factory import create_runner
17
- from ._feature_extraction import FeatureExtractionRunner, _chunk_text_with_offsets, _pool_hidden
18
17
  from ._gliner2 import (
19
18
  GLiNER2Runner,
20
19
  _apply_classification_validation,
@@ -35,7 +34,6 @@ __all__ = [
35
34
  "_IMAGE_CONTENT_TYPES",
36
35
  "_TEXT_CONTENT_TYPES",
37
36
  "BaseRunner",
38
- "FeatureExtractionRunner",
39
37
  "GLiNER2Runner",
40
38
  "ImageClassificationRunner",
41
39
  "LLMRunner",
@@ -45,12 +43,10 @@ __all__ = [
45
43
  "_apply_classification_validation",
46
44
  "_apply_entity_validation",
47
45
  "_chunk_text",
48
- "_chunk_text_with_offsets",
49
46
  "_load_regex_engine",
50
47
  "_normalise_classification_output",
51
48
  "_normalise_entity_output",
52
49
  "_normalise_span",
53
- "_pool_hidden",
54
50
  "_resolve_pipeline_severity",
55
51
  "create_runner",
56
52
  ]
@@ -3,7 +3,6 @@
3
3
  from __future__ import annotations
4
4
 
5
5
  from ....models.generated_detectors import (
6
- FeatureExtractionPipelineSchema,
7
6
  GLiNER2PipelineSchema,
8
7
  ImageClassificationPipelineSchema,
9
8
  LLMPipelineSchema,
@@ -12,7 +11,6 @@ from ....models.generated_detectors import (
12
11
  TextClassificationPipelineSchema,
13
12
  )
14
13
  from ._base import BaseRunner
15
- from ._feature_extraction import FeatureExtractionRunner
16
14
  from ._gliner2 import GLiNER2Runner
17
15
  from ._image_classification import ImageClassificationRunner
18
16
  from ._llm import LLMRunner
@@ -28,7 +26,6 @@ def create_runner(
28
26
  | LLMPipelineSchema
29
27
  | TextClassificationPipelineSchema
30
28
  | ImageClassificationPipelineSchema
31
- | FeatureExtractionPipelineSchema
32
29
  | ObjectDetectionPipelineSchema
33
30
  ),
34
31
  detector_key: str = "",
@@ -39,8 +36,6 @@ def create_runner(
39
36
  return TextClassificationRunner(schema, detector_key, detector_name)
40
37
  if isinstance(schema, ImageClassificationPipelineSchema):
41
38
  return ImageClassificationRunner(schema, detector_key, detector_name)
42
- if isinstance(schema, FeatureExtractionPipelineSchema):
43
- return FeatureExtractionRunner(schema, detector_key, detector_name)
44
39
  if isinstance(schema, ObjectDetectionPipelineSchema):
45
40
  return ObjectDetectionRunner(schema, detector_key, detector_name)
46
41
  if isinstance(schema, RegexPipelineSchema):
@@ -264,7 +264,7 @@ async def run_command_async(args: argparse.Namespace, recipe: dict[str, Any]) ->
264
264
  logger.info("Phase 1 complete: %d assets discovered", total_assets)
265
265
 
266
266
  # --- Phase 2: Processing ---
267
- if has_detectors and all_stubs:
267
+ if all_stubs:
268
268
  import asyncio as _asyncio
269
269
 
270
270
  processed_count = 0
@@ -319,6 +319,10 @@ async def run_command_async(args: argparse.Namespace, recipe: dict[str, Any]) ->
319
319
  )
320
320
  payload = _asset_to_payload(result)
321
321
  await sink.emit_batch([payload], skip_findings=False)
322
+ if hasattr(sink, "emit_text_chunks"):
323
+ artifact = pipeline.take_text_artifact(asset_hash)
324
+ if artifact is not None:
325
+ await sink.emit_text_chunks(asset_hash, artifact)
322
326
 
323
327
  if hasattr(sink, "update_asset_status"):
324
328
  f_total, f_by_sev, f_by_det = _compute_findings_counts(
@@ -353,22 +357,6 @@ async def run_command_async(args: argparse.Namespace, recipe: dict[str, Any]) ->
353
357
  processed_count,
354
358
  error_count,
355
359
  )
356
- elif all_stubs and hasattr(sink, "update_asset_status"):
357
- # No detectors configured: mark discovered assets as PROCESSED
358
- import asyncio as _asyncio
359
-
360
- async def _mark_processed(asset: Any) -> None:
361
- asset_hash = getattr(asset, "hash", None) or ""
362
- await sink.update_asset_status(
363
- asset_hash, "PROCESSED", findings_total=0
364
- )
365
-
366
- tasks = [_asyncio.create_task(_mark_processed(a)) for a in all_stubs]
367
- await _asyncio.gather(*tasks, return_exceptions=True)
368
- logger.info(
369
- "Phase 2 skipped (no detectors): %d assets marked processed",
370
- len(all_stubs),
371
- )
372
360
 
373
361
  # Persist the advanced AUTOMATIC sampling cursor (no-op for
374
362
  # other strategies, which return None). Only on the normal
@@ -1255,78 +1255,6 @@ class ImageClassificationPipelineSchema(BaseModel):
1255
1255
 
1256
1256
 
1257
1257
  class Type7(StrEnum):
1258
- FEATURE_EXTRACTION = 'FEATURE_EXTRACTION'
1259
-
1260
-
1261
- class PoolingStrategy(StrEnum):
1262
- """
1263
- How to aggregate per-token hidden states into a single embedding vector.
1264
- """
1265
-
1266
- mean = 'mean'
1267
- cls = 'cls'
1268
- max = 'max'
1269
- none = 'none'
1270
-
1271
-
1272
- class ChunkSize2(RootModel[int]):
1273
- root: int = Field(
1274
- None,
1275
- description='Split text into chunks of this many characters before embedding. Each chunk produces its own finding.',
1276
- ge=1,
1277
- )
1278
-
1279
-
1280
- class FeatureExtractionPipelineSchema(BaseModel):
1281
- """
1282
- Feature extraction (embedding) pipeline using a HuggingFace model. Runs a single encoder; create multiple custom detectors to run multiple embedding models.
1283
- """
1284
-
1285
- model_config = ConfigDict(
1286
- extra='forbid',
1287
- )
1288
- type: Literal['FEATURE_EXTRACTION']
1289
- model: str = Field(
1290
- ...,
1291
- description="HuggingFace hub ID (e.g. 'BAAI/bge-base-en-v1.5', 'sentence-transformers/all-MiniLM-L6-v2') or absolute local directory path.",
1292
- )
1293
- model_revision: str | None = Field(
1294
- None,
1295
- description='Git branch, tag, or commit hash when fetching from the HuggingFace hub.',
1296
- )
1297
- device: str | None = Field(
1298
- 'cpu',
1299
- description="Inference device: 'cpu' (default), 'cuda', 'mps', or a CUDA device string like 'cuda:0'.",
1300
- )
1301
- pooling_strategy: PoolingStrategy | None = Field(
1302
- 'mean',
1303
- description='How to aggregate per-token hidden states into a single embedding vector.',
1304
- )
1305
- normalize_embeddings: bool | None = Field(
1306
- True,
1307
- description='L2-normalise the final embedding vector. Recommended for cosine-similarity workloads.',
1308
- )
1309
- truncation: bool | None = Field(
1310
- True, description="Truncate input to the model's maximum sequence length."
1311
- )
1312
- max_length: int | None = Field(
1313
- None, description="Override the tokenizer's default maximum sequence length."
1314
- )
1315
- batch_size: int | None = Field(
1316
- 8, description='Number of texts to encode in a single forward pass.'
1317
- )
1318
- chunk_size: ChunkSize2 | None = Field(
1319
- None,
1320
- description='Split text into chunks of this many characters before embedding. Each chunk produces its own finding.',
1321
- )
1322
- chunk_overlap: ChunkOverlap1 | None = Field(
1323
- 0,
1324
- description='Character overlap between consecutive chunks.',
1325
- validate_default=True,
1326
- )
1327
-
1328
-
1329
- class Type8(StrEnum):
1330
1258
  OBJECT_DETECTION = 'OBJECT_DETECTION'
1331
1259
 
1332
1260
 
@@ -1407,7 +1335,6 @@ class CustomDetectorConfig(DetectorConfig):
1407
1335
  | LLMPipelineSchema
1408
1336
  | TextClassificationPipelineSchema
1409
1337
  | ImageClassificationPipelineSchema
1410
- | FeatureExtractionPipelineSchema
1411
1338
  | ObjectDetectionPipelineSchema
1412
1339
  | None
1413
1340
  ) = Field(None, discriminator='type', title='AnyPipelineSchema')
@@ -87,6 +87,19 @@ class Location(BaseModel):
87
87
  end: int | None = Field(None, description='End offset (0-indexed)')
88
88
 
89
89
 
90
+ class TextExtractionStatus(StrEnum):
91
+ """
92
+ Structured text coverage outcome. EXTRACTED means detector-visible text was produced. EMPTY means extraction completed without an error but yielded no text. ENGINE_UNAVAILABLE, ZERO_FRAMES, and FAILED are incomplete coverage states and must not be treated as an empty document.
93
+ """
94
+
95
+ NOT_APPLICABLE = 'NOT_APPLICABLE'
96
+ EXTRACTED = 'EXTRACTED'
97
+ EMPTY = 'EMPTY'
98
+ ENGINE_UNAVAILABLE = 'ENGINE_UNAVAILABLE'
99
+ ZERO_FRAMES = 'ZERO_FRAMES'
100
+ FAILED = 'FAILED'
101
+
102
+
90
103
  class DetectionResult(BaseModel):
91
104
  """
92
105
  Result from detector scan
@@ -221,6 +234,10 @@ class ScanStats(BaseModel):
221
234
  None,
222
235
  description="True when an asset that should carry text yielded none. Most significant for assets whose text is derived rather than native (OCR'd images and scanned PDFs, transcribed audio/video): empty derived text means the content may have been missed entirely, not that the document is blank. Distinct from an error — nothing failed — so it is reported as coverage, not as a run failure.",
223
236
  )
237
+ text_extraction_status: TextExtractionStatus | None = Field(
238
+ None,
239
+ description='Structured text coverage outcome. EXTRACTED means detector-visible text was produced. EMPTY means extraction completed without an error but yielded no text. ENGINE_UNAVAILABLE, ZERO_FRAMES, and FAILED are incomplete coverage states and must not be treated as an empty document.',
240
+ )
224
241
  findings_count: int | None = Field(
225
242
  None, description='Total number of findings detected'
226
243
  )
@@ -10,6 +10,7 @@ from pydantic import BaseModel, ConfigDict, Field
10
10
  from requests.adapters import HTTPAdapter
11
11
  from urllib3.util.retry import Retry # type: ignore[import-untyped]
12
12
 
13
+ from ..pipeline.text_artifact import TextArtifact
13
14
  from .base import OutputRuntimeContext, OutputType
14
15
 
15
16
  logger = logging.getLogger(__name__)
@@ -223,6 +224,15 @@ class RestOutputSink:
223
224
  payload.model_dump(mode="json", by_alias=True),
224
225
  )
225
226
 
227
+ async def emit_text_chunks(self, asset_hash: str, artifact: TextArtifact) -> None:
228
+ """Send extracted text provenance; the API embeds it asynchronously."""
229
+ source_id = self._require_source_id()
230
+ self._request_json(
231
+ "POST",
232
+ f"/sources/{source_id}/embeddings/chunks",
233
+ {"assetHash": asset_hash, "chunks": artifact.chunks},
234
+ )
235
+
226
236
  def _split_by_size(self, assets: list[dict[str, Any]]) -> list[list[dict[str, Any]]]:
227
237
  """Split assets into chunks that each stay under _MAX_BATCH_BYTES."""
228
238
  import json as _json
@@ -0,0 +1,25 @@
1
+ """Pipeline for processing assets through detectors.
2
+
3
+ Exports are lazy so lightweight callers do not import the detector and source
4
+ graph (which itself uses the REST output types).
5
+ """
6
+
7
+ from typing import Any
8
+
9
+ __all__ = ["ContentProvider", "DetectorPipeline", "ParsedContentProvider"]
10
+
11
+
12
+ def __getattr__(name: str) -> Any:
13
+ if name == "ContentProvider":
14
+ from .content_provider import ContentProvider
15
+
16
+ return ContentProvider
17
+ if name == "DetectorPipeline":
18
+ from .detector_pipeline import DetectorPipeline
19
+
20
+ return DetectorPipeline
21
+ if name == "ParsedContentProvider":
22
+ from .parsed_content_provider import ParsedContentProvider
23
+
24
+ return ParsedContentProvider
25
+ raise AttributeError(name)
@@ -20,10 +20,12 @@ from ..models.generated_single_asset_scan_results import (
20
20
  ScanStats,
21
21
  SingleAssetScanResults,
22
22
  Status,
23
+ TextExtractionStatus,
23
24
  )
24
25
  from ..sources.base import BaseSource
25
- from ..utils.file_parser import resolve_mime_type
26
+ from ..utils.file_parser import TextExtractionCoverageError, resolve_mime_type
26
27
  from .content_provider import ContentProvider
28
+ from .text_artifact import TextArtifact
27
29
  from .worker_pool import DetectorWorkerPool, is_io_bound_detector
28
30
 
29
31
  logger = logging.getLogger(__name__)
@@ -67,6 +69,12 @@ class DetectorPipeline:
67
69
 
68
70
  self.content_provider = ParsedContentProvider(source)
69
71
  self.init_warnings: list[str] = []
72
+ self._text_artifacts: dict[str, TextArtifact] = {}
73
+ self._text_extraction_errors: dict[str, list[str]] = {}
74
+ self._text_extraction_statuses: dict[str, TextExtractionStatus] = {}
75
+
76
+ def take_text_artifact(self, asset_hash: str) -> TextArtifact | None:
77
+ return self._text_artifacts.pop(asset_hash, None)
70
78
 
71
79
  def _register_detector_info(self, detector: BaseDetector, info: _DetectorInfo) -> None:
72
80
  self._detector_info[id(detector)] = info
@@ -105,10 +113,6 @@ class DetectorPipeline:
105
113
  findings_flush_size: int = 50,
106
114
  ) -> SingleAssetScanResults:
107
115
  """Process a single asset through detectors."""
108
- if not self.detectors:
109
- asset.findings = []
110
- return asset
111
-
112
116
  scan_started = datetime.now(UTC)
113
117
  text_content_type = self._text_content_type_for_asset(asset.asset_type)
114
118
  link_content = self._build_links_payload(asset.links)
@@ -167,8 +171,10 @@ class DetectorPipeline:
167
171
  # Per-asset, per-detector outcomes. Local to this call, so concurrent
168
172
  # assets never share it.
169
173
  outcome_sink: dict[tuple[DetectorType, str | None], DetectorOutcome] = {}
174
+ artifact = TextArtifact()
175
+ self._text_artifacts[str(asset.hash)] = artifact
170
176
 
171
- if text_detectors:
177
+ if text_content_type:
172
178
  (
173
179
  text_findings,
174
180
  text_detector_types_run,
@@ -187,6 +193,7 @@ class DetectorPipeline:
187
193
  findings.extend(text_findings)
188
194
  scan_warnings.extend(text_warnings)
189
195
  scan_errors.extend(text_errors)
196
+ scan_errors.extend(self._text_extraction_errors.pop(str(asset.hash), []))
190
197
  detector_types_run = self._merge_detector_types(
191
198
  detector_types_run,
192
199
  text_detector_types_run,
@@ -243,6 +250,16 @@ class DetectorPipeline:
243
250
  # Structured rather than left for a consumer to parse out of the
244
251
  # warning strings, so missing-text coverage is queryable.
245
252
  empty_text=should_warn_on_empty_text and content_size == 0,
253
+ text_extraction_status=self._text_extraction_statuses.pop(
254
+ str(asset.hash),
255
+ (
256
+ TextExtractionStatus.EXTRACTED
257
+ if text_content_type and content_size > 0
258
+ else TextExtractionStatus.EMPTY
259
+ if text_content_type
260
+ else TextExtractionStatus.NOT_APPLICABLE
261
+ ),
262
+ ),
246
263
  findings_count=len(findings),
247
264
  warnings=scan_warnings or None,
248
265
  errors=scan_errors or None,
@@ -354,6 +371,7 @@ class DetectorPipeline:
354
371
  async for text_content in self._iter_text_content_pages(asset):
355
372
  page_index += 1
356
373
  content_size += len(text_content)
374
+ self._text_artifacts[str(asset.hash)].add_page(text_content, page_index)
357
375
 
358
376
  if not text_content:
359
377
  continue
@@ -484,6 +502,7 @@ class DetectorPipeline:
484
502
  async for text_content in self._iter_text_content_pages(asset):
485
503
  page_index += 1
486
504
  content_size += len(text_content)
505
+ self._text_artifacts[str(asset.hash)].add_page(text_content, page_index)
487
506
 
488
507
  if not text_content:
489
508
  continue
@@ -535,11 +554,23 @@ class DetectorPipeline:
535
554
 
536
555
  for candidate_id in candidate_ids:
537
556
  saw_candidate_content = False
538
- async for text_content in self.content_provider.fetch_text_pages(candidate_id):
539
- if not text_content:
540
- continue
541
- saw_candidate_content = True
542
- yield text_content
557
+ try:
558
+ async for text_content in self.content_provider.fetch_text_pages(candidate_id):
559
+ if not text_content:
560
+ continue
561
+ saw_candidate_content = True
562
+ yield text_content
563
+ except TextExtractionCoverageError as exc:
564
+ # Text extraction failure must not discard independent link or
565
+ # binary detector results. The structured empty-text coverage
566
+ # signal still records that this asset was not embedded.
567
+ message = f"Text extraction failed for {candidate_id}: {exc}"
568
+ logger.warning(message)
569
+ self._text_extraction_errors.setdefault(str(asset.hash), []).append(message)
570
+ self._text_extraction_statuses[str(asset.hash)] = TextExtractionStatus(
571
+ exc.code.value
572
+ )
573
+ return
543
574
 
544
575
  if saw_candidate_content:
545
576
  return
@@ -0,0 +1,54 @@
1
+ """Extracted text chunks sent to the API for asynchronous semantic indexing."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import re
6
+ from dataclasses import dataclass, field
7
+ from typing import Any
8
+
9
+ _WHITESPACE = re.compile(r"\s+")
10
+
11
+
12
+ def normalize_text(text: str) -> str:
13
+ return _WHITESPACE.sub(" ", text).strip()
14
+
15
+
16
+ def chunk_text_with_offsets(
17
+ text: str, *, max_chars: int = 1200, overlap_chars: int = 200
18
+ ) -> list[tuple[str, int, int]]:
19
+ normalized = normalize_text(text)
20
+ if not normalized:
21
+ return []
22
+ chunks: list[tuple[str, int, int]] = []
23
+ start = 0
24
+ while start < len(normalized):
25
+ end = min(len(normalized), start + max_chars)
26
+ if end < len(normalized):
27
+ boundary = normalized.rfind(" ", start + max_chars // 2, end)
28
+ if boundary > start:
29
+ end = boundary
30
+ value = normalized[start:end].strip()
31
+ if value:
32
+ actual_start = normalized.find(value, start, end + 1)
33
+ chunks.append((value, actual_start, len(value)))
34
+ if end >= len(normalized):
35
+ break
36
+ start = max(start + 1, end - overlap_chars)
37
+ return chunks
38
+
39
+
40
+ @dataclass
41
+ class TextArtifact:
42
+ chunks: list[dict[str, Any]] = field(default_factory=list)
43
+
44
+ def add_page(self, text: str, page: int) -> None:
45
+ for value, offset, length in chunk_text_with_offsets(text):
46
+ self.chunks.append(
47
+ {
48
+ "ordinal": len(self.chunks),
49
+ "page": page,
50
+ "charOffset": offset,
51
+ "charLength": length,
52
+ "text": value,
53
+ }
54
+ )
@@ -158,7 +158,6 @@ _ML_PIPELINE_TYPES = frozenset(
158
158
  "GLINER2",
159
159
  "TEXT_CLASSIFICATION",
160
160
  "IMAGE_CLASSIFICATION",
161
- "FEATURE_EXTRACTION",
162
161
  "OBJECT_DETECTION",
163
162
  }
164
163
  )
@@ -7,12 +7,34 @@ import tempfile
7
7
  import threading
8
8
  from collections.abc import Generator
9
9
  from dataclasses import dataclass
10
+ from enum import StrEnum
10
11
  from pathlib import Path
11
12
  from urllib.parse import urlsplit
12
13
 
13
14
  logger = logging.getLogger(__name__)
14
15
 
15
16
 
17
+ class TextExtractionCoverageCode(StrEnum):
18
+ """Machine-readable reason detector-visible text could not be produced."""
19
+
20
+ ENGINE_UNAVAILABLE = "ENGINE_UNAVAILABLE"
21
+ ZERO_FRAMES = "ZERO_FRAMES"
22
+ FAILED = "FAILED"
23
+
24
+
25
+ class TextExtractionCoverageError(RuntimeError):
26
+ """Extraction failed after source content was fetched successfully."""
27
+
28
+ def __init__(
29
+ self,
30
+ message: str,
31
+ *,
32
+ code: TextExtractionCoverageCode = TextExtractionCoverageCode.FAILED,
33
+ ) -> None:
34
+ super().__init__(message)
35
+ self.code = code
36
+
37
+
16
38
  @dataclass
17
39
  class ParsedFile:
18
40
  """Result of parsing a local file."""
@@ -432,6 +454,7 @@ def extract_text(
432
454
  Returns:
433
455
  (text_content, error_message_or_None)
434
456
  """
457
+ ocr_error: str | None = None
435
458
  if _supports_docling_ocr(mime_type, file_name):
436
459
  # PDFs: try cheap native text extraction first. Only hand off to the
437
460
  # heavy docling pipeline when the native path yields too little text,
@@ -460,6 +483,7 @@ def extract_text(
460
483
  return text, None
461
484
  if error:
462
485
  logger.warning("OCR extraction failed for %s: %s", file_name or mime_type, error)
486
+ ocr_error = error
463
487
 
464
488
  # Media processing is based on detected content type, never source config.
465
489
  if mime_type.startswith("audio/"):
@@ -497,11 +521,12 @@ def extract_text(
497
521
  # Unsupported image formats have no native text path; supported images were
498
522
  # already handled by the automatic OCR branch above.
499
523
  if mime_type.startswith("image/"):
500
- return "", None
524
+ return "", ocr_error
501
525
 
502
526
  # PDF
503
527
  if mime_type == "application/pdf":
504
- return _extract_pdf_text(file_bytes)
528
+ native_text, native_error = _extract_pdf_text(file_bytes)
529
+ return native_text, native_error or ocr_error
505
530
 
506
531
  # DOCX
507
532
  if mime_type == "application/vnd.openxmlformats-officedocument.wordprocessingml.document":
@@ -576,7 +601,7 @@ def extract_text(
576
601
  return "", f"Parquet extraction failed: {e}"
577
602
 
578
603
  # Unknown / binary
579
- return "", None
604
+ return "", ocr_error
580
605
 
581
606
 
582
607
  def _decode_bytes(file_bytes: bytes) -> str:
@@ -707,7 +732,11 @@ def iter_file_pages(
707
732
  )
708
733
  elif normalized.startswith("video/"):
709
734
  from .transcription import iter_transcription_pages
710
- from .video_processing import iter_video_ocr_segments
735
+ from .video_processing import (
736
+ VideoOCREngineUnavailableError,
737
+ VideoOCRZeroFramesError,
738
+ iter_video_ocr_segments,
739
+ )
711
740
 
712
741
  try:
713
742
  yield from iter_transcription_pages(
@@ -721,7 +750,15 @@ def iter_file_pages(
721
750
  try:
722
751
  yield from iter_video_ocr_segments(file_bytes, file_name=file_name)
723
752
  except Exception as exc:
724
- logger.warning("Video OCR failed for %s: %s", file_name or mime_type, exc)
753
+ code = TextExtractionCoverageCode.FAILED
754
+ if isinstance(exc, VideoOCREngineUnavailableError):
755
+ code = TextExtractionCoverageCode.ENGINE_UNAVAILABLE
756
+ elif isinstance(exc, VideoOCRZeroFramesError):
757
+ code = TextExtractionCoverageCode.ZERO_FRAMES
758
+ raise TextExtractionCoverageError(
759
+ f"Video OCR coverage failed for {file_name or mime_type}: {exc}",
760
+ code=code,
761
+ ) from exc
725
762
  else:
726
763
  text, error = extract_text(
727
764
  file_bytes,
@@ -730,6 +767,17 @@ def iter_file_pages(
730
767
  )
731
768
  if error:
732
769
  logger.warning("Text extraction error (%s): %s", mime_type, error)
770
+ if not text:
771
+ normalized_error = error.casefold()
772
+ unavailable = "unavailable" in normalized_error or "dependency" in normalized_error
773
+ raise TextExtractionCoverageError(
774
+ f"Text extraction coverage failed for {file_name or mime_type}: {error}",
775
+ code=(
776
+ TextExtractionCoverageCode.ENGINE_UNAVAILABLE
777
+ if unavailable
778
+ else TextExtractionCoverageCode.FAILED
779
+ ),
780
+ )
733
781
  if text:
734
782
  yield from _iter_text_lines(text, batch_size)
735
783