classifyre-cli 0.6.11__tar.gz → 0.6.12__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.
- classifyre_cli-0.6.12/.turbo/turbo-build.log +3 -0
- {classifyre_cli-0.6.11 → classifyre_cli-0.6.12}/PKG-INFO +1 -1
- {classifyre_cli-0.6.11 → classifyre_cli-0.6.12}/package.json +1 -1
- {classifyre_cli-0.6.11 → classifyre_cli-0.6.12}/pyproject.toml +1 -1
- {classifyre_cli-0.6.11 → classifyre_cli-0.6.12}/src/main.py +33 -1
- {classifyre_cli-0.6.11 → classifyre_cli-0.6.12}/src/outputs/rest.py +22 -6
- {classifyre_cli-0.6.11 → classifyre_cli-0.6.12}/src/pipeline/detector_pipeline.py +55 -0
- classifyre_cli-0.6.12/src/pipeline/finding_context.py +99 -0
- classifyre_cli-0.6.12/src/pipeline/media_artifact.py +262 -0
- {classifyre_cli-0.6.11 → classifyre_cli-0.6.12}/src/pipeline/scan_cache.py +58 -2
- {classifyre_cli-0.6.11 → classifyre_cli-0.6.12}/src/pipeline/text_artifact.py +20 -1
- classifyre_cli-0.6.12/tests/test_finding_context.py +79 -0
- classifyre_cli-0.6.12/tests/test_media_embedding.py +437 -0
- {classifyre_cli-0.6.11 → classifyre_cli-0.6.12}/uv.lock +116 -116
- classifyre_cli-0.6.11/.turbo/turbo-build.log +0 -3
- {classifyre_cli-0.6.11 → classifyre_cli-0.6.12}/.gitignore +0 -0
- {classifyre_cli-0.6.11 → classifyre_cli-0.6.12}/.python-version +0 -0
- {classifyre_cli-0.6.11 → classifyre_cli-0.6.12}/README.md +0 -0
- {classifyre_cli-0.6.11 → classifyre_cli-0.6.12}/main.py +0 -0
- {classifyre_cli-0.6.11 → classifyre_cli-0.6.12}/scripts/generate_models.py +0 -0
- {classifyre_cli-0.6.11 → classifyre_cli-0.6.12}/scripts/generate_runtime_packages.py +0 -0
- {classifyre_cli-0.6.11 → classifyre_cli-0.6.12}/scripts/generate_sdk_completions.py +0 -0
- {classifyre_cli-0.6.11 → classifyre_cli-0.6.12}/src/__init__.py +0 -0
- {classifyre_cli-0.6.11 → classifyre_cli-0.6.12}/src/augmentation/__init__.py +0 -0
- {classifyre_cli-0.6.11 → classifyre_cli-0.6.12}/src/augmentation/contract.py +0 -0
- {classifyre_cli-0.6.11 → classifyre_cli-0.6.12}/src/augmentation/preview.py +0 -0
- {classifyre_cli-0.6.11 → classifyre_cli-0.6.12}/src/augmentation/runner.py +0 -0
- {classifyre_cli-0.6.11 → classifyre_cli-0.6.12}/src/augmentation/sdk.py +0 -0
- {classifyre_cli-0.6.11 → classifyre_cli-0.6.12}/src/augmentation/session.py +0 -0
- {classifyre_cli-0.6.11 → classifyre_cli-0.6.12}/src/config.py +0 -0
- {classifyre_cli-0.6.11 → classifyre_cli-0.6.12}/src/detectors/__init__.py +0 -0
- {classifyre_cli-0.6.11 → classifyre_cli-0.6.12}/src/detectors/base.py +0 -0
- {classifyre_cli-0.6.11 → classifyre_cli-0.6.12}/src/detectors/broken_links/__init__.py +0 -0
- {classifyre_cli-0.6.11 → classifyre_cli-0.6.12}/src/detectors/broken_links/detector.py +0 -0
- {classifyre_cli-0.6.11 → classifyre_cli-0.6.12}/src/detectors/config.py +0 -0
- {classifyre_cli-0.6.11 → classifyre_cli-0.6.12}/src/detectors/content/__init__.py +0 -0
- {classifyre_cli-0.6.11 → classifyre_cli-0.6.12}/src/detectors/custom/__init__.py +0 -0
- {classifyre_cli-0.6.11 → classifyre_cli-0.6.12}/src/detectors/custom/detector.py +0 -0
- {classifyre_cli-0.6.11 → classifyre_cli-0.6.12}/src/detectors/custom/extractor.py +0 -0
- {classifyre_cli-0.6.11 → classifyre_cli-0.6.12}/src/detectors/custom/runners/__init__.py +0 -0
- {classifyre_cli-0.6.11 → classifyre_cli-0.6.12}/src/detectors/custom/runners/_base.py +0 -0
- {classifyre_cli-0.6.11 → classifyre_cli-0.6.12}/src/detectors/custom/runners/_custom_detector.py +0 -0
- {classifyre_cli-0.6.11 → classifyre_cli-0.6.12}/src/detectors/custom/runners/_decision.py +0 -0
- {classifyre_cli-0.6.11 → classifyre_cli-0.6.12}/src/detectors/custom/runners/_factory.py +0 -0
- {classifyre_cli-0.6.11 → classifyre_cli-0.6.12}/src/detectors/custom/runners/_gliner2.py +0 -0
- {classifyre_cli-0.6.11 → classifyre_cli-0.6.12}/src/detectors/custom/runners/_image_classification.py +0 -0
- {classifyre_cli-0.6.11 → classifyre_cli-0.6.12}/src/detectors/custom/runners/_llm.py +0 -0
- {classifyre_cli-0.6.11 → classifyre_cli-0.6.12}/src/detectors/custom/runners/_object_detection.py +0 -0
- {classifyre_cli-0.6.11 → classifyre_cli-0.6.12}/src/detectors/custom/runners/_provider_call.py +0 -0
- {classifyre_cli-0.6.11 → classifyre_cli-0.6.12}/src/detectors/custom/runners/_regex.py +0 -0
- {classifyre_cli-0.6.11 → classifyre_cli-0.6.12}/src/detectors/custom/runners/_tag.py +0 -0
- {classifyre_cli-0.6.11 → classifyre_cli-0.6.12}/src/detectors/custom/runners/_text_classification.py +0 -0
- {classifyre_cli-0.6.11 → classifyre_cli-0.6.12}/src/detectors/custom/trainer.py +0 -0
- {classifyre_cli-0.6.11 → classifyre_cli-0.6.12}/src/detectors/custom_detector/__init__.py +0 -0
- {classifyre_cli-0.6.11 → classifyre_cli-0.6.12}/src/detectors/custom_detector/contract.py +0 -0
- {classifyre_cli-0.6.11 → classifyre_cli-0.6.12}/src/detectors/custom_detector/files.py +0 -0
- {classifyre_cli-0.6.11 → classifyre_cli-0.6.12}/src/detectors/custom_detector/preview.py +0 -0
- {classifyre_cli-0.6.11 → classifyre_cli-0.6.12}/src/detectors/custom_detector/results.py +0 -0
- {classifyre_cli-0.6.11 → classifyre_cli-0.6.12}/src/detectors/custom_detector/runner.py +0 -0
- {classifyre_cli-0.6.11 → classifyre_cli-0.6.12}/src/detectors/custom_detector/sdk.py +0 -0
- {classifyre_cli-0.6.11 → classifyre_cli-0.6.12}/src/detectors/custom_detector/session.py +0 -0
- {classifyre_cli-0.6.11 → classifyre_cli-0.6.12}/src/detectors/dependencies.py +0 -0
- {classifyre_cli-0.6.11 → classifyre_cli-0.6.12}/src/detectors/engine_version.py +0 -0
- {classifyre_cli-0.6.11 → classifyre_cli-0.6.12}/src/detectors/errors.py +0 -0
- {classifyre_cli-0.6.11 → classifyre_cli-0.6.12}/src/detectors/pii/__init__.py +0 -0
- {classifyre_cli-0.6.11 → classifyre_cli-0.6.12}/src/detectors/pii/detector.py +0 -0
- {classifyre_cli-0.6.11 → classifyre_cli-0.6.12}/src/detectors/secrets/__init__.py +0 -0
- {classifyre_cli-0.6.11 → classifyre_cli-0.6.12}/src/detectors/secrets/detector.py +0 -0
- {classifyre_cli-0.6.11 → classifyre_cli-0.6.12}/src/detectors/threat/__init__.py +0 -0
- {classifyre_cli-0.6.11 → classifyre_cli-0.6.12}/src/detectors/threat/code_security_detector.py +0 -0
- {classifyre_cli-0.6.11 → classifyre_cli-0.6.12}/src/detectors/threat/yara_detector.py +0 -0
- {classifyre_cli-0.6.11 → classifyre_cli-0.6.12}/src/file_evaluation/__init__.py +0 -0
- {classifyre_cli-0.6.11 → classifyre_cli-0.6.12}/src/file_evaluation/asset_fixture.py +0 -0
- {classifyre_cli-0.6.11 → classifyre_cli-0.6.12}/src/file_evaluation/runner.py +0 -0
- {classifyre_cli-0.6.11 → classifyre_cli-0.6.12}/src/graph/__init__.py +0 -0
- {classifyre_cli-0.6.11 → classifyre_cli-0.6.12}/src/graph/edges.py +0 -0
- {classifyre_cli-0.6.11 → classifyre_cli-0.6.12}/src/models/generated_detectors.py +0 -0
- {classifyre_cli-0.6.11 → classifyre_cli-0.6.12}/src/models/generated_input.py +0 -0
- {classifyre_cli-0.6.11 → classifyre_cli-0.6.12}/src/models/generated_single_asset_scan_results.py +0 -0
- {classifyre_cli-0.6.11 → classifyre_cli-0.6.12}/src/notebook/__init__.py +0 -0
- {classifyre_cli-0.6.11 → classifyre_cli-0.6.12}/src/notebook/child.py +0 -0
- {classifyre_cli-0.6.11 → classifyre_cli-0.6.12}/src/notebook/cli.py +0 -0
- {classifyre_cli-0.6.11 → classifyre_cli-0.6.12}/src/notebook/cohort.py +0 -0
- {classifyre_cli-0.6.11 → classifyre_cli-0.6.12}/src/notebook/contract.py +0 -0
- {classifyre_cli-0.6.11 → classifyre_cli-0.6.12}/src/notebook/execute.py +0 -0
- {classifyre_cli-0.6.11 → classifyre_cli-0.6.12}/src/notebook/files.py +0 -0
- {classifyre_cli-0.6.11 → classifyre_cli-0.6.12}/src/notebook/groups.py +0 -0
- {classifyre_cli-0.6.11 → classifyre_cli-0.6.12}/src/notebook/outputs.py +0 -0
- {classifyre_cli-0.6.11 → classifyre_cli-0.6.12}/src/notebook/packages.py +0 -0
- {classifyre_cli-0.6.11 → classifyre_cli-0.6.12}/src/notebook/protocol.py +0 -0
- {classifyre_cli-0.6.11 → classifyre_cli-0.6.12}/src/notebook/redact.py +0 -0
- {classifyre_cli-0.6.11 → classifyre_cli-0.6.12}/src/notebook/scaffold.py +0 -0
- {classifyre_cli-0.6.11 → classifyre_cli-0.6.12}/src/notebook/sdk.py +0 -0
- {classifyre_cli-0.6.11 → classifyre_cli-0.6.12}/src/notebook/serialize.py +0 -0
- {classifyre_cli-0.6.11 → classifyre_cli-0.6.12}/src/outputs/__init__.py +0 -0
- {classifyre_cli-0.6.11 → classifyre_cli-0.6.12}/src/outputs/base.py +0 -0
- {classifyre_cli-0.6.11 → classifyre_cli-0.6.12}/src/outputs/console.py +0 -0
- {classifyre_cli-0.6.11 → classifyre_cli-0.6.12}/src/outputs/factory.py +0 -0
- {classifyre_cli-0.6.11 → classifyre_cli-0.6.12}/src/outputs/file.py +0 -0
- {classifyre_cli-0.6.11 → classifyre_cli-0.6.12}/src/pipeline/__init__.py +0 -0
- {classifyre_cli-0.6.11 → classifyre_cli-0.6.12}/src/pipeline/asset_payload_server.py +0 -0
- {classifyre_cli-0.6.11 → classifyre_cli-0.6.12}/src/pipeline/content_provider.py +0 -0
- {classifyre_cli-0.6.11 → classifyre_cli-0.6.12}/src/pipeline/detector_breaker.py +0 -0
- {classifyre_cli-0.6.11 → classifyre_cli-0.6.12}/src/pipeline/parsed_content_provider.py +0 -0
- {classifyre_cli-0.6.11 → classifyre_cli-0.6.12}/src/pipeline/payload_window.py +0 -0
- {classifyre_cli-0.6.11 → classifyre_cli-0.6.12}/src/pipeline/worker_pool.py +0 -0
- {classifyre_cli-0.6.11 → classifyre_cli-0.6.12}/src/sources/__init__.py +0 -0
- {classifyre_cli-0.6.11 → classifyre_cli-0.6.12}/src/sources/asset_metadata.py +0 -0
- {classifyre_cli-0.6.11 → classifyre_cli-0.6.12}/src/sources/atlassian_common.py +0 -0
- {classifyre_cli-0.6.11 → classifyre_cli-0.6.12}/src/sources/azure_blob_storage/__init__.py +0 -0
- {classifyre_cli-0.6.11 → classifyre_cli-0.6.12}/src/sources/azure_blob_storage/source.py +0 -0
- {classifyre_cli-0.6.11 → classifyre_cli-0.6.12}/src/sources/base.py +0 -0
- {classifyre_cli-0.6.11 → classifyre_cli-0.6.12}/src/sources/confluence/__init__.py +0 -0
- {classifyre_cli-0.6.11 → classifyre_cli-0.6.12}/src/sources/confluence/source.py +0 -0
- {classifyre_cli-0.6.11 → classifyre_cli-0.6.12}/src/sources/custom/__init__.py +0 -0
- {classifyre_cli-0.6.11 → classifyre_cli-0.6.12}/src/sources/custom/env.py +0 -0
- {classifyre_cli-0.6.11 → classifyre_cli-0.6.12}/src/sources/custom/runner.py +0 -0
- {classifyre_cli-0.6.11 → classifyre_cli-0.6.12}/src/sources/custom/source.py +0 -0
- {classifyre_cli-0.6.11 → classifyre_cli-0.6.12}/src/sources/databricks/__init__.py +0 -0
- {classifyre_cli-0.6.11 → classifyre_cli-0.6.12}/src/sources/databricks/source.py +0 -0
- {classifyre_cli-0.6.11 → classifyre_cli-0.6.12}/src/sources/delta_lake/__init__.py +0 -0
- {classifyre_cli-0.6.11 → classifyre_cli-0.6.12}/src/sources/delta_lake/source.py +0 -0
- {classifyre_cli-0.6.11 → classifyre_cli-0.6.12}/src/sources/dependencies.py +0 -0
- {classifyre_cli-0.6.11 → classifyre_cli-0.6.12}/src/sources/dremio/__init__.py +0 -0
- {classifyre_cli-0.6.11 → classifyre_cli-0.6.12}/src/sources/dremio/client.py +0 -0
- {classifyre_cli-0.6.11 → classifyre_cli-0.6.12}/src/sources/dremio/source.py +0 -0
- {classifyre_cli-0.6.11 → classifyre_cli-0.6.12}/src/sources/dropbox/__init__.py +0 -0
- {classifyre_cli-0.6.11 → classifyre_cli-0.6.12}/src/sources/dropbox/auth.py +0 -0
- {classifyre_cli-0.6.11 → classifyre_cli-0.6.12}/src/sources/dropbox/source.py +0 -0
- {classifyre_cli-0.6.11 → classifyre_cli-0.6.12}/src/sources/elasticsearch/__init__.py +0 -0
- {classifyre_cli-0.6.11 → classifyre_cli-0.6.12}/src/sources/elasticsearch/source.py +0 -0
- {classifyre_cli-0.6.11 → classifyre_cli-0.6.12}/src/sources/email/__init__.py +0 -0
- {classifyre_cli-0.6.11 → classifyre_cli-0.6.12}/src/sources/email/source.py +0 -0
- {classifyre_cli-0.6.11 → classifyre_cli-0.6.12}/src/sources/git/__init__.py +0 -0
- {classifyre_cli-0.6.11 → classifyre_cli-0.6.12}/src/sources/git/source.py +0 -0
- {classifyre_cli-0.6.11 → classifyre_cli-0.6.12}/src/sources/google_cloud_storage/__init__.py +0 -0
- {classifyre_cli-0.6.11 → classifyre_cli-0.6.12}/src/sources/google_cloud_storage/source.py +0 -0
- {classifyre_cli-0.6.11 → classifyre_cli-0.6.12}/src/sources/google_workspace/__init__.py +0 -0
- {classifyre_cli-0.6.11 → classifyre_cli-0.6.12}/src/sources/google_workspace/source.py +0 -0
- {classifyre_cli-0.6.11 → classifyre_cli-0.6.12}/src/sources/hive/__init__.py +0 -0
- {classifyre_cli-0.6.11 → classifyre_cli-0.6.12}/src/sources/hive/source.py +0 -0
- {classifyre_cli-0.6.11 → classifyre_cli-0.6.12}/src/sources/hugging_face/__init__.py +0 -0
- {classifyre_cli-0.6.11 → classifyre_cli-0.6.12}/src/sources/hugging_face/source.py +0 -0
- {classifyre_cli-0.6.11 → classifyre_cli-0.6.12}/src/sources/iceberg/__init__.py +0 -0
- {classifyre_cli-0.6.11 → classifyre_cli-0.6.12}/src/sources/iceberg/source.py +0 -0
- {classifyre_cli-0.6.11 → classifyre_cli-0.6.12}/src/sources/jira/__init__.py +0 -0
- {classifyre_cli-0.6.11 → classifyre_cli-0.6.12}/src/sources/jira/source.py +0 -0
- {classifyre_cli-0.6.11 → classifyre_cli-0.6.12}/src/sources/kafka/__init__.py +0 -0
- {classifyre_cli-0.6.11 → classifyre_cli-0.6.12}/src/sources/kafka/rest.py +0 -0
- {classifyre_cli-0.6.11 → classifyre_cli-0.6.12}/src/sources/kafka/source.py +0 -0
- {classifyre_cli-0.6.11 → classifyre_cli-0.6.12}/src/sources/lakehouse_base.py +0 -0
- {classifyre_cli-0.6.11 → classifyre_cli-0.6.12}/src/sources/local_folder/__init__.py +0 -0
- {classifyre_cli-0.6.11 → classifyre_cli-0.6.12}/src/sources/local_folder/source.py +0 -0
- {classifyre_cli-0.6.11 → classifyre_cli-0.6.12}/src/sources/meilisearch/__init__.py +0 -0
- {classifyre_cli-0.6.11 → classifyre_cli-0.6.12}/src/sources/meilisearch/source.py +0 -0
- {classifyre_cli-0.6.11 → classifyre_cli-0.6.12}/src/sources/microsoft_365/__init__.py +0 -0
- {classifyre_cli-0.6.11 → classifyre_cli-0.6.12}/src/sources/microsoft_365/source.py +0 -0
- {classifyre_cli-0.6.11 → classifyre_cli-0.6.12}/src/sources/mongodb/__init__.py +0 -0
- {classifyre_cli-0.6.11 → classifyre_cli-0.6.12}/src/sources/mongodb/source.py +0 -0
- {classifyre_cli-0.6.11 → classifyre_cli-0.6.12}/src/sources/mssql/__init__.py +0 -0
- {classifyre_cli-0.6.11 → classifyre_cli-0.6.12}/src/sources/mssql/source.py +0 -0
- {classifyre_cli-0.6.11 → classifyre_cli-0.6.12}/src/sources/mysql/__init__.py +0 -0
- {classifyre_cli-0.6.11 → classifyre_cli-0.6.12}/src/sources/mysql/source.py +0 -0
- {classifyre_cli-0.6.11 → classifyre_cli-0.6.12}/src/sources/neo4j/__init__.py +0 -0
- {classifyre_cli-0.6.11 → classifyre_cli-0.6.12}/src/sources/neo4j/source.py +0 -0
- {classifyre_cli-0.6.11 → classifyre_cli-0.6.12}/src/sources/notion/__init__.py +0 -0
- {classifyre_cli-0.6.11 → classifyre_cli-0.6.12}/src/sources/notion/client.py +0 -0
- {classifyre_cli-0.6.11 → classifyre_cli-0.6.12}/src/sources/notion/source.py +0 -0
- {classifyre_cli-0.6.11 → classifyre_cli-0.6.12}/src/sources/object_storage/base.py +0 -0
- {classifyre_cli-0.6.11 → classifyre_cli-0.6.12}/src/sources/opensearch/__init__.py +0 -0
- {classifyre_cli-0.6.11 → classifyre_cli-0.6.12}/src/sources/opensearch/source.py +0 -0
- {classifyre_cli-0.6.11 → classifyre_cli-0.6.12}/src/sources/oracle/__init__.py +0 -0
- {classifyre_cli-0.6.11 → classifyre_cli-0.6.12}/src/sources/oracle/source.py +0 -0
- {classifyre_cli-0.6.11 → classifyre_cli-0.6.12}/src/sources/postgresql/__init__.py +0 -0
- {classifyre_cli-0.6.11 → classifyre_cli-0.6.12}/src/sources/postgresql/source.py +0 -0
- {classifyre_cli-0.6.11 → classifyre_cli-0.6.12}/src/sources/powerbi/__init__.py +0 -0
- {classifyre_cli-0.6.11 → classifyre_cli-0.6.12}/src/sources/powerbi/source.py +0 -0
- {classifyre_cli-0.6.11 → classifyre_cli-0.6.12}/src/sources/recipe_normalizer.py +0 -0
- {classifyre_cli-0.6.11 → classifyre_cli-0.6.12}/src/sources/reddit/__init__.py +0 -0
- {classifyre_cli-0.6.11 → classifyre_cli-0.6.12}/src/sources/reddit/source.py +0 -0
- {classifyre_cli-0.6.11 → classifyre_cli-0.6.12}/src/sources/s3_client.py +0 -0
- {classifyre_cli-0.6.11 → classifyre_cli-0.6.12}/src/sources/s3_compatible_storage/README.md +0 -0
- {classifyre_cli-0.6.11 → classifyre_cli-0.6.12}/src/sources/s3_compatible_storage/__init__.py +0 -0
- {classifyre_cli-0.6.11 → classifyre_cli-0.6.12}/src/sources/s3_compatible_storage/source.py +0 -0
- {classifyre_cli-0.6.11 → classifyre_cli-0.6.12}/src/sources/sandbox/__init__.py +0 -0
- {classifyre_cli-0.6.11 → classifyre_cli-0.6.12}/src/sources/sandbox/source.py +0 -0
- {classifyre_cli-0.6.11 → classifyre_cli-0.6.12}/src/sources/search_engine_base.py +0 -0
- {classifyre_cli-0.6.11 → classifyre_cli-0.6.12}/src/sources/servicedesk/__init__.py +0 -0
- {classifyre_cli-0.6.11 → classifyre_cli-0.6.12}/src/sources/servicedesk/source.py +0 -0
- {classifyre_cli-0.6.11 → classifyre_cli-0.6.12}/src/sources/slack/__init__.py +0 -0
- {classifyre_cli-0.6.11 → classifyre_cli-0.6.12}/src/sources/slack/source.py +0 -0
- {classifyre_cli-0.6.11 → classifyre_cli-0.6.12}/src/sources/snowflake/__init__.py +0 -0
- {classifyre_cli-0.6.11 → classifyre_cli-0.6.12}/src/sources/snowflake/source.py +0 -0
- {classifyre_cli-0.6.11 → classifyre_cli-0.6.12}/src/sources/sqlite/__init__.py +0 -0
- {classifyre_cli-0.6.11 → classifyre_cli-0.6.12}/src/sources/sqlite/source.py +0 -0
- {classifyre_cli-0.6.11 → classifyre_cli-0.6.12}/src/sources/tableau/__init__.py +0 -0
- {classifyre_cli-0.6.11 → classifyre_cli-0.6.12}/src/sources/tableau/source.py +0 -0
- {classifyre_cli-0.6.11 → classifyre_cli-0.6.12}/src/sources/tabular_base.py +0 -0
- {classifyre_cli-0.6.11 → classifyre_cli-0.6.12}/src/sources/tabular_utils.py +0 -0
- {classifyre_cli-0.6.11 → classifyre_cli-0.6.12}/src/sources/wordpress/__init__.py +0 -0
- {classifyre_cli-0.6.11 → classifyre_cli-0.6.12}/src/sources/wordpress/source.py +0 -0
- {classifyre_cli-0.6.11 → classifyre_cli-0.6.12}/src/sources/youtube/__init__.py +0 -0
- {classifyre_cli-0.6.11 → classifyre_cli-0.6.12}/src/sources/youtube/source.py +0 -0
- {classifyre_cli-0.6.11 → classifyre_cli-0.6.12}/src/telemetry.py +0 -0
- {classifyre_cli-0.6.11 → classifyre_cli-0.6.12}/src/utils/__init__.py +0 -0
- {classifyre_cli-0.6.11 → classifyre_cli-0.6.12}/src/utils/archive_extraction.py +0 -0
- {classifyre_cli-0.6.11 → classifyre_cli-0.6.12}/src/utils/content_extraction.py +0 -0
- {classifyre_cli-0.6.11 → classifyre_cli-0.6.12}/src/utils/dependency_groups.py +0 -0
- {classifyre_cli-0.6.11 → classifyre_cli-0.6.12}/src/utils/embedded_files.py +0 -0
- {classifyre_cli-0.6.11 → classifyre_cli-0.6.12}/src/utils/external_video.py +0 -0
- {classifyre_cli-0.6.11 → classifyre_cli-0.6.12}/src/utils/file_metadata.py +0 -0
- {classifyre_cli-0.6.11 → classifyre_cli-0.6.12}/src/utils/file_parser.py +0 -0
- {classifyre_cli-0.6.11 → classifyre_cli-0.6.12}/src/utils/file_to_images.py +0 -0
- {classifyre_cli-0.6.11 → classifyre_cli-0.6.12}/src/utils/hashing.py +0 -0
- {classifyre_cli-0.6.11 → classifyre_cli-0.6.12}/src/utils/legacy_office.py +0 -0
- {classifyre_cli-0.6.11 → classifyre_cli-0.6.12}/src/utils/payload.py +0 -0
- {classifyre_cli-0.6.11 → classifyre_cli-0.6.12}/src/utils/range_reader.py +0 -0
- {classifyre_cli-0.6.11 → classifyre_cli-0.6.12}/src/utils/redaction.py +0 -0
- {classifyre_cli-0.6.11 → classifyre_cli-0.6.12}/src/utils/resources.py +0 -0
- {classifyre_cli-0.6.11 → classifyre_cli-0.6.12}/src/utils/sampling_cursor.py +0 -0
- {classifyre_cli-0.6.11 → classifyre_cli-0.6.12}/src/utils/sampling_frontier.py +0 -0
- {classifyre_cli-0.6.11 → classifyre_cli-0.6.12}/src/utils/source_files.py +0 -0
- {classifyre_cli-0.6.11 → classifyre_cli-0.6.12}/src/utils/sql_lineage.py +0 -0
- {classifyre_cli-0.6.11 → classifyre_cli-0.6.12}/src/utils/torch_runtime.py +0 -0
- {classifyre_cli-0.6.11 → classifyre_cli-0.6.12}/src/utils/transcription.py +0 -0
- {classifyre_cli-0.6.11 → classifyre_cli-0.6.12}/src/utils/urn.py +0 -0
- {classifyre_cli-0.6.11 → classifyre_cli-0.6.12}/src/utils/uv_sync.py +0 -0
- {classifyre_cli-0.6.11 → classifyre_cli-0.6.12}/src/utils/validation.py +0 -0
- {classifyre_cli-0.6.11 → classifyre_cli-0.6.12}/src/utils/video_processing.py +0 -0
- {classifyre_cli-0.6.11 → classifyre_cli-0.6.12}/tests/__init__.py +0 -0
- {classifyre_cli-0.6.11 → classifyre_cli-0.6.12}/tests/_lakehouse_fakes.py +0 -0
- {classifyre_cli-0.6.11 → classifyre_cli-0.6.12}/tests/conftest.py +0 -0
- {classifyre_cli-0.6.11 → classifyre_cli-0.6.12}/tests/detectors/__init__.py +0 -0
- {classifyre_cli-0.6.11 → classifyre_cli-0.6.12}/tests/detectors/broken_links/test_broken_links_detector.py +0 -0
- {classifyre_cli-0.6.11 → classifyre_cli-0.6.12}/tests/detectors/conftest.py +0 -0
- {classifyre_cli-0.6.11 → classifyre_cli-0.6.12}/tests/detectors/content/__init__.py +0 -0
- {classifyre_cli-0.6.11 → classifyre_cli-0.6.12}/tests/detectors/custom/__init__.py +0 -0
- {classifyre_cli-0.6.11 → classifyre_cli-0.6.12}/tests/detectors/custom/conftest.py +0 -0
- {classifyre_cli-0.6.11 → classifyre_cli-0.6.12}/tests/detectors/custom/test_decision_runner.py +0 -0
- {classifyre_cli-0.6.11 → classifyre_cli-0.6.12}/tests/detectors/custom/test_gliner2_classification.py +0 -0
- {classifyre_cli-0.6.11 → classifyre_cli-0.6.12}/tests/detectors/custom/test_invoice_extraction.py +0 -0
- {classifyre_cli-0.6.11 → classifyre_cli-0.6.12}/tests/detectors/custom/test_llm_runner.py +0 -0
- {classifyre_cli-0.6.11 → classifyre_cli-0.6.12}/tests/detectors/custom/test_pipeline_integration.py +0 -0
- {classifyre_cli-0.6.11 → classifyre_cli-0.6.12}/tests/detectors/custom/test_regex_runner.py +0 -0
- {classifyre_cli-0.6.11 → classifyre_cli-0.6.12}/tests/detectors/custom/test_runner_hardening.py +0 -0
- {classifyre_cli-0.6.11 → classifyre_cli-0.6.12}/tests/detectors/custom/test_transformer_runners.py +0 -0
- {classifyre_cli-0.6.11 → classifyre_cli-0.6.12}/tests/detectors/pii/__init__.py +0 -0
- {classifyre_cli-0.6.11 → classifyre_cli-0.6.12}/tests/detectors/pii/conftest.py +0 -0
- {classifyre_cli-0.6.11 → classifyre_cli-0.6.12}/tests/detectors/pii/sample_invoice.pdf +0 -0
- {classifyre_cli-0.6.11 → classifyre_cli-0.6.12}/tests/detectors/pii/test_pii_chunking.py +0 -0
- {classifyre_cli-0.6.11 → classifyre_cli-0.6.12}/tests/detectors/pii/test_pii_detector.py +0 -0
- {classifyre_cli-0.6.11 → classifyre_cli-0.6.12}/tests/detectors/pii/test_pii_detector_extended.py +0 -0
- {classifyre_cli-0.6.11 → classifyre_cli-0.6.12}/tests/detectors/pii/test_pii_regional_entities.py +0 -0
- {classifyre_cli-0.6.11 → classifyre_cli-0.6.12}/tests/detectors/pii/test_pii_severity.py +0 -0
- {classifyre_cli-0.6.11 → classifyre_cli-0.6.12}/tests/detectors/secrets/__init__.py +0 -0
- {classifyre_cli-0.6.11 → classifyre_cli-0.6.12}/tests/detectors/secrets/test_secrets_detector.py +0 -0
- {classifyre_cli-0.6.11 → classifyre_cli-0.6.12}/tests/detectors/secrets/test_secrets_detector_extended.py +0 -0
- {classifyre_cli-0.6.11 → classifyre_cli-0.6.12}/tests/detectors/test_base_detector.py +0 -0
- {classifyre_cli-0.6.11 → classifyre_cli-0.6.12}/tests/detectors/test_custom_detector_examples_runtime.py +0 -0
- {classifyre_cli-0.6.11 → classifyre_cli-0.6.12}/tests/detectors/test_custom_detector_session.py +0 -0
- {classifyre_cli-0.6.11 → classifyre_cli-0.6.12}/tests/detectors/test_detector_catalog_commercial.py +0 -0
- {classifyre_cli-0.6.11 → classifyre_cli-0.6.12}/tests/detectors/test_detector_pipeline_types.py +0 -0
- {classifyre_cli-0.6.11 → classifyre_cli-0.6.12}/tests/detectors/test_detector_schema_examples.py +0 -0
- {classifyre_cli-0.6.11 → classifyre_cli-0.6.12}/tests/detectors/test_detector_types.py +0 -0
- {classifyre_cli-0.6.11 → classifyre_cli-0.6.12}/tests/detectors/test_phase2_detectors.py +0 -0
- {classifyre_cli-0.6.11 → classifyre_cli-0.6.12}/tests/detectors/test_registry.py +0 -0
- {classifyre_cli-0.6.11 → classifyre_cli-0.6.12}/tests/detectors/test_tag_runner.py +0 -0
- {classifyre_cli-0.6.11 → classifyre_cli-0.6.12}/tests/detectors/threat/__init__.py +0 -0
- {classifyre_cli-0.6.11 → classifyre_cli-0.6.12}/tests/detectors/threat/test_code_security_detector.py +0 -0
- {classifyre_cli-0.6.11 → classifyre_cli-0.6.12}/tests/detectors/threat/test_yara_detector.py +0 -0
- {classifyre_cli-0.6.11 → classifyre_cli-0.6.12}/tests/integration/test_wordpress_broken_links_detector.py +0 -0
- {classifyre_cli-0.6.11 → classifyre_cli-0.6.12}/tests/integration/test_wordpress_links_assets.py +0 -0
- {classifyre_cli-0.6.11 → classifyre_cli-0.6.12}/tests/pipeline/test_detector_breaker.py +0 -0
- {classifyre_cli-0.6.11 → classifyre_cli-0.6.12}/tests/pipeline/test_detector_outcomes.py +0 -0
- {classifyre_cli-0.6.11 → classifyre_cli-0.6.12}/tests/pipeline/test_detector_pipeline.py +0 -0
- {classifyre_cli-0.6.11 → classifyre_cli-0.6.12}/tests/pipeline/test_worker_pool.py +0 -0
- {classifyre_cli-0.6.11 → classifyre_cli-0.6.12}/tests/test_arrow_ipc_payloads.py +0 -0
- {classifyre_cli-0.6.11 → classifyre_cli-0.6.12}/tests/test_assets_metadata_catalog.py +0 -0
- {classifyre_cli-0.6.11 → classifyre_cli-0.6.12}/tests/test_augmentation_contract.py +0 -0
- {classifyre_cli-0.6.11 → classifyre_cli-0.6.12}/tests/test_augmentation_edges.py +0 -0
- {classifyre_cli-0.6.11 → classifyre_cli-0.6.12}/tests/test_augmentation_patch.py +0 -0
- {classifyre_cli-0.6.11 → classifyre_cli-0.6.12}/tests/test_augmentation_scan_cache.py +0 -0
- {classifyre_cli-0.6.11 → classifyre_cli-0.6.12}/tests/test_augmentation_sdk.py +0 -0
- {classifyre_cli-0.6.11 → classifyre_cli-0.6.12}/tests/test_augmentation_session.py +0 -0
- {classifyre_cli-0.6.11 → classifyre_cli-0.6.12}/tests/test_augmentation_tags_end_to_end.py +0 -0
- {classifyre_cli-0.6.11 → classifyre_cli-0.6.12}/tests/test_automatic_new_first.py +0 -0
- {classifyre_cli-0.6.11 → classifyre_cli-0.6.12}/tests/test_azure_blob_storage_source.py +0 -0
- {classifyre_cli-0.6.11 → classifyre_cli-0.6.12}/tests/test_base_source_attachment.py +0 -0
- {classifyre_cli-0.6.11 → classifyre_cli-0.6.12}/tests/test_base_source_sampling.py +0 -0
- {classifyre_cli-0.6.11 → classifyre_cli-0.6.12}/tests/test_byte_valued_fields.py +0 -0
- {classifyre_cli-0.6.11 → classifyre_cli-0.6.12}/tests/test_config.py +0 -0
- {classifyre_cli-0.6.11 → classifyre_cli-0.6.12}/tests/test_confluence_source.py +0 -0
- {classifyre_cli-0.6.11 → classifyre_cli-0.6.12}/tests/test_custom_detector_end_to_end.py +0 -0
- {classifyre_cli-0.6.11 → classifyre_cli-0.6.12}/tests/test_custom_detector_evaluation.py +0 -0
- {classifyre_cli-0.6.11 → classifyre_cli-0.6.12}/tests/test_custom_detector_notebook.py +0 -0
- {classifyre_cli-0.6.11 → classifyre_cli-0.6.12}/tests/test_custom_detector_templates.py +0 -0
- {classifyre_cli-0.6.11 → classifyre_cli-0.6.12}/tests/test_custom_extractor.py +0 -0
- {classifyre_cli-0.6.11 → classifyre_cli-0.6.12}/tests/test_custom_source.py +0 -0
- {classifyre_cli-0.6.11 → classifyre_cli-0.6.12}/tests/test_custom_source_relationships.py +0 -0
- {classifyre_cli-0.6.11 → classifyre_cli-0.6.12}/tests/test_custom_source_tags_end_to_end.py +0 -0
- {classifyre_cli-0.6.11 → classifyre_cli-0.6.12}/tests/test_databricks_source.py +0 -0
- {classifyre_cli-0.6.11 → classifyre_cli-0.6.12}/tests/test_delta_lake_source.py +0 -0
- {classifyre_cli-0.6.11 → classifyre_cli-0.6.12}/tests/test_dependency_groups.py +0 -0
- {classifyre_cli-0.6.11 → classifyre_cli-0.6.12}/tests/test_detector_scope.py +0 -0
- {classifyre_cli-0.6.11 → classifyre_cli-0.6.12}/tests/test_dremio_source.py +0 -0
- {classifyre_cli-0.6.11 → classifyre_cli-0.6.12}/tests/test_dropbox_auth_command.py +0 -0
- {classifyre_cli-0.6.11 → classifyre_cli-0.6.12}/tests/test_dropbox_source.py +0 -0
- {classifyre_cli-0.6.11 → classifyre_cli-0.6.12}/tests/test_elasticsearch_source.py +0 -0
- {classifyre_cli-0.6.11 → classifyre_cli-0.6.12}/tests/test_email_source.py +0 -0
- {classifyre_cli-0.6.11 → classifyre_cli-0.6.12}/tests/test_file_evaluation_runner.py +0 -0
- {classifyre_cli-0.6.11 → classifyre_cli-0.6.12}/tests/test_git_source.py +0 -0
- {classifyre_cli-0.6.11 → classifyre_cli-0.6.12}/tests/test_google_cloud_storage_source.py +0 -0
- {classifyre_cli-0.6.11 → classifyre_cli-0.6.12}/tests/test_google_workspace_source.py +0 -0
- {classifyre_cli-0.6.11 → classifyre_cli-0.6.12}/tests/test_graph_edges.py +0 -0
- {classifyre_cli-0.6.11 → classifyre_cli-0.6.12}/tests/test_hashing.py +0 -0
- {classifyre_cli-0.6.11 → classifyre_cli-0.6.12}/tests/test_hive_source.py +0 -0
- {classifyre_cli-0.6.11 → classifyre_cli-0.6.12}/tests/test_hugging_face_source.py +0 -0
- {classifyre_cli-0.6.11 → classifyre_cli-0.6.12}/tests/test_iceberg_source.py +0 -0
- {classifyre_cli-0.6.11 → classifyre_cli-0.6.12}/tests/test_jira_source.py +0 -0
- {classifyre_cli-0.6.11 → classifyre_cli-0.6.12}/tests/test_kafka_source.py +0 -0
- {classifyre_cli-0.6.11 → classifyre_cli-0.6.12}/tests/test_lineage_loss_reporting.py +0 -0
- {classifyre_cli-0.6.11 → classifyre_cli-0.6.12}/tests/test_local_folder_source.py +0 -0
- {classifyre_cli-0.6.11 → classifyre_cli-0.6.12}/tests/test_main.py +0 -0
- {classifyre_cli-0.6.11 → classifyre_cli-0.6.12}/tests/test_meilisearch_source.py +0 -0
- {classifyre_cli-0.6.11 → classifyre_cli-0.6.12}/tests/test_microsoft_365_source.py +0 -0
- {classifyre_cli-0.6.11 → classifyre_cli-0.6.12}/tests/test_mongodb_source.py +0 -0
- {classifyre_cli-0.6.11 → classifyre_cli-0.6.12}/tests/test_mssql_source.py +0 -0
- {classifyre_cli-0.6.11 → classifyre_cli-0.6.12}/tests/test_mysql_source.py +0 -0
- {classifyre_cli-0.6.11 → classifyre_cli-0.6.12}/tests/test_neo4j_source.py +0 -0
- {classifyre_cli-0.6.11 → classifyre_cli-0.6.12}/tests/test_notebook_asset_entity.py +0 -0
- {classifyre_cli-0.6.11 → classifyre_cli-0.6.12}/tests/test_notebook_asset_extract.py +0 -0
- {classifyre_cli-0.6.11 → classifyre_cli-0.6.12}/tests/test_notebook_asset_tags.py +0 -0
- {classifyre_cli-0.6.11 → classifyre_cli-0.6.12}/tests/test_notebook_cohort.py +0 -0
- {classifyre_cli-0.6.11 → classifyre_cli-0.6.12}/tests/test_notebook_execute.py +0 -0
- {classifyre_cli-0.6.11 → classifyre_cli-0.6.12}/tests/test_notebook_files.py +0 -0
- {classifyre_cli-0.6.11 → classifyre_cli-0.6.12}/tests/test_notebook_groups.py +0 -0
- {classifyre_cli-0.6.11 → classifyre_cli-0.6.12}/tests/test_notebook_log_levels.py +0 -0
- {classifyre_cli-0.6.11 → classifyre_cli-0.6.12}/tests/test_notebook_packages.py +0 -0
- {classifyre_cli-0.6.11 → classifyre_cli-0.6.12}/tests/test_notebook_query_assets.py +0 -0
- {classifyre_cli-0.6.11 → classifyre_cli-0.6.12}/tests/test_notebook_sdk_exports.py +0 -0
- {classifyre_cli-0.6.11 → classifyre_cli-0.6.12}/tests/test_notebook_serialize.py +0 -0
- {classifyre_cli-0.6.11 → classifyre_cli-0.6.12}/tests/test_notebook_templates.py +0 -0
- {classifyre_cli-0.6.11 → classifyre_cli-0.6.12}/tests/test_notion_source.py +0 -0
- {classifyre_cli-0.6.11 → classifyre_cli-0.6.12}/tests/test_object_storage_download_efficiency.py +0 -0
- {classifyre_cli-0.6.11 → classifyre_cli-0.6.12}/tests/test_opensearch_source.py +0 -0
- {classifyre_cli-0.6.11 → classifyre_cli-0.6.12}/tests/test_oracle_source.py +0 -0
- {classifyre_cli-0.6.11 → classifyre_cli-0.6.12}/tests/test_outputs.py +0 -0
- {classifyre_cli-0.6.11 → classifyre_cli-0.6.12}/tests/test_payload_window.py +0 -0
- {classifyre_cli-0.6.11 → classifyre_cli-0.6.12}/tests/test_payload_window_parquet.py +0 -0
- {classifyre_cli-0.6.11 → classifyre_cli-0.6.12}/tests/test_postgresql_source.py +0 -0
- {classifyre_cli-0.6.11 → classifyre_cli-0.6.12}/tests/test_powerbi_source.py +0 -0
- {classifyre_cli-0.6.11 → classifyre_cli-0.6.12}/tests/test_range_reading.py +0 -0
- {classifyre_cli-0.6.11 → classifyre_cli-0.6.12}/tests/test_recipe_normalizer.py +0 -0
- {classifyre_cli-0.6.11 → classifyre_cli-0.6.12}/tests/test_reddit_source.py +0 -0
- {classifyre_cli-0.6.11 → classifyre_cli-0.6.12}/tests/test_rest_backpressure.py +0 -0
- {classifyre_cli-0.6.11 → classifyre_cli-0.6.12}/tests/test_runtime_packages.py +0 -0
- {classifyre_cli-0.6.11 → classifyre_cli-0.6.12}/tests/test_s3_compatible_storage_source.py +0 -0
- {classifyre_cli-0.6.11 → classifyre_cli-0.6.12}/tests/test_sampling_automatic.py +0 -0
- {classifyre_cli-0.6.11 → classifyre_cli-0.6.12}/tests/test_sampling_cursor.py +0 -0
- {classifyre_cli-0.6.11 → classifyre_cli-0.6.12}/tests/test_sampling_frontier.py +0 -0
- {classifyre_cli-0.6.11 → classifyre_cli-0.6.12}/tests/test_sandbox_source.py +0 -0
- {classifyre_cli-0.6.11 → classifyre_cli-0.6.12}/tests/test_scan_cache.py +0 -0
- {classifyre_cli-0.6.11 → classifyre_cli-0.6.12}/tests/test_sdk_completions.py +0 -0
- {classifyre_cli-0.6.11 → classifyre_cli-0.6.12}/tests/test_secret_redaction.py +0 -0
- {classifyre_cli-0.6.11 → classifyre_cli-0.6.12}/tests/test_semantic_declarations.py +0 -0
- {classifyre_cli-0.6.11 → classifyre_cli-0.6.12}/tests/test_servicedesk_source.py +0 -0
- {classifyre_cli-0.6.11 → classifyre_cli-0.6.12}/tests/test_slack_source.py +0 -0
- {classifyre_cli-0.6.11 → classifyre_cli-0.6.12}/tests/test_snowflake_source.py +0 -0
- {classifyre_cli-0.6.11 → classifyre_cli-0.6.12}/tests/test_source_dependency_groups.py +0 -0
- {classifyre_cli-0.6.11 → classifyre_cli-0.6.12}/tests/test_source_strategy_conformance.py +0 -0
- {classifyre_cli-0.6.11 → classifyre_cli-0.6.12}/tests/test_sqlite_source.py +0 -0
- {classifyre_cli-0.6.11 → classifyre_cli-0.6.12}/tests/test_tableau_source.py +0 -0
- {classifyre_cli-0.6.11 → classifyre_cli-0.6.12}/tests/test_tabular_automatic_sampling.py +0 -0
- {classifyre_cli-0.6.11 → classifyre_cli-0.6.12}/tests/test_tabular_lineage.py +0 -0
- {classifyre_cli-0.6.11 → classifyre_cli-0.6.12}/tests/test_tabular_utils.py +0 -0
- {classifyre_cli-0.6.11 → classifyre_cli-0.6.12}/tests/test_torch_runtime.py +0 -0
- {classifyre_cli-0.6.11 → classifyre_cli-0.6.12}/tests/test_urn.py +0 -0
- {classifyre_cli-0.6.11 → classifyre_cli-0.6.12}/tests/test_uv_sync.py +0 -0
- {classifyre_cli-0.6.11 → classifyre_cli-0.6.12}/tests/test_wordpress_source.py +0 -0
- {classifyre_cli-0.6.11 → classifyre_cli-0.6.12}/tests/test_youtube_source.py +0 -0
- {classifyre_cli-0.6.11 → classifyre_cli-0.6.12}/tests/test_youtube_source_integration.py +0 -0
- {classifyre_cli-0.6.11 → classifyre_cli-0.6.12}/tests/utils/test_archive_extraction.py +0 -0
- {classifyre_cli-0.6.11 → classifyre_cli-0.6.12}/tests/utils/test_content_extraction.py +0 -0
- {classifyre_cli-0.6.11 → classifyre_cli-0.6.12}/tests/utils/test_embedded_files.py +0 -0
- {classifyre_cli-0.6.11 → classifyre_cli-0.6.12}/tests/utils/test_file_metadata.py +0 -0
- {classifyre_cli-0.6.11 → classifyre_cli-0.6.12}/tests/utils/test_file_parser.py +0 -0
- {classifyre_cli-0.6.11 → classifyre_cli-0.6.12}/tests/utils/test_file_to_images.py +0 -0
- {classifyre_cli-0.6.11 → classifyre_cli-0.6.12}/tests/utils/test_legacy_office.py +0 -0
- {classifyre_cli-0.6.11 → classifyre_cli-0.6.12}/tests/utils/test_payload.py +0 -0
- {classifyre_cli-0.6.11 → classifyre_cli-0.6.12}/tests/utils/test_transcription.py +0 -0
- {classifyre_cli-0.6.11 → classifyre_cli-0.6.12}/tests/utils/test_video_processing.py +0 -0
|
@@ -553,6 +553,24 @@ async def run_command_async(args: argparse.Namespace, recipe: dict[str, Any]) ->
|
|
|
553
553
|
await augmentation.startup()
|
|
554
554
|
|
|
555
555
|
scan_cache = ScanCache(recipe, source, payload_windows=payload_windows)
|
|
556
|
+
|
|
557
|
+
# What the workspace embeds besides text follows its
|
|
558
|
+
# embedding model, so the API says and the run asks once.
|
|
559
|
+
# The pipeline prepares that media; the scan cache uses the
|
|
560
|
+
# same plan to know which unchanged assets still owe theirs.
|
|
561
|
+
if hasattr(sink, "fetch_embedding_plan"):
|
|
562
|
+
from .pipeline.media_artifact import EmbeddingPlan
|
|
563
|
+
|
|
564
|
+
embedding_plan = EmbeddingPlan.from_payload(
|
|
565
|
+
await sink.fetch_embedding_plan()
|
|
566
|
+
)
|
|
567
|
+
if embedding_plan.collects_media:
|
|
568
|
+
logger.info(
|
|
569
|
+
"Preparing media for embedding: %s",
|
|
570
|
+
", ".join(sorted(embedding_plan.media)),
|
|
571
|
+
)
|
|
572
|
+
pipeline.set_embedding_plan(embedding_plan)
|
|
573
|
+
scan_cache.set_embedding_plan(embedding_plan)
|
|
556
574
|
if scan_cache.enabled:
|
|
557
575
|
logger.info(
|
|
558
576
|
"Scan cache active (verify=%s): unchanged assets and"
|
|
@@ -675,7 +693,13 @@ async def run_command_async(args: argparse.Namespace, recipe: dict[str, Any]) ->
|
|
|
675
693
|
source.evict_asset_cache(asset_hash)
|
|
676
694
|
return
|
|
677
695
|
|
|
678
|
-
if plan.
|
|
696
|
+
if plan.media_only:
|
|
697
|
+
logger.info(
|
|
698
|
+
"Cached %s, but %s",
|
|
699
|
+
asset.name,
|
|
700
|
+
plan.reason,
|
|
701
|
+
)
|
|
702
|
+
elif plan.mode == "partial":
|
|
679
703
|
logger.info(
|
|
680
704
|
"Partially cached %s: running %s",
|
|
681
705
|
asset.name,
|
|
@@ -761,6 +785,13 @@ async def run_command_async(args: argparse.Namespace, recipe: dict[str, Any]) ->
|
|
|
761
785
|
f_total, f_by_sev, f_by_det = _compute_findings_counts(
|
|
762
786
|
result.findings or []
|
|
763
787
|
)
|
|
788
|
+
if plan.media_only and plan.prior is not None:
|
|
789
|
+
# Read again only to send its picture or
|
|
790
|
+
# recording: no detector ran, so the counts
|
|
791
|
+
# of its last real scan are still its counts.
|
|
792
|
+
f_total = plan.prior.findings_total or 0
|
|
793
|
+
f_by_sev = plan.prior.findings_by_severity or {}
|
|
794
|
+
f_by_det = plan.prior.findings_by_detector or {}
|
|
764
795
|
payload = _asset_to_payload(result)
|
|
765
796
|
|
|
766
797
|
content_hash = plan.content_hash
|
|
@@ -785,6 +816,7 @@ async def run_command_async(args: argparse.Namespace, recipe: dict[str, Any]) ->
|
|
|
785
816
|
findings_total=f_total,
|
|
786
817
|
findings_by_severity=f_by_sev,
|
|
787
818
|
findings_by_detector=f_by_det,
|
|
819
|
+
media_state=scan_cache.media_state_for(asset),
|
|
788
820
|
)
|
|
789
821
|
if state is not None:
|
|
790
822
|
payload["scan_cache"] = state
|
|
@@ -347,13 +347,29 @@ class RestOutputSink:
|
|
|
347
347
|
)
|
|
348
348
|
|
|
349
349
|
async def emit_text_chunks(self, asset_hash: str, artifact: TextArtifact) -> None:
|
|
350
|
-
"""Send extracted text provenance; the API embeds it asynchronously.
|
|
350
|
+
"""Send extracted text provenance; the API embeds it asynchronously.
|
|
351
|
+
|
|
352
|
+
Prepared pictures and clips ride in the same request, because the API
|
|
353
|
+
replaces everything it holds for the asset in one step.
|
|
354
|
+
"""
|
|
351
355
|
source_id = self._require_source_id()
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
356
|
+
body: dict[str, Any] = {"assetHash": asset_hash, "chunks": artifact.chunks}
|
|
357
|
+
if artifact.media:
|
|
358
|
+
body["media"] = artifact.media
|
|
359
|
+
self._request_json("POST", f"/sources/{source_id}/embeddings/chunks", body)
|
|
360
|
+
|
|
361
|
+
async def fetch_embedding_plan(self) -> dict[str, Any]:
|
|
362
|
+
"""What this workspace wants prepared for embedding besides text.
|
|
363
|
+
|
|
364
|
+
Asked once per run. An API that predates the endpoint answers 404, and
|
|
365
|
+
any failure here means the same thing: prepare no media, and embed the
|
|
366
|
+
text as every scan before this did.
|
|
367
|
+
"""
|
|
368
|
+
try:
|
|
369
|
+
return self._request_json("GET", "/embeddings/collection")
|
|
370
|
+
except Exception as exc:
|
|
371
|
+
logger.info("No embedding collection plan (%s); embedding text only", exc)
|
|
372
|
+
return {}
|
|
357
373
|
|
|
358
374
|
def _split_by_size(self, assets: list[dict[str, Any]]) -> list[list[dict[str, Any]]]:
|
|
359
375
|
"""Split assets into chunks that each stay under _MAX_BATCH_BYTES."""
|
|
@@ -29,6 +29,8 @@ from ..utils.file_parser import TextExtractionCoverageError, resolve_mime_type
|
|
|
29
29
|
from ..utils.redaction import Redactor, redact_generic, short_id_for_log
|
|
30
30
|
from .content_provider import ContentProvider
|
|
31
31
|
from .detector_breaker import DetectorBreaker
|
|
32
|
+
from .finding_context import attach_context
|
|
33
|
+
from .media_artifact import EmbeddingPlan, prepare_media
|
|
32
34
|
from .text_artifact import TextArtifact
|
|
33
35
|
from .worker_pool import DetectorWorkerPool, is_io_bound_detector
|
|
34
36
|
|
|
@@ -151,12 +153,60 @@ class DetectorPipeline:
|
|
|
151
153
|
# disabled here is disabled for this run and nothing longer.
|
|
152
154
|
self._breaker = DetectorBreaker()
|
|
153
155
|
self._text_artifacts: dict[str, TextArtifact] = {}
|
|
156
|
+
# What the workspace wants embedded besides text. Empty until the run
|
|
157
|
+
# asks the API, and empty for good on a text-only embedding model.
|
|
158
|
+
self._embedding_plan = EmbeddingPlan()
|
|
154
159
|
self._text_extraction_errors: dict[str, list[str]] = {}
|
|
155
160
|
self._text_extraction_statuses: dict[str, TextExtractionStatus] = {}
|
|
156
161
|
|
|
157
162
|
def take_text_artifact(self, asset_hash: str) -> TextArtifact | None:
|
|
158
163
|
return self._text_artifacts.pop(asset_hash, None)
|
|
159
164
|
|
|
165
|
+
def set_embedding_plan(self, plan: EmbeddingPlan) -> None:
|
|
166
|
+
"""Say which media, if any, this run prepares for embedding."""
|
|
167
|
+
self._embedding_plan = plan
|
|
168
|
+
|
|
169
|
+
async def _collect_media(self, asset: SingleAssetScanResults, artifact: TextArtifact) -> None:
|
|
170
|
+
"""Attach the asset's own picture or recording, prepared for embedding.
|
|
171
|
+
|
|
172
|
+
Only for an asset that IS a picture or a recording, and only when the
|
|
173
|
+
workspace's model embeds that kind of content. Best-effort throughout:
|
|
174
|
+
media adds a way to find the asset, and failing to prepare it leaves
|
|
175
|
+
the asset exactly as findable as it was before this existed.
|
|
176
|
+
"""
|
|
177
|
+
spec = self._embedding_plan.spec_for(asset.asset_type)
|
|
178
|
+
if spec is None:
|
|
179
|
+
return
|
|
180
|
+
for candidate in (asset.external_url, asset.hash):
|
|
181
|
+
asset_id = str(candidate or "").strip()
|
|
182
|
+
if not asset_id:
|
|
183
|
+
continue
|
|
184
|
+
try:
|
|
185
|
+
fetched = await self.content_provider.fetch_bytes(asset_id)
|
|
186
|
+
except Exception as exc:
|
|
187
|
+
logger.warning(
|
|
188
|
+
"Could not read %s to prepare its %s for embedding: %s",
|
|
189
|
+
asset.name,
|
|
190
|
+
spec.modality,
|
|
191
|
+
redact_generic(self._redactor.redact(str(exc))),
|
|
192
|
+
)
|
|
193
|
+
return
|
|
194
|
+
if not fetched or not fetched[0]:
|
|
195
|
+
continue
|
|
196
|
+
# Decoding and re-encoding is CPU work; keep it off the event loop
|
|
197
|
+
# the rest of the scan is running on.
|
|
198
|
+
items = await asyncio.to_thread(prepare_media, fetched[0], spec)
|
|
199
|
+
for mime_type, data in items:
|
|
200
|
+
artifact.add_media(spec.modality, mime_type, data)
|
|
201
|
+
if items:
|
|
202
|
+
logger.info(
|
|
203
|
+
"Prepared %d %s item(s) of %s for embedding",
|
|
204
|
+
len(items),
|
|
205
|
+
spec.modality,
|
|
206
|
+
asset.name,
|
|
207
|
+
)
|
|
208
|
+
return
|
|
209
|
+
|
|
160
210
|
def _register_detector_info(self, detector: BaseDetector, info: _DetectorInfo) -> None:
|
|
161
211
|
self._detector_info[id(detector)] = info
|
|
162
212
|
|
|
@@ -401,6 +451,9 @@ class DetectorPipeline:
|
|
|
401
451
|
scan_warnings.extend(code_warnings)
|
|
402
452
|
detector_types_run = self._merge_detector_types(detector_types_run, code_types_run)
|
|
403
453
|
|
|
454
|
+
if not records_only:
|
|
455
|
+
await self._collect_media(asset, artifact)
|
|
456
|
+
|
|
404
457
|
scan_duration = int((datetime.now(UTC) - scan_started).total_seconds() * 1000)
|
|
405
458
|
|
|
406
459
|
asset.findings = findings
|
|
@@ -1011,6 +1064,8 @@ class DetectorPipeline:
|
|
|
1011
1064
|
}
|
|
1012
1065
|
)
|
|
1013
1066
|
detector_findings.append(finding_with_meta)
|
|
1067
|
+
if isinstance(content, str):
|
|
1068
|
+
detector_findings = attach_context(detector_findings, content)
|
|
1014
1069
|
|
|
1015
1070
|
tripped = self._breaker.record(
|
|
1016
1071
|
breaker_key,
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
"""The text around a match.
|
|
2
|
+
|
|
3
|
+
A finding is a value and where it was found. What the value means is in the
|
|
4
|
+
words either side of it: "IBAN DE89..." in a payment reminder and the same
|
|
5
|
+
number in a list of test fixtures are two different pieces of evidence, and
|
|
6
|
+
nothing downstream can tell them apart from the value alone. The API ranks a
|
|
7
|
+
finding by its surrounding text, embeds the match together with it, and shows
|
|
8
|
+
it to the reviewer next to the match.
|
|
9
|
+
|
|
10
|
+
No detector returns that text: each answers with the value and, at most, a
|
|
11
|
+
position. So it is cut here, once, from the content the detectors were given,
|
|
12
|
+
for every finding that does not already carry it.
|
|
13
|
+
"""
|
|
14
|
+
|
|
15
|
+
from __future__ import annotations
|
|
16
|
+
|
|
17
|
+
import re
|
|
18
|
+
|
|
19
|
+
from ..models.generated_single_asset_scan_results import DetectionResult
|
|
20
|
+
|
|
21
|
+
# Characters kept on each side. The ranking treats 320 characters of evidence
|
|
22
|
+
# as a full context, so a match of ordinary length with both sides reaches it.
|
|
23
|
+
CONTEXT_CHARS = 160
|
|
24
|
+
|
|
25
|
+
# A match longer than this is already a passage (an LLM detector quoting a
|
|
26
|
+
# paragraph); text around it would be the next paragraph, not its context.
|
|
27
|
+
MAX_MATCH_CHARS = 600
|
|
28
|
+
|
|
29
|
+
_CHAR_RANGE = re.compile(r"character range (\d+)-(\d+)")
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def _locate(content: str, matched: str, finding: DetectionResult, start_at: int) -> int:
|
|
33
|
+
"""Offset of this finding's match in `content`, or -1."""
|
|
34
|
+
description = getattr(finding.location, "description", None) if finding.location else None
|
|
35
|
+
stated = _CHAR_RANGE.search(description) if isinstance(description, str) else None
|
|
36
|
+
if stated:
|
|
37
|
+
start, end = int(stated.group(1)), int(stated.group(2))
|
|
38
|
+
if content[start:end] == matched:
|
|
39
|
+
return start
|
|
40
|
+
found = content.find(matched, start_at)
|
|
41
|
+
return found if found >= 0 else content.find(matched)
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def _before(content: str, start: int) -> str | None:
|
|
45
|
+
cut = max(0, start - CONTEXT_CHARS)
|
|
46
|
+
text = content[cut:start]
|
|
47
|
+
# A window that opens in the middle of a word starts with half of it.
|
|
48
|
+
if cut > 0 and not content[cut - 1].isspace() and text[:1].strip():
|
|
49
|
+
parts = text.split(None, 1)
|
|
50
|
+
text = parts[1] if len(parts) > 1 else ""
|
|
51
|
+
return " ".join(text.split()) or None
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def _after(content: str, end: int) -> str | None:
|
|
55
|
+
cut = min(len(content), end + CONTEXT_CHARS)
|
|
56
|
+
text = content[end:cut]
|
|
57
|
+
if cut < len(content) and not content[cut].isspace() and text[-1:].strip():
|
|
58
|
+
parts = text.rsplit(None, 1)
|
|
59
|
+
text = parts[0] if len(parts) > 1 else ""
|
|
60
|
+
return " ".join(text.split()) or None
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def attach_context(findings: list[DetectionResult], content: str) -> list[DetectionResult]:
|
|
64
|
+
"""`findings` with the text around each match filled in where it was missing.
|
|
65
|
+
|
|
66
|
+
A value that occurs several times is matched to its occurrences in order,
|
|
67
|
+
so two findings of one value do not both get the first one's surroundings.
|
|
68
|
+
A match that cannot be found in the content (a detector that reports a
|
|
69
|
+
normalised value, or a verdict rather than a quotation) is left as it is.
|
|
70
|
+
"""
|
|
71
|
+
if not content:
|
|
72
|
+
return findings
|
|
73
|
+
cursor: dict[str, int] = {}
|
|
74
|
+
out: list[DetectionResult] = []
|
|
75
|
+
for finding in findings:
|
|
76
|
+
matched = finding.matched_content
|
|
77
|
+
if (
|
|
78
|
+
finding.context_before
|
|
79
|
+
or finding.context_after
|
|
80
|
+
or not matched
|
|
81
|
+
or len(matched) > MAX_MATCH_CHARS
|
|
82
|
+
):
|
|
83
|
+
out.append(finding)
|
|
84
|
+
continue
|
|
85
|
+
start = _locate(content, matched, finding, cursor.get(matched, 0))
|
|
86
|
+
if start < 0:
|
|
87
|
+
out.append(finding)
|
|
88
|
+
continue
|
|
89
|
+
end = start + len(matched)
|
|
90
|
+
cursor[matched] = end
|
|
91
|
+
out.append(
|
|
92
|
+
finding.model_copy(
|
|
93
|
+
update={
|
|
94
|
+
"context_before": _before(content, start),
|
|
95
|
+
"context_after": _after(content, end),
|
|
96
|
+
}
|
|
97
|
+
)
|
|
98
|
+
)
|
|
99
|
+
return out
|
|
@@ -0,0 +1,262 @@
|
|
|
1
|
+
"""Pictures and sound prepared for embedding as what they are.
|
|
2
|
+
|
|
3
|
+
Text read out of an asset (OCR, transcription) is embedded as text. A model
|
|
4
|
+
that also embeds pictures or sound can find an asset by what it shows or how it
|
|
5
|
+
sounds, which is the only way to find a photograph with no writing in it. This
|
|
6
|
+
module prepares that media the way the API asks for it: one small JPEG per
|
|
7
|
+
picture, mono 16 kHz WAV clips per recording. The API stores the prepared copy,
|
|
8
|
+
because a later rebuild re-embeds from it and the scan will not send it again.
|
|
9
|
+
|
|
10
|
+
What to prepare is the API's decision (``GET /embeddings/collection``), since
|
|
11
|
+
it follows the workspace's embedding model. With nothing asked for, nothing
|
|
12
|
+
here runs.
|
|
13
|
+
"""
|
|
14
|
+
|
|
15
|
+
from __future__ import annotations
|
|
16
|
+
|
|
17
|
+
import io
|
|
18
|
+
import logging
|
|
19
|
+
import wave
|
|
20
|
+
from dataclasses import dataclass, field
|
|
21
|
+
from typing import Any
|
|
22
|
+
|
|
23
|
+
from ..detectors.dependencies import MissingDependencyError, require_module
|
|
24
|
+
|
|
25
|
+
logger = logging.getLogger(__name__)
|
|
26
|
+
|
|
27
|
+
# uv groups that carry Pillow, and PyAV (which arrives with faster-whisper). A
|
|
28
|
+
# picture or a recording has normally been through OCR or transcription by the
|
|
29
|
+
# time it gets here, so these are already installed; a scan that somehow is
|
|
30
|
+
# not installs the first one on demand.
|
|
31
|
+
_IMAGE_UV_GROUPS = ["file-processing", "content", "llm", "custom"]
|
|
32
|
+
_AUDIO_UV_GROUPS = ["transcription"]
|
|
33
|
+
|
|
34
|
+
# Scan-cache key under which an asset's state records that its media was
|
|
35
|
+
# collected. Lives beside the detector fingerprints because it answers the same
|
|
36
|
+
# question for the same reason: was this work done for this asset under the
|
|
37
|
+
# rules in force now. The API names the key with the plan (it is the one that
|
|
38
|
+
# takes the record back when the media is deleted); this is the same name, for
|
|
39
|
+
# an API that does not send one.
|
|
40
|
+
MEDIA_CACHE_KEY_PREFIX = "EMBED_MEDIA::"
|
|
41
|
+
|
|
42
|
+
# Shortest clip worth sending: of a whole recording, and of what is left over
|
|
43
|
+
# after the full clips of a longer one.
|
|
44
|
+
_MIN_CLIP_SECONDS = 1
|
|
45
|
+
_MIN_TAIL_SECONDS = 5
|
|
46
|
+
|
|
47
|
+
# Asset types whose own bytes are a picture or a recording.
|
|
48
|
+
_MODALITY_BY_ASSET_TYPE = {"IMAGE": "image", "AUDIO": "audio"}
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
@dataclass(frozen=True)
|
|
52
|
+
class MediaSpec:
|
|
53
|
+
"""How one kind of media is prepared."""
|
|
54
|
+
|
|
55
|
+
modality: str
|
|
56
|
+
mime_type: str
|
|
57
|
+
max_items: int = 1
|
|
58
|
+
max_edge: int = 768
|
|
59
|
+
quality: int = 85
|
|
60
|
+
sample_rate: int = 16_000
|
|
61
|
+
clip_seconds: int = 30
|
|
62
|
+
cache_key: str = ""
|
|
63
|
+
# Changes when the way this kind of media is prepared changes.
|
|
64
|
+
fingerprint: str = ""
|
|
65
|
+
|
|
66
|
+
@classmethod
|
|
67
|
+
def from_payload(cls, raw: Any) -> MediaSpec | None:
|
|
68
|
+
if not isinstance(raw, dict):
|
|
69
|
+
return None
|
|
70
|
+
modality = str(raw.get("modality") or "").strip().lower()
|
|
71
|
+
if modality not in {"image", "audio"}:
|
|
72
|
+
return None
|
|
73
|
+
|
|
74
|
+
def number(key: str, default: int, minimum: int) -> int:
|
|
75
|
+
value = raw.get(key)
|
|
76
|
+
return max(minimum, int(value)) if isinstance(value, int | float) else default
|
|
77
|
+
|
|
78
|
+
return cls(
|
|
79
|
+
modality=modality,
|
|
80
|
+
mime_type=str(
|
|
81
|
+
raw.get("mimeType") or ("image/jpeg" if modality == "image" else "audio/wav")
|
|
82
|
+
),
|
|
83
|
+
max_items=number("maxItems", 1, 1),
|
|
84
|
+
max_edge=number("maxEdge", 768, 32),
|
|
85
|
+
quality=min(100, number("quality", 85, 1)),
|
|
86
|
+
sample_rate=number("sampleRate", 16_000, 8_000),
|
|
87
|
+
clip_seconds=number("clipSeconds", 30, 1),
|
|
88
|
+
cache_key=str(raw.get("cacheKey") or f"{MEDIA_CACHE_KEY_PREFIX}{modality}"),
|
|
89
|
+
fingerprint=str(raw.get("fingerprint") or ""),
|
|
90
|
+
)
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
@dataclass(frozen=True)
|
|
94
|
+
class EmbeddingPlan:
|
|
95
|
+
"""What this workspace wants sent for embedding."""
|
|
96
|
+
|
|
97
|
+
media: dict[str, MediaSpec] = field(default_factory=dict)
|
|
98
|
+
|
|
99
|
+
@classmethod
|
|
100
|
+
def from_payload(cls, raw: Any) -> EmbeddingPlan:
|
|
101
|
+
if not isinstance(raw, dict):
|
|
102
|
+
return cls()
|
|
103
|
+
specs: dict[str, MediaSpec] = {}
|
|
104
|
+
entries = raw.get("media")
|
|
105
|
+
for entry in entries if isinstance(entries, list) else []:
|
|
106
|
+
spec = MediaSpec.from_payload(entry)
|
|
107
|
+
if spec is not None:
|
|
108
|
+
specs[spec.modality] = spec
|
|
109
|
+
return cls(media=specs)
|
|
110
|
+
|
|
111
|
+
@property
|
|
112
|
+
def collects_media(self) -> bool:
|
|
113
|
+
return bool(self.media)
|
|
114
|
+
|
|
115
|
+
def spec_for(self, asset_type: Any) -> MediaSpec | None:
|
|
116
|
+
"""The spec for an asset whose own bytes are media, if it is collected."""
|
|
117
|
+
name = str(getattr(asset_type, "value", asset_type) or "").upper()
|
|
118
|
+
modality = _MODALITY_BY_ASSET_TYPE.get(name)
|
|
119
|
+
return self.media.get(modality) if modality else None
|
|
120
|
+
|
|
121
|
+
def cache_entry_for(self, asset_type: Any) -> tuple[str, str] | None:
|
|
122
|
+
"""Scan-cache (key, fingerprint) proving this asset's media was sent.
|
|
123
|
+
|
|
124
|
+
Per kind of media, so that starting to embed sound does not make every
|
|
125
|
+
picture read as not yet sent.
|
|
126
|
+
"""
|
|
127
|
+
spec = self.spec_for(asset_type)
|
|
128
|
+
if spec is None:
|
|
129
|
+
return None
|
|
130
|
+
key = spec.cache_key or f"{MEDIA_CACHE_KEY_PREFIX}{spec.modality}"
|
|
131
|
+
return key, spec.fingerprint or "1"
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
def prepare_image(raw: bytes, spec: MediaSpec) -> bytes | None:
|
|
135
|
+
"""Re-encode a picture as an RGB JPEG no larger than ``spec.max_edge``.
|
|
136
|
+
|
|
137
|
+
Vision encoders work at a few hundred pixels a side, so a larger copy is
|
|
138
|
+
only storage. Returns None when the bytes are not a picture Pillow can
|
|
139
|
+
read, or Pillow is not available; the asset is then embedded by its text
|
|
140
|
+
alone, as it would have been anyway.
|
|
141
|
+
"""
|
|
142
|
+
if not raw:
|
|
143
|
+
return None
|
|
144
|
+
try:
|
|
145
|
+
pil = require_module("PIL.Image", "media embedding", _IMAGE_UV_GROUPS)
|
|
146
|
+
ops = require_module("PIL.ImageOps", "media embedding", _IMAGE_UV_GROUPS)
|
|
147
|
+
except MissingDependencyError as exc:
|
|
148
|
+
logger.warning("Cannot prepare a picture for embedding: %s", exc)
|
|
149
|
+
return None
|
|
150
|
+
try:
|
|
151
|
+
# HEIC (every recent phone photograph) is read through a plug-in that
|
|
152
|
+
# has to be registered; without it Pillow does not know the format.
|
|
153
|
+
try:
|
|
154
|
+
import pillow_heif # type: ignore[import-not-found]
|
|
155
|
+
|
|
156
|
+
pillow_heif.register_heif_opener()
|
|
157
|
+
except Exception:
|
|
158
|
+
pass
|
|
159
|
+
with pil.open(io.BytesIO(raw)) as image:
|
|
160
|
+
# A phone photograph is stored sideways with a tag saying which way
|
|
161
|
+
# is up; the encoder only sees pixels.
|
|
162
|
+
upright = ops.exif_transpose(image) or image
|
|
163
|
+
# Flatten transparency onto white: JPEG has no alpha, and dropping
|
|
164
|
+
# the channel turns a transparent background black.
|
|
165
|
+
if upright.mode in {"RGBA", "LA"} or "transparency" in upright.info:
|
|
166
|
+
rgba = upright.convert("RGBA")
|
|
167
|
+
canvas = pil.new("RGB", rgba.size, (255, 255, 255))
|
|
168
|
+
canvas.paste(rgba, mask=rgba.getchannel("A"))
|
|
169
|
+
rgb = canvas
|
|
170
|
+
else:
|
|
171
|
+
rgb = upright.convert("RGB")
|
|
172
|
+
rgb.thumbnail((spec.max_edge, spec.max_edge), pil.Resampling.LANCZOS)
|
|
173
|
+
buffer = io.BytesIO()
|
|
174
|
+
rgb.save(buffer, format="JPEG", quality=spec.quality, optimize=True)
|
|
175
|
+
return buffer.getvalue()
|
|
176
|
+
except Exception as exc:
|
|
177
|
+
logger.warning("Could not prepare a picture for embedding: %s", exc)
|
|
178
|
+
return None
|
|
179
|
+
|
|
180
|
+
|
|
181
|
+
def _pcm_to_wav(pcm: bytes, sample_rate: int) -> bytes:
|
|
182
|
+
buffer = io.BytesIO()
|
|
183
|
+
with wave.open(buffer, "wb") as out:
|
|
184
|
+
out.setnchannels(1)
|
|
185
|
+
out.setsampwidth(2)
|
|
186
|
+
out.setframerate(sample_rate)
|
|
187
|
+
out.writeframes(pcm)
|
|
188
|
+
return buffer.getvalue()
|
|
189
|
+
|
|
190
|
+
|
|
191
|
+
def prepare_audio_clips(raw: bytes, spec: MediaSpec) -> list[bytes]:
|
|
192
|
+
"""Decode a recording to mono 16-bit WAV clips at ``spec.sample_rate``.
|
|
193
|
+
|
|
194
|
+
At most ``spec.max_items`` clips of ``spec.clip_seconds`` each, from the
|
|
195
|
+
start: the opening minutes say what a recording is, and every clip is a
|
|
196
|
+
megabyte kept. Decoding stops once enough has been read, so a long file is
|
|
197
|
+
not decoded to its end. Returns [] when the bytes hold no audio PyAV can
|
|
198
|
+
read, or PyAV is not available.
|
|
199
|
+
"""
|
|
200
|
+
if not raw:
|
|
201
|
+
return []
|
|
202
|
+
try:
|
|
203
|
+
pyav = require_module("av", "media embedding", _AUDIO_UV_GROUPS)
|
|
204
|
+
except MissingDependencyError as exc:
|
|
205
|
+
logger.warning("Cannot prepare a recording for embedding: %s", exc)
|
|
206
|
+
return []
|
|
207
|
+
|
|
208
|
+
clip_bytes = spec.sample_rate * spec.clip_seconds * 2
|
|
209
|
+
wanted = clip_bytes * spec.max_items
|
|
210
|
+
pcm = bytearray()
|
|
211
|
+
container = None
|
|
212
|
+
try:
|
|
213
|
+
container = pyav.open(io.BytesIO(raw), metadata_errors="ignore")
|
|
214
|
+
streams = [stream for stream in container.streams if stream.type == "audio"]
|
|
215
|
+
if not streams:
|
|
216
|
+
return []
|
|
217
|
+
resampler = pyav.audio.resampler.AudioResampler(
|
|
218
|
+
format="s16", layout="mono", rate=spec.sample_rate
|
|
219
|
+
)
|
|
220
|
+
|
|
221
|
+
def drain(frames: Any) -> None:
|
|
222
|
+
for frame in frames if isinstance(frames, list) else [frames]:
|
|
223
|
+
if frame is not None:
|
|
224
|
+
pcm.extend(bytes(frame.planes[0])[: frame.samples * 2])
|
|
225
|
+
|
|
226
|
+
for frame in container.decode(streams[0]):
|
|
227
|
+
drain(resampler.resample(frame))
|
|
228
|
+
if len(pcm) >= wanted:
|
|
229
|
+
break
|
|
230
|
+
else:
|
|
231
|
+
# Only a file read to its end has a resampler tail worth flushing.
|
|
232
|
+
try:
|
|
233
|
+
drain(resampler.resample(None))
|
|
234
|
+
except Exception:
|
|
235
|
+
pass
|
|
236
|
+
except Exception as exc:
|
|
237
|
+
logger.warning("Could not prepare a recording for embedding: %s", exc)
|
|
238
|
+
return []
|
|
239
|
+
finally:
|
|
240
|
+
if container is not None:
|
|
241
|
+
container.close()
|
|
242
|
+
|
|
243
|
+
clips: list[bytes] = []
|
|
244
|
+
for start in range(0, min(len(pcm), wanted), clip_bytes):
|
|
245
|
+
chunk = bytes(pcm[start : start + clip_bytes])
|
|
246
|
+
# A short recording is sent whole: a second of sound can be all there
|
|
247
|
+
# is. The tail of a long one is different, since the clips before it
|
|
248
|
+
# already say what the recording is and a few seconds more add a vector
|
|
249
|
+
# of their own for almost nothing.
|
|
250
|
+
leftover = bool(clips) and len(chunk) < clip_bytes
|
|
251
|
+
shortest = _MIN_TAIL_SECONDS if leftover else _MIN_CLIP_SECONDS
|
|
252
|
+
if len(chunk) >= spec.sample_rate * 2 * shortest:
|
|
253
|
+
clips.append(_pcm_to_wav(chunk, spec.sample_rate))
|
|
254
|
+
return clips
|
|
255
|
+
|
|
256
|
+
|
|
257
|
+
def prepare_media(raw: bytes, spec: MediaSpec) -> list[tuple[str, bytes]]:
|
|
258
|
+
"""Prepared (mime type, bytes) items for one asset under ``spec``."""
|
|
259
|
+
if spec.modality == "image":
|
|
260
|
+
image = prepare_image(raw, spec)
|
|
261
|
+
return [(spec.mime_type, image)] if image else []
|
|
262
|
+
return [(spec.mime_type, clip) for clip in prepare_audio_clips(raw, spec)]
|