classifyre-cli 0.4.51__tar.gz → 0.4.53__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.4.53/.turbo/turbo-build.log +3 -0
- {classifyre_cli-0.4.51 → classifyre_cli-0.4.53}/PKG-INFO +1 -1
- {classifyre_cli-0.4.51 → classifyre_cli-0.4.53}/package.json +1 -1
- {classifyre_cli-0.4.51 → classifyre_cli-0.4.53}/pyproject.toml +13 -1
- {classifyre_cli-0.4.51 → classifyre_cli-0.4.53}/src/models/generated_input.py +0 -12
- {classifyre_cli-0.4.51 → classifyre_cli-0.4.53}/src/pipeline/detector_pipeline.py +7 -10
- {classifyre_cli-0.4.51 → classifyre_cli-0.4.53}/src/sources/base.py +0 -16
- {classifyre_cli-0.4.51 → classifyre_cli-0.4.53}/src/sources/object_storage/base.py +5 -10
- {classifyre_cli-0.4.51 → classifyre_cli-0.4.53}/src/sources/youtube/source.py +131 -75
- {classifyre_cli-0.4.51 → classifyre_cli-0.4.53}/src/utils/dependency_groups.py +0 -7
- {classifyre_cli-0.4.51 → classifyre_cli-0.4.53}/src/utils/file_parser.py +59 -41
- {classifyre_cli-0.4.51 → classifyre_cli-0.4.53}/src/utils/transcription.py +161 -58
- classifyre_cli-0.4.53/src/utils/video_processing.py +299 -0
- {classifyre_cli-0.4.51 → classifyre_cli-0.4.53}/tests/pipeline/test_detector_pipeline.py +12 -2
- {classifyre_cli-0.4.51 → classifyre_cli-0.4.53}/tests/test_dependency_groups.py +7 -13
- {classifyre_cli-0.4.51 → classifyre_cli-0.4.53}/tests/test_jira_source.py +7 -7
- {classifyre_cli-0.4.51 → classifyre_cli-0.4.53}/tests/test_s3_compatible_storage_source.py +2 -10
- {classifyre_cli-0.4.51 → classifyre_cli-0.4.53}/tests/test_youtube_source.py +69 -13
- {classifyre_cli-0.4.51 → classifyre_cli-0.4.53}/tests/utils/test_file_parser.py +39 -64
- {classifyre_cli-0.4.51 → classifyre_cli-0.4.53}/tests/utils/test_transcription.py +26 -1
- classifyre_cli-0.4.53/tests/utils/test_video_processing.py +141 -0
- {classifyre_cli-0.4.51 → classifyre_cli-0.4.53}/uv.lock +81 -85
- classifyre_cli-0.4.51/.turbo/turbo-build.log +0 -3
- {classifyre_cli-0.4.51 → classifyre_cli-0.4.53}/.gitignore +0 -0
- {classifyre_cli-0.4.51 → classifyre_cli-0.4.53}/.python-version +0 -0
- {classifyre_cli-0.4.51 → classifyre_cli-0.4.53}/README.md +0 -0
- {classifyre_cli-0.4.51 → classifyre_cli-0.4.53}/main.py +0 -0
- {classifyre_cli-0.4.51 → classifyre_cli-0.4.53}/scripts/generate_models.py +0 -0
- {classifyre_cli-0.4.51 → classifyre_cli-0.4.53}/src/__init__.py +0 -0
- {classifyre_cli-0.4.51 → classifyre_cli-0.4.53}/src/config.py +0 -0
- {classifyre_cli-0.4.51 → classifyre_cli-0.4.53}/src/detectors/__init__.py +0 -0
- {classifyre_cli-0.4.51 → classifyre_cli-0.4.53}/src/detectors/base.py +0 -0
- {classifyre_cli-0.4.51 → classifyre_cli-0.4.53}/src/detectors/broken_links/__init__.py +0 -0
- {classifyre_cli-0.4.51 → classifyre_cli-0.4.53}/src/detectors/broken_links/detector.py +0 -0
- {classifyre_cli-0.4.51 → classifyre_cli-0.4.53}/src/detectors/config.py +0 -0
- {classifyre_cli-0.4.51 → classifyre_cli-0.4.53}/src/detectors/content/__init__.py +0 -0
- {classifyre_cli-0.4.51 → classifyre_cli-0.4.53}/src/detectors/custom/__init__.py +0 -0
- {classifyre_cli-0.4.51 → classifyre_cli-0.4.53}/src/detectors/custom/detector.py +0 -0
- {classifyre_cli-0.4.51 → classifyre_cli-0.4.53}/src/detectors/custom/extractor.py +0 -0
- {classifyre_cli-0.4.51 → classifyre_cli-0.4.53}/src/detectors/custom/runners/__init__.py +0 -0
- {classifyre_cli-0.4.51 → classifyre_cli-0.4.53}/src/detectors/custom/runners/_base.py +0 -0
- {classifyre_cli-0.4.51 → classifyre_cli-0.4.53}/src/detectors/custom/runners/_factory.py +0 -0
- {classifyre_cli-0.4.51 → classifyre_cli-0.4.53}/src/detectors/custom/runners/_feature_extraction.py +0 -0
- {classifyre_cli-0.4.51 → classifyre_cli-0.4.53}/src/detectors/custom/runners/_gliner2.py +0 -0
- {classifyre_cli-0.4.51 → classifyre_cli-0.4.53}/src/detectors/custom/runners/_image_classification.py +0 -0
- {classifyre_cli-0.4.51 → classifyre_cli-0.4.53}/src/detectors/custom/runners/_llm.py +0 -0
- {classifyre_cli-0.4.51 → classifyre_cli-0.4.53}/src/detectors/custom/runners/_object_detection.py +0 -0
- {classifyre_cli-0.4.51 → classifyre_cli-0.4.53}/src/detectors/custom/runners/_regex.py +0 -0
- {classifyre_cli-0.4.51 → classifyre_cli-0.4.53}/src/detectors/custom/runners/_text_classification.py +0 -0
- {classifyre_cli-0.4.51 → classifyre_cli-0.4.53}/src/detectors/custom/trainer.py +0 -0
- {classifyre_cli-0.4.51 → classifyre_cli-0.4.53}/src/detectors/dependencies.py +0 -0
- {classifyre_cli-0.4.51 → classifyre_cli-0.4.53}/src/detectors/pii/__init__.py +0 -0
- {classifyre_cli-0.4.51 → classifyre_cli-0.4.53}/src/detectors/pii/detector.py +0 -0
- {classifyre_cli-0.4.51 → classifyre_cli-0.4.53}/src/detectors/secrets/__init__.py +0 -0
- {classifyre_cli-0.4.51 → classifyre_cli-0.4.53}/src/detectors/secrets/detector.py +0 -0
- {classifyre_cli-0.4.51 → classifyre_cli-0.4.53}/src/detectors/threat/__init__.py +0 -0
- {classifyre_cli-0.4.51 → classifyre_cli-0.4.53}/src/detectors/threat/code_security_detector.py +0 -0
- {classifyre_cli-0.4.51 → classifyre_cli-0.4.53}/src/detectors/threat/yara_detector.py +0 -0
- {classifyre_cli-0.4.51 → classifyre_cli-0.4.53}/src/main.py +0 -0
- {classifyre_cli-0.4.51 → classifyre_cli-0.4.53}/src/models/generated_detectors.py +0 -0
- {classifyre_cli-0.4.51 → classifyre_cli-0.4.53}/src/models/generated_single_asset_scan_results.py +0 -0
- {classifyre_cli-0.4.51 → classifyre_cli-0.4.53}/src/outputs/__init__.py +0 -0
- {classifyre_cli-0.4.51 → classifyre_cli-0.4.53}/src/outputs/base.py +0 -0
- {classifyre_cli-0.4.51 → classifyre_cli-0.4.53}/src/outputs/console.py +0 -0
- {classifyre_cli-0.4.51 → classifyre_cli-0.4.53}/src/outputs/factory.py +0 -0
- {classifyre_cli-0.4.51 → classifyre_cli-0.4.53}/src/outputs/file.py +0 -0
- {classifyre_cli-0.4.51 → classifyre_cli-0.4.53}/src/outputs/rest.py +0 -0
- {classifyre_cli-0.4.51 → classifyre_cli-0.4.53}/src/pipeline/__init__.py +0 -0
- {classifyre_cli-0.4.51 → classifyre_cli-0.4.53}/src/pipeline/content_provider.py +0 -0
- {classifyre_cli-0.4.51 → classifyre_cli-0.4.53}/src/pipeline/parsed_content_provider.py +0 -0
- {classifyre_cli-0.4.51 → classifyre_cli-0.4.53}/src/pipeline/worker_pool.py +0 -0
- {classifyre_cli-0.4.51 → classifyre_cli-0.4.53}/src/sandbox/__init__.py +0 -0
- {classifyre_cli-0.4.51 → classifyre_cli-0.4.53}/src/sandbox/runner.py +0 -0
- {classifyre_cli-0.4.51 → classifyre_cli-0.4.53}/src/sources/__init__.py +0 -0
- {classifyre_cli-0.4.51 → classifyre_cli-0.4.53}/src/sources/asset_metadata.py +0 -0
- {classifyre_cli-0.4.51 → classifyre_cli-0.4.53}/src/sources/atlassian_common.py +0 -0
- {classifyre_cli-0.4.51 → classifyre_cli-0.4.53}/src/sources/azure_blob_storage/__init__.py +0 -0
- {classifyre_cli-0.4.51 → classifyre_cli-0.4.53}/src/sources/azure_blob_storage/source.py +0 -0
- {classifyre_cli-0.4.51 → classifyre_cli-0.4.53}/src/sources/confluence/__init__.py +0 -0
- {classifyre_cli-0.4.51 → classifyre_cli-0.4.53}/src/sources/confluence/source.py +0 -0
- {classifyre_cli-0.4.51 → classifyre_cli-0.4.53}/src/sources/databricks/__init__.py +0 -0
- {classifyre_cli-0.4.51 → classifyre_cli-0.4.53}/src/sources/databricks/source.py +0 -0
- {classifyre_cli-0.4.51 → classifyre_cli-0.4.53}/src/sources/delta_lake/__init__.py +0 -0
- {classifyre_cli-0.4.51 → classifyre_cli-0.4.53}/src/sources/delta_lake/source.py +0 -0
- {classifyre_cli-0.4.51 → classifyre_cli-0.4.53}/src/sources/dependencies.py +0 -0
- {classifyre_cli-0.4.51 → classifyre_cli-0.4.53}/src/sources/elasticsearch/__init__.py +0 -0
- {classifyre_cli-0.4.51 → classifyre_cli-0.4.53}/src/sources/elasticsearch/source.py +0 -0
- {classifyre_cli-0.4.51 → classifyre_cli-0.4.53}/src/sources/email/__init__.py +0 -0
- {classifyre_cli-0.4.51 → classifyre_cli-0.4.53}/src/sources/email/source.py +0 -0
- {classifyre_cli-0.4.51 → classifyre_cli-0.4.53}/src/sources/google_cloud_storage/__init__.py +0 -0
- {classifyre_cli-0.4.51 → classifyre_cli-0.4.53}/src/sources/google_cloud_storage/source.py +0 -0
- {classifyre_cli-0.4.51 → classifyre_cli-0.4.53}/src/sources/google_workspace/__init__.py +0 -0
- {classifyre_cli-0.4.51 → classifyre_cli-0.4.53}/src/sources/google_workspace/source.py +0 -0
- {classifyre_cli-0.4.51 → classifyre_cli-0.4.53}/src/sources/hive/__init__.py +0 -0
- {classifyre_cli-0.4.51 → classifyre_cli-0.4.53}/src/sources/hive/source.py +0 -0
- {classifyre_cli-0.4.51 → classifyre_cli-0.4.53}/src/sources/iceberg/__init__.py +0 -0
- {classifyre_cli-0.4.51 → classifyre_cli-0.4.53}/src/sources/iceberg/source.py +0 -0
- {classifyre_cli-0.4.51 → classifyre_cli-0.4.53}/src/sources/jira/__init__.py +0 -0
- {classifyre_cli-0.4.51 → classifyre_cli-0.4.53}/src/sources/jira/source.py +0 -0
- {classifyre_cli-0.4.51 → classifyre_cli-0.4.53}/src/sources/kafka/__init__.py +0 -0
- {classifyre_cli-0.4.51 → classifyre_cli-0.4.53}/src/sources/kafka/source.py +0 -0
- {classifyre_cli-0.4.51 → classifyre_cli-0.4.53}/src/sources/lakehouse_base.py +0 -0
- {classifyre_cli-0.4.51 → classifyre_cli-0.4.53}/src/sources/local_folder/__init__.py +0 -0
- {classifyre_cli-0.4.51 → classifyre_cli-0.4.53}/src/sources/local_folder/source.py +0 -0
- {classifyre_cli-0.4.51 → classifyre_cli-0.4.53}/src/sources/meilisearch/__init__.py +0 -0
- {classifyre_cli-0.4.51 → classifyre_cli-0.4.53}/src/sources/meilisearch/source.py +0 -0
- {classifyre_cli-0.4.51 → classifyre_cli-0.4.53}/src/sources/microsoft_365/__init__.py +0 -0
- {classifyre_cli-0.4.51 → classifyre_cli-0.4.53}/src/sources/microsoft_365/source.py +0 -0
- {classifyre_cli-0.4.51 → classifyre_cli-0.4.53}/src/sources/mongodb/__init__.py +0 -0
- {classifyre_cli-0.4.51 → classifyre_cli-0.4.53}/src/sources/mongodb/source.py +0 -0
- {classifyre_cli-0.4.51 → classifyre_cli-0.4.53}/src/sources/mssql/__init__.py +0 -0
- {classifyre_cli-0.4.51 → classifyre_cli-0.4.53}/src/sources/mssql/source.py +0 -0
- {classifyre_cli-0.4.51 → classifyre_cli-0.4.53}/src/sources/mysql/__init__.py +0 -0
- {classifyre_cli-0.4.51 → classifyre_cli-0.4.53}/src/sources/mysql/source.py +0 -0
- {classifyre_cli-0.4.51 → classifyre_cli-0.4.53}/src/sources/neo4j/__init__.py +0 -0
- {classifyre_cli-0.4.51 → classifyre_cli-0.4.53}/src/sources/neo4j/source.py +0 -0
- {classifyre_cli-0.4.51 → classifyre_cli-0.4.53}/src/sources/notion/__init__.py +0 -0
- {classifyre_cli-0.4.51 → classifyre_cli-0.4.53}/src/sources/notion/client.py +0 -0
- {classifyre_cli-0.4.51 → classifyre_cli-0.4.53}/src/sources/notion/source.py +0 -0
- {classifyre_cli-0.4.51 → classifyre_cli-0.4.53}/src/sources/opensearch/__init__.py +0 -0
- {classifyre_cli-0.4.51 → classifyre_cli-0.4.53}/src/sources/opensearch/source.py +0 -0
- {classifyre_cli-0.4.51 → classifyre_cli-0.4.53}/src/sources/oracle/__init__.py +0 -0
- {classifyre_cli-0.4.51 → classifyre_cli-0.4.53}/src/sources/oracle/source.py +0 -0
- {classifyre_cli-0.4.51 → classifyre_cli-0.4.53}/src/sources/postgresql/__init__.py +0 -0
- {classifyre_cli-0.4.51 → classifyre_cli-0.4.53}/src/sources/postgresql/source.py +0 -0
- {classifyre_cli-0.4.51 → classifyre_cli-0.4.53}/src/sources/powerbi/__init__.py +0 -0
- {classifyre_cli-0.4.51 → classifyre_cli-0.4.53}/src/sources/powerbi/source.py +0 -0
- {classifyre_cli-0.4.51 → classifyre_cli-0.4.53}/src/sources/recipe_normalizer.py +0 -0
- {classifyre_cli-0.4.51 → classifyre_cli-0.4.53}/src/sources/s3_client.py +0 -0
- {classifyre_cli-0.4.51 → classifyre_cli-0.4.53}/src/sources/s3_compatible_storage/README.md +0 -0
- {classifyre_cli-0.4.51 → classifyre_cli-0.4.53}/src/sources/s3_compatible_storage/__init__.py +0 -0
- {classifyre_cli-0.4.51 → classifyre_cli-0.4.53}/src/sources/s3_compatible_storage/source.py +0 -0
- {classifyre_cli-0.4.51 → classifyre_cli-0.4.53}/src/sources/search_engine_base.py +0 -0
- {classifyre_cli-0.4.51 → classifyre_cli-0.4.53}/src/sources/servicedesk/__init__.py +0 -0
- {classifyre_cli-0.4.51 → classifyre_cli-0.4.53}/src/sources/servicedesk/source.py +0 -0
- {classifyre_cli-0.4.51 → classifyre_cli-0.4.53}/src/sources/slack/__init__.py +0 -0
- {classifyre_cli-0.4.51 → classifyre_cli-0.4.53}/src/sources/slack/source.py +0 -0
- {classifyre_cli-0.4.51 → classifyre_cli-0.4.53}/src/sources/snowflake/__init__.py +0 -0
- {classifyre_cli-0.4.51 → classifyre_cli-0.4.53}/src/sources/snowflake/source.py +0 -0
- {classifyre_cli-0.4.51 → classifyre_cli-0.4.53}/src/sources/sqlite/__init__.py +0 -0
- {classifyre_cli-0.4.51 → classifyre_cli-0.4.53}/src/sources/sqlite/source.py +0 -0
- {classifyre_cli-0.4.51 → classifyre_cli-0.4.53}/src/sources/tableau/__init__.py +0 -0
- {classifyre_cli-0.4.51 → classifyre_cli-0.4.53}/src/sources/tableau/source.py +0 -0
- {classifyre_cli-0.4.51 → classifyre_cli-0.4.53}/src/sources/tabular_base.py +0 -0
- {classifyre_cli-0.4.51 → classifyre_cli-0.4.53}/src/sources/tabular_utils.py +0 -0
- {classifyre_cli-0.4.51 → classifyre_cli-0.4.53}/src/sources/wordpress/__init__.py +0 -0
- {classifyre_cli-0.4.51 → classifyre_cli-0.4.53}/src/sources/wordpress/source.py +0 -0
- {classifyre_cli-0.4.51 → classifyre_cli-0.4.53}/src/sources/youtube/__init__.py +0 -0
- {classifyre_cli-0.4.51 → classifyre_cli-0.4.53}/src/telemetry.py +0 -0
- {classifyre_cli-0.4.51 → classifyre_cli-0.4.53}/src/utils/__init__.py +0 -0
- {classifyre_cli-0.4.51 → classifyre_cli-0.4.53}/src/utils/content_extraction.py +0 -0
- {classifyre_cli-0.4.51 → classifyre_cli-0.4.53}/src/utils/embedded_images.py +0 -0
- {classifyre_cli-0.4.51 → classifyre_cli-0.4.53}/src/utils/file_metadata.py +0 -0
- {classifyre_cli-0.4.51 → classifyre_cli-0.4.53}/src/utils/file_to_images.py +0 -0
- {classifyre_cli-0.4.51 → classifyre_cli-0.4.53}/src/utils/hashing.py +0 -0
- {classifyre_cli-0.4.51 → classifyre_cli-0.4.53}/src/utils/resources.py +0 -0
- {classifyre_cli-0.4.51 → classifyre_cli-0.4.53}/src/utils/uv_sync.py +0 -0
- {classifyre_cli-0.4.51 → classifyre_cli-0.4.53}/src/utils/validation.py +0 -0
- {classifyre_cli-0.4.51 → classifyre_cli-0.4.53}/tests/__init__.py +0 -0
- {classifyre_cli-0.4.51 → classifyre_cli-0.4.53}/tests/_lakehouse_fakes.py +0 -0
- {classifyre_cli-0.4.51 → classifyre_cli-0.4.53}/tests/conftest.py +0 -0
- {classifyre_cli-0.4.51 → classifyre_cli-0.4.53}/tests/detectors/__init__.py +0 -0
- {classifyre_cli-0.4.51 → classifyre_cli-0.4.53}/tests/detectors/broken_links/test_broken_links_detector.py +0 -0
- {classifyre_cli-0.4.51 → classifyre_cli-0.4.53}/tests/detectors/conftest.py +0 -0
- {classifyre_cli-0.4.51 → classifyre_cli-0.4.53}/tests/detectors/content/__init__.py +0 -0
- {classifyre_cli-0.4.51 → classifyre_cli-0.4.53}/tests/detectors/custom/__init__.py +0 -0
- {classifyre_cli-0.4.51 → classifyre_cli-0.4.53}/tests/detectors/custom/conftest.py +0 -0
- {classifyre_cli-0.4.51 → classifyre_cli-0.4.53}/tests/detectors/custom/test_invoice_extraction.py +0 -0
- {classifyre_cli-0.4.51 → classifyre_cli-0.4.53}/tests/detectors/custom/test_llm_runner.py +0 -0
- {classifyre_cli-0.4.51 → classifyre_cli-0.4.53}/tests/detectors/custom/test_pipeline_integration.py +0 -0
- {classifyre_cli-0.4.51 → classifyre_cli-0.4.53}/tests/detectors/custom/test_regex_runner.py +0 -0
- {classifyre_cli-0.4.51 → classifyre_cli-0.4.53}/tests/detectors/custom/test_runner_hardening.py +0 -0
- {classifyre_cli-0.4.51 → classifyre_cli-0.4.53}/tests/detectors/custom/test_transformer_runners.py +0 -0
- {classifyre_cli-0.4.51 → classifyre_cli-0.4.53}/tests/detectors/pii/__init__.py +0 -0
- {classifyre_cli-0.4.51 → classifyre_cli-0.4.53}/tests/detectors/pii/conftest.py +0 -0
- {classifyre_cli-0.4.51 → classifyre_cli-0.4.53}/tests/detectors/pii/sample_invoice.pdf +0 -0
- {classifyre_cli-0.4.51 → classifyre_cli-0.4.53}/tests/detectors/pii/test_pii_detector.py +0 -0
- {classifyre_cli-0.4.51 → classifyre_cli-0.4.53}/tests/detectors/pii/test_pii_detector_extended.py +0 -0
- {classifyre_cli-0.4.51 → classifyre_cli-0.4.53}/tests/detectors/secrets/__init__.py +0 -0
- {classifyre_cli-0.4.51 → classifyre_cli-0.4.53}/tests/detectors/secrets/test_secrets_detector.py +0 -0
- {classifyre_cli-0.4.51 → classifyre_cli-0.4.53}/tests/detectors/secrets/test_secrets_detector_extended.py +0 -0
- {classifyre_cli-0.4.51 → classifyre_cli-0.4.53}/tests/detectors/test_base_detector.py +0 -0
- {classifyre_cli-0.4.51 → classifyre_cli-0.4.53}/tests/detectors/test_custom_detector_examples_runtime.py +0 -0
- {classifyre_cli-0.4.51 → classifyre_cli-0.4.53}/tests/detectors/test_detector_catalog_commercial.py +0 -0
- {classifyre_cli-0.4.51 → classifyre_cli-0.4.53}/tests/detectors/test_detector_pipeline_types.py +0 -0
- {classifyre_cli-0.4.51 → classifyre_cli-0.4.53}/tests/detectors/test_detector_schema_examples.py +0 -0
- {classifyre_cli-0.4.51 → classifyre_cli-0.4.53}/tests/detectors/test_detector_types.py +0 -0
- {classifyre_cli-0.4.51 → classifyre_cli-0.4.53}/tests/detectors/test_phase2_detectors.py +0 -0
- {classifyre_cli-0.4.51 → classifyre_cli-0.4.53}/tests/detectors/test_registry.py +0 -0
- {classifyre_cli-0.4.51 → classifyre_cli-0.4.53}/tests/detectors/threat/__init__.py +0 -0
- {classifyre_cli-0.4.51 → classifyre_cli-0.4.53}/tests/detectors/threat/test_code_security_detector.py +0 -0
- {classifyre_cli-0.4.51 → classifyre_cli-0.4.53}/tests/detectors/threat/test_yara_detector.py +0 -0
- {classifyre_cli-0.4.51 → classifyre_cli-0.4.53}/tests/integration/test_wordpress_broken_links_detector.py +0 -0
- {classifyre_cli-0.4.51 → classifyre_cli-0.4.53}/tests/integration/test_wordpress_links_assets.py +0 -0
- {classifyre_cli-0.4.51 → classifyre_cli-0.4.53}/tests/pipeline/test_worker_pool.py +0 -0
- {classifyre_cli-0.4.51 → classifyre_cli-0.4.53}/tests/test_assets_metadata_catalog.py +0 -0
- {classifyre_cli-0.4.51 → classifyre_cli-0.4.53}/tests/test_azure_blob_storage_source.py +0 -0
- {classifyre_cli-0.4.51 → classifyre_cli-0.4.53}/tests/test_base_source_attachment.py +0 -0
- {classifyre_cli-0.4.51 → classifyre_cli-0.4.53}/tests/test_base_source_sampling.py +0 -0
- {classifyre_cli-0.4.51 → classifyre_cli-0.4.53}/tests/test_config.py +0 -0
- {classifyre_cli-0.4.51 → classifyre_cli-0.4.53}/tests/test_confluence_source.py +0 -0
- {classifyre_cli-0.4.51 → classifyre_cli-0.4.53}/tests/test_custom_extractor.py +0 -0
- {classifyre_cli-0.4.51 → classifyre_cli-0.4.53}/tests/test_databricks_source.py +0 -0
- {classifyre_cli-0.4.51 → classifyre_cli-0.4.53}/tests/test_delta_lake_source.py +0 -0
- {classifyre_cli-0.4.51 → classifyre_cli-0.4.53}/tests/test_elasticsearch_source.py +0 -0
- {classifyre_cli-0.4.51 → classifyre_cli-0.4.53}/tests/test_email_source.py +0 -0
- {classifyre_cli-0.4.51 → classifyre_cli-0.4.53}/tests/test_google_cloud_storage_source.py +0 -0
- {classifyre_cli-0.4.51 → classifyre_cli-0.4.53}/tests/test_google_workspace_source.py +0 -0
- {classifyre_cli-0.4.51 → classifyre_cli-0.4.53}/tests/test_hashing.py +0 -0
- {classifyre_cli-0.4.51 → classifyre_cli-0.4.53}/tests/test_hive_source.py +0 -0
- {classifyre_cli-0.4.51 → classifyre_cli-0.4.53}/tests/test_iceberg_source.py +0 -0
- {classifyre_cli-0.4.51 → classifyre_cli-0.4.53}/tests/test_kafka_source.py +0 -0
- {classifyre_cli-0.4.51 → classifyre_cli-0.4.53}/tests/test_local_folder_source.py +0 -0
- {classifyre_cli-0.4.51 → classifyre_cli-0.4.53}/tests/test_meilisearch_source.py +0 -0
- {classifyre_cli-0.4.51 → classifyre_cli-0.4.53}/tests/test_microsoft_365_source.py +0 -0
- {classifyre_cli-0.4.51 → classifyre_cli-0.4.53}/tests/test_mongodb_source.py +0 -0
- {classifyre_cli-0.4.51 → classifyre_cli-0.4.53}/tests/test_mssql_source.py +0 -0
- {classifyre_cli-0.4.51 → classifyre_cli-0.4.53}/tests/test_mysql_source.py +0 -0
- {classifyre_cli-0.4.51 → classifyre_cli-0.4.53}/tests/test_neo4j_source.py +0 -0
- {classifyre_cli-0.4.51 → classifyre_cli-0.4.53}/tests/test_notion_source.py +0 -0
- {classifyre_cli-0.4.51 → classifyre_cli-0.4.53}/tests/test_opensearch_source.py +0 -0
- {classifyre_cli-0.4.51 → classifyre_cli-0.4.53}/tests/test_oracle_source.py +0 -0
- {classifyre_cli-0.4.51 → classifyre_cli-0.4.53}/tests/test_outputs.py +0 -0
- {classifyre_cli-0.4.51 → classifyre_cli-0.4.53}/tests/test_postgresql_source.py +0 -0
- {classifyre_cli-0.4.51 → classifyre_cli-0.4.53}/tests/test_powerbi_source.py +0 -0
- {classifyre_cli-0.4.51 → classifyre_cli-0.4.53}/tests/test_recipe_normalizer.py +0 -0
- {classifyre_cli-0.4.51 → classifyre_cli-0.4.53}/tests/test_sampling_automatic.py +0 -0
- {classifyre_cli-0.4.51 → classifyre_cli-0.4.53}/tests/test_sandbox_runner.py +0 -0
- {classifyre_cli-0.4.51 → classifyre_cli-0.4.53}/tests/test_servicedesk_source.py +0 -0
- {classifyre_cli-0.4.51 → classifyre_cli-0.4.53}/tests/test_slack_source.py +0 -0
- {classifyre_cli-0.4.51 → classifyre_cli-0.4.53}/tests/test_snowflake_source.py +0 -0
- {classifyre_cli-0.4.51 → classifyre_cli-0.4.53}/tests/test_source_dependency_groups.py +0 -0
- {classifyre_cli-0.4.51 → classifyre_cli-0.4.53}/tests/test_source_strategy_conformance.py +0 -0
- {classifyre_cli-0.4.51 → classifyre_cli-0.4.53}/tests/test_sqlite_source.py +0 -0
- {classifyre_cli-0.4.51 → classifyre_cli-0.4.53}/tests/test_tableau_source.py +0 -0
- {classifyre_cli-0.4.51 → classifyre_cli-0.4.53}/tests/test_tabular_automatic_sampling.py +0 -0
- {classifyre_cli-0.4.51 → classifyre_cli-0.4.53}/tests/test_tabular_utils.py +0 -0
- {classifyre_cli-0.4.51 → classifyre_cli-0.4.53}/tests/test_uv_sync.py +0 -0
- {classifyre_cli-0.4.51 → classifyre_cli-0.4.53}/tests/test_wordpress_source.py +0 -0
- {classifyre_cli-0.4.51 → classifyre_cli-0.4.53}/tests/test_youtube_source_integration.py +0 -0
- {classifyre_cli-0.4.51 → classifyre_cli-0.4.53}/tests/utils/test_content_extraction.py +0 -0
- {classifyre_cli-0.4.51 → classifyre_cli-0.4.53}/tests/utils/test_embedded_images.py +0 -0
- {classifyre_cli-0.4.51 → classifyre_cli-0.4.53}/tests/utils/test_file_metadata.py +0 -0
- {classifyre_cli-0.4.51 → classifyre_cli-0.4.53}/tests/utils/test_file_to_images.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "classifyre-cli"
|
|
3
|
-
version = "0.4.
|
|
3
|
+
version = "0.4.53"
|
|
4
4
|
description = "Classifyre CLI — scan and classify unstructured data sources"
|
|
5
5
|
readme = "README.md"
|
|
6
6
|
requires-python = ">=3.12"
|
|
@@ -128,6 +128,7 @@ detectors = [
|
|
|
128
128
|
{ include-group = "regex" },
|
|
129
129
|
{ include-group = "llm" },
|
|
130
130
|
{ include-group = "transcription" },
|
|
131
|
+
{ include-group = "video" },
|
|
131
132
|
]
|
|
132
133
|
file-processing = [
|
|
133
134
|
"filetype>=1.2.0",
|
|
@@ -145,9 +146,16 @@ transcription = [
|
|
|
145
146
|
# PyAV (no system ffmpeg required) and runs ctranslate2 for inference.
|
|
146
147
|
"faster-whisper>=1.1.0",
|
|
147
148
|
]
|
|
149
|
+
video = [
|
|
150
|
+
# Use the server-safe OpenCV 5 wheel. RapidOCR's desktop OpenCV dependency
|
|
151
|
+
# is excluded below because both distributions install the same cv2 module.
|
|
152
|
+
"opencv-python-headless>=5.0.0.93,<6.0.0",
|
|
153
|
+
"rapidocr-onnxruntime>=1.4.0",
|
|
154
|
+
]
|
|
148
155
|
ocr = [
|
|
149
156
|
{ include-group = "file-processing" },
|
|
150
157
|
"docling>=2.94.0",
|
|
158
|
+
"opencv-python-headless>=5.0.0.93,<6.0.0",
|
|
151
159
|
"rapidocr-onnxruntime>=1.4.0",
|
|
152
160
|
# torchvision is a transitive dep of docling-ibm-models, but [tool.uv.sources]
|
|
153
161
|
# index pins only apply to dependencies declared directly in this project.
|
|
@@ -407,6 +415,10 @@ markers = [
|
|
|
407
415
|
|
|
408
416
|
[tool.uv]
|
|
409
417
|
package = true
|
|
418
|
+
# RapidOCR is the sole requester of opencv-python and works with the headless
|
|
419
|
+
# distribution declared by our video and OCR groups. Installing both would
|
|
420
|
+
# overwrite the same cv2 namespace and add GUI libraries to CPU-only containers.
|
|
421
|
+
exclude-dependencies = ["opencv-python"]
|
|
410
422
|
# Every platform we ship (desktop bundles + CI) must resolve to versions with
|
|
411
423
|
# usable wheels. Without this, uv happily locks versions that are missing a
|
|
412
424
|
# platform — e.g. deltalake 1.6.1 published no win_amd64 wheel, which broke the
|
|
@@ -112,14 +112,6 @@ class SamplingConfig(BaseModel):
|
|
|
112
112
|
extra='forbid',
|
|
113
113
|
)
|
|
114
114
|
strategy: SamplingStrategy
|
|
115
|
-
enable_ocr: bool | None = Field(
|
|
116
|
-
False,
|
|
117
|
-
description='When true, enable OCR/text extraction for supported binary documents and images before routing text-capable detectors.',
|
|
118
|
-
)
|
|
119
|
-
enable_transcription: bool | None = Field(
|
|
120
|
-
False,
|
|
121
|
-
description='When true, transcribe audio and video files to text (via faster-whisper) before routing text-capable detectors. Slower and requires the transcription dependency.',
|
|
122
|
-
)
|
|
123
115
|
order_by_column: str | None = Field(
|
|
124
116
|
None,
|
|
125
117
|
description='Column to use for LATEST sampling mode in tabular sources (usually created_at/updated_at). Auto-detected when not set.',
|
|
@@ -407,10 +399,6 @@ class YouTubeOptionalTranscript(BaseModel):
|
|
|
407
399
|
None,
|
|
408
400
|
description='Preferred caption language codes in priority order (e.g. ["en"]). Empty means accept any available language.',
|
|
409
401
|
)
|
|
410
|
-
skip_transcript: bool | None = Field(
|
|
411
|
-
False,
|
|
412
|
-
description='When true, skip transcript fetching entirely (metadata-only assets, no detector content).',
|
|
413
|
-
)
|
|
414
402
|
|
|
415
403
|
|
|
416
404
|
class YouTubeOptionalConnection(BaseModel):
|
|
@@ -108,11 +108,7 @@ class DetectorPipeline:
|
|
|
108
108
|
return asset
|
|
109
109
|
|
|
110
110
|
scan_started = datetime.now(UTC)
|
|
111
|
-
|
|
112
|
-
transcription_enabled = self.source.transcription_enabled()
|
|
113
|
-
text_content_type = self._text_content_type_for_asset(
|
|
114
|
-
asset.asset_type, ocr_enabled, transcription_enabled
|
|
115
|
-
)
|
|
111
|
+
text_content_type = self._text_content_type_for_asset(asset.asset_type)
|
|
116
112
|
link_content = self._build_links_payload(asset.links)
|
|
117
113
|
|
|
118
114
|
text_detectors = []
|
|
@@ -765,8 +761,6 @@ class DetectorPipeline:
|
|
|
765
761
|
def _text_content_type_for_asset(
|
|
766
762
|
self,
|
|
767
763
|
asset_type: OutputAssetType,
|
|
768
|
-
ocr_enabled: bool,
|
|
769
|
-
transcription_enabled: bool = False,
|
|
770
764
|
) -> str | None:
|
|
771
765
|
mapping = {
|
|
772
766
|
OutputAssetType.TXT: "text/plain",
|
|
@@ -775,9 +769,12 @@ class DetectorPipeline:
|
|
|
775
769
|
}
|
|
776
770
|
if asset_type in mapping:
|
|
777
771
|
return mapping[asset_type]
|
|
778
|
-
if
|
|
779
|
-
|
|
780
|
-
|
|
772
|
+
if asset_type in {
|
|
773
|
+
OutputAssetType.IMAGE,
|
|
774
|
+
OutputAssetType.BINARY,
|
|
775
|
+
OutputAssetType.AUDIO,
|
|
776
|
+
OutputAssetType.VIDEO,
|
|
777
|
+
}:
|
|
781
778
|
return "text/plain"
|
|
782
779
|
return None
|
|
783
780
|
|
|
@@ -325,18 +325,6 @@ class BaseSource(ABC):
|
|
|
325
325
|
return stored.strip()
|
|
326
326
|
return fallback_url
|
|
327
327
|
|
|
328
|
-
def ocr_enabled(self) -> bool:
|
|
329
|
-
"""Return whether sampling-level OCR is enabled for this source."""
|
|
330
|
-
config = getattr(self, "config", None)
|
|
331
|
-
sampling = getattr(config, "sampling", None) if config is not None else None
|
|
332
|
-
return bool(getattr(sampling, "enable_ocr", False))
|
|
333
|
-
|
|
334
|
-
def transcription_enabled(self) -> bool:
|
|
335
|
-
"""Return whether sampling-level audio/video transcription is enabled."""
|
|
336
|
-
config = getattr(self, "config", None)
|
|
337
|
-
sampling = getattr(config, "sampling", None) if config is not None else None
|
|
338
|
-
return bool(getattr(sampling, "enable_transcription", False))
|
|
339
|
-
|
|
340
328
|
def parse_asset_bytes(
|
|
341
329
|
self,
|
|
342
330
|
file_bytes: bytes,
|
|
@@ -350,8 +338,6 @@ class BaseSource(ABC):
|
|
|
350
338
|
file_bytes,
|
|
351
339
|
declared_mime_type=declared_mime_type,
|
|
352
340
|
file_name=file_name,
|
|
353
|
-
enable_ocr=self.ocr_enabled(),
|
|
354
|
-
enable_transcription=self.transcription_enabled(),
|
|
355
341
|
)
|
|
356
342
|
|
|
357
343
|
def iter_asset_pages(
|
|
@@ -371,8 +357,6 @@ class BaseSource(ABC):
|
|
|
371
357
|
batch_size,
|
|
372
358
|
include_column_names,
|
|
373
359
|
file_name=file_name,
|
|
374
|
-
enable_ocr=self.ocr_enabled(),
|
|
375
|
-
enable_transcription=self.transcription_enabled(),
|
|
376
360
|
)
|
|
377
361
|
|
|
378
362
|
async def fetch_content_bytes(self, asset_id: str) -> tuple[bytes, str] | None:
|
|
@@ -393,16 +393,11 @@ class ObjectStorageSourceBase(BaseSource, ABC):
|
|
|
393
393
|
)
|
|
394
394
|
normalized_mime = mime_type.split(";", 1)[0].strip().lower()
|
|
395
395
|
|
|
396
|
-
#
|
|
397
|
-
#
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
is_media = normalized_mime.startswith(("audio/", "video/"))
|
|
402
|
-
is_non_extractable = (
|
|
403
|
-
normalized_mime.startswith("image/")
|
|
404
|
-
or (is_media and not self.transcription_enabled())
|
|
405
|
-
or normalized_mime in ("application/octet-stream", "application/zip")
|
|
396
|
+
# Images and audio/video are extractable automatically. Opaque binaries
|
|
397
|
+
# remain available to binary detectors but have no text page stream.
|
|
398
|
+
is_non_extractable = normalized_mime in (
|
|
399
|
+
"application/octet-stream",
|
|
400
|
+
"application/zip",
|
|
406
401
|
)
|
|
407
402
|
|
|
408
403
|
return ContentSnapshot(
|
|
@@ -6,10 +6,9 @@ Pipeline:
|
|
|
6
6
|
-> video ids
|
|
7
7
|
-> youtube-transcript-api (fetch captions)
|
|
8
8
|
-> transcript found -> asset + metadata, transcript is detector content
|
|
9
|
-
->
|
|
10
|
-
->
|
|
11
|
-
|
|
12
|
-
-> otherwise -> asset + metadata only (no detector content)
|
|
9
|
+
-> download a bounded-resolution muxed stream
|
|
10
|
+
-> no captions -> transcribe audio with faster-whisper
|
|
11
|
+
-> sparse changed-frame OCR with OpenCV + RapidOCR
|
|
13
12
|
|
|
14
13
|
Both libraries scrape public data and need no API key. ``yt-dlp`` and
|
|
15
14
|
``youtube-transcript-api`` are optional dependencies (``[youtube]`` group) and
|
|
@@ -89,6 +88,9 @@ class YouTubeSource(BaseSource):
|
|
|
89
88
|
# Transcript text keyed by hash / external_url / video_id so the detector
|
|
90
89
|
# pipeline (which probes both external_url and hash) resolves content.
|
|
91
90
|
self._transcripts: dict[str, str] = {}
|
|
91
|
+
self._video_id_by_key: dict[str, str] = {}
|
|
92
|
+
self._assets_by_video_id: dict[str, SingleAssetScanResults] = {}
|
|
93
|
+
self._media_attempted: set[str] = set()
|
|
92
94
|
self._cookiefile: str | None = None
|
|
93
95
|
|
|
94
96
|
# ------------------------------------------------------------------
|
|
@@ -289,9 +291,8 @@ class YouTubeSource(BaseSource):
|
|
|
289
291
|
|
|
290
292
|
Handles the documented failure cases (captions disabled, no captions,
|
|
291
293
|
age-restricted/private, rate limiting) by logging and returning None so
|
|
292
|
-
the asset is still emitted
|
|
293
|
-
|
|
294
|
-
source has ``sampling.enable_transcription`` set.
|
|
294
|
+
the asset is still emitted. When no captions exist, the downloaded video
|
|
295
|
+
audio is always transcribed by ``_process_video_media``.
|
|
295
296
|
"""
|
|
296
297
|
yt_mod = require_module("youtube_transcript_api", "YouTube", uv_groups=["youtube"])
|
|
297
298
|
|
|
@@ -331,17 +332,16 @@ class YouTubeSource(BaseSource):
|
|
|
331
332
|
logger.warning("Transcript fetch failed for video %s: %s", video_id, exc)
|
|
332
333
|
return None
|
|
333
334
|
|
|
334
|
-
def
|
|
335
|
-
"""Download
|
|
336
|
-
|
|
337
|
-
Returns the path to the downloaded file, or None on failure. No yt-dlp
|
|
338
|
-
post-processing is used, so the raw audio container (.m4a/.webm/…) is
|
|
339
|
-
written directly — faster-whisper decodes it via bundled PyAV, so no
|
|
340
|
-
system ffmpeg is required.
|
|
341
|
-
"""
|
|
335
|
+
def _download_video(self, video_id: str, dest_dir: Path) -> Path | None:
|
|
336
|
+
"""Download one muxed stream, preferring at most 480p to bound CPU/network."""
|
|
342
337
|
opts = self._base_ydl_opts()
|
|
343
338
|
opts["skip_download"] = False
|
|
344
|
-
|
|
339
|
+
# Requiring both codecs avoids yt-dlp's ffmpeg merge path. Visual OCR only
|
|
340
|
+
# needs readable text, while the audio track remains suitable for Whisper.
|
|
341
|
+
opts["format"] = (
|
|
342
|
+
"best[height<=480][acodec!=none][vcodec!=none]/"
|
|
343
|
+
"best[acodec!=none][vcodec!=none]"
|
|
344
|
+
)
|
|
345
345
|
opts["noplaylist"] = True
|
|
346
346
|
opts["outtmpl"] = str(dest_dir / "%(id)s.%(ext)s")
|
|
347
347
|
url = _WATCH_URL.format(video_id=video_id)
|
|
@@ -349,45 +349,53 @@ class YouTubeSource(BaseSource):
|
|
|
349
349
|
with self._ydl_class()(opts) as ydl:
|
|
350
350
|
ydl.extract_info(url, download=True)
|
|
351
351
|
except Exception as exc:
|
|
352
|
-
logger.warning("Failed to download
|
|
352
|
+
logger.warning("Failed to download video %s: %s", video_id, exc)
|
|
353
353
|
return None
|
|
354
354
|
files = [p for p in dest_dir.iterdir() if p.is_file()]
|
|
355
355
|
if not files:
|
|
356
|
-
logger.warning("
|
|
356
|
+
logger.warning("Video download produced no file for video %s", video_id)
|
|
357
357
|
return None
|
|
358
|
-
#
|
|
358
|
+
# The selected format yields one file; pick the largest if a sidecar slipped in.
|
|
359
359
|
return max(files, key=lambda p: p.stat().st_size)
|
|
360
360
|
|
|
361
|
-
def
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
361
|
+
def _process_video_media(
|
|
362
|
+
self,
|
|
363
|
+
video_id: str,
|
|
364
|
+
*,
|
|
365
|
+
transcribe: bool,
|
|
366
|
+
) -> tuple[_TranscriptResult | None, str]:
|
|
367
|
+
"""Download once, then transcribe when needed and always OCR changed frames."""
|
|
368
|
+
from ...utils.transcription import transcribe_media_path
|
|
369
|
+
from ...utils.video_processing import extract_video_ocr_path
|
|
370
|
+
|
|
371
|
+
with tempfile.TemporaryDirectory(prefix="yt_video_") as tmp:
|
|
372
|
+
path = self._download_video(video_id, Path(tmp))
|
|
371
373
|
if path is None:
|
|
372
|
-
return None
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
374
|
+
return None, ""
|
|
375
|
+
visual_text, visual_error = extract_video_ocr_path(path)
|
|
376
|
+
if visual_error:
|
|
377
|
+
logger.warning("Visual OCR failed for YouTube video %s: %s", video_id, visual_error)
|
|
378
|
+
|
|
379
|
+
transcript: _TranscriptResult | None = None
|
|
380
|
+
if transcribe:
|
|
381
|
+
text, error = transcribe_media_path(
|
|
382
|
+
path,
|
|
383
|
+
mime_type="video/mp4",
|
|
384
|
+
)
|
|
385
|
+
if error:
|
|
386
|
+
logger.warning("Whisper transcription failed for video %s: %s", video_id, error)
|
|
387
|
+
elif text:
|
|
388
|
+
logger.info(
|
|
389
|
+
"Transcribed video %s via faster-whisper (%d chars)", video_id, len(text)
|
|
390
|
+
)
|
|
391
|
+
transcript = _TranscriptResult(
|
|
392
|
+
text=text,
|
|
393
|
+
language=None,
|
|
394
|
+
is_generated=True,
|
|
395
|
+
available_languages=[],
|
|
396
|
+
source="whisper",
|
|
397
|
+
)
|
|
398
|
+
return transcript, visual_text
|
|
391
399
|
|
|
392
400
|
# ------------------------------------------------------------------
|
|
393
401
|
# Asset construction
|
|
@@ -417,18 +425,6 @@ class YouTubeSource(BaseSource):
|
|
|
417
425
|
else:
|
|
418
426
|
created_at = datetime.now(UTC)
|
|
419
427
|
|
|
420
|
-
transcript: _TranscriptResult | None = None
|
|
421
|
-
if not self._transcript_options().skip_transcript:
|
|
422
|
-
transcript = self._fetch_transcript(video_id)
|
|
423
|
-
# No captions: fall back to downloading the audio and transcribing it
|
|
424
|
-
# with faster-whisper when the source has transcription enabled.
|
|
425
|
-
if transcript is None and self.transcription_enabled():
|
|
426
|
-
transcript = self._transcribe_audio(video_id)
|
|
427
|
-
if transcript is not None:
|
|
428
|
-
self._transcripts[asset_hash] = transcript.text
|
|
429
|
-
self._transcripts[external_url] = transcript.text
|
|
430
|
-
self._transcripts[video_id] = transcript.text
|
|
431
|
-
|
|
432
428
|
asset_metadata: dict[str, Any] = {
|
|
433
429
|
"video_id": video_id,
|
|
434
430
|
"title": title,
|
|
@@ -451,25 +447,17 @@ class YouTubeSource(BaseSource):
|
|
|
451
447
|
if isinstance(upload_date, str) and upload_date:
|
|
452
448
|
asset_metadata["upload_date"] = upload_date
|
|
453
449
|
|
|
454
|
-
asset_metadata["transcript_available"] =
|
|
455
|
-
if transcript is not None:
|
|
456
|
-
asset_metadata["transcript_source"] = transcript.source
|
|
457
|
-
if transcript.language:
|
|
458
|
-
asset_metadata["transcript_language"] = transcript.language
|
|
459
|
-
if transcript.is_generated is not None:
|
|
460
|
-
asset_metadata["transcript_is_generated"] = transcript.is_generated
|
|
461
|
-
if transcript.available_languages:
|
|
462
|
-
asset_metadata["caption_tracks"] = transcript.available_languages
|
|
450
|
+
asset_metadata["transcript_available"] = False
|
|
463
451
|
|
|
464
452
|
# Checksum must only reflect content changes: view/like counters move on
|
|
465
453
|
# every re-scan and would flip UNCHANGED assets to UPDATED spuriously.
|
|
466
454
|
checksum_data = {
|
|
467
455
|
"video_id": video_id,
|
|
468
456
|
"title": title,
|
|
469
|
-
"
|
|
457
|
+
"detector_content": "",
|
|
470
458
|
}
|
|
471
459
|
|
|
472
|
-
|
|
460
|
+
asset = SingleAssetScanResults(
|
|
473
461
|
hash=asset_hash,
|
|
474
462
|
checksum=self.calculate_checksum(checksum_data),
|
|
475
463
|
name=title,
|
|
@@ -482,6 +470,58 @@ class YouTubeSource(BaseSource):
|
|
|
482
470
|
runner_id=self.runner_id,
|
|
483
471
|
**self.metadata_fields("video", asset_metadata),
|
|
484
472
|
)
|
|
473
|
+
self._assets_by_video_id[video_id] = asset
|
|
474
|
+
for key in (asset_hash, external_url, video_id):
|
|
475
|
+
self._video_id_by_key[key] = video_id
|
|
476
|
+
|
|
477
|
+
# Direct source usage (tests/local integrations) preserves eager behavior.
|
|
478
|
+
# The CLI's two-phase runner sets discovery_only and processes lazily in
|
|
479
|
+
# fetch_content(), bounded by max_concurrent_assets.
|
|
480
|
+
if not self._discovery_only:
|
|
481
|
+
self._populate_video_content(video_id)
|
|
482
|
+
return asset
|
|
483
|
+
|
|
484
|
+
def _populate_video_content(self, video_id: str) -> None:
|
|
485
|
+
if video_id in self._media_attempted:
|
|
486
|
+
return
|
|
487
|
+
self._media_attempted.add(video_id)
|
|
488
|
+
|
|
489
|
+
asset = self._assets_by_video_id.get(video_id)
|
|
490
|
+
if asset is None:
|
|
491
|
+
return
|
|
492
|
+
transcript = self._fetch_transcript(video_id)
|
|
493
|
+
media_transcript, visual_text = self._process_video_media(
|
|
494
|
+
video_id,
|
|
495
|
+
transcribe=transcript is None,
|
|
496
|
+
)
|
|
497
|
+
transcript = transcript or media_transcript
|
|
498
|
+
content_parts: list[str] = []
|
|
499
|
+
if transcript is not None:
|
|
500
|
+
content_parts.append(f"[Transcript]\n{transcript.text}")
|
|
501
|
+
if visual_text:
|
|
502
|
+
content_parts.append(visual_text)
|
|
503
|
+
detector_content = "\n\n".join(content_parts)
|
|
504
|
+
|
|
505
|
+
asset.metadata["transcript_available"] = transcript is not None
|
|
506
|
+
if transcript is not None:
|
|
507
|
+
asset.metadata["transcript_source"] = transcript.source
|
|
508
|
+
if transcript.language:
|
|
509
|
+
asset.metadata["transcript_language"] = transcript.language
|
|
510
|
+
if transcript.is_generated is not None:
|
|
511
|
+
asset.metadata["transcript_is_generated"] = transcript.is_generated
|
|
512
|
+
if transcript.available_languages:
|
|
513
|
+
asset.metadata["caption_tracks"] = transcript.available_languages
|
|
514
|
+
|
|
515
|
+
asset.checksum = self.calculate_checksum(
|
|
516
|
+
{
|
|
517
|
+
"video_id": video_id,
|
|
518
|
+
"title": asset.name,
|
|
519
|
+
"detector_content": detector_content,
|
|
520
|
+
}
|
|
521
|
+
)
|
|
522
|
+
if detector_content:
|
|
523
|
+
for key in (asset.hash, asset.external_url, video_id):
|
|
524
|
+
self._transcripts[key] = detector_content
|
|
485
525
|
|
|
486
526
|
# ------------------------------------------------------------------
|
|
487
527
|
# BaseSource interface
|
|
@@ -527,6 +567,9 @@ class YouTubeSource(BaseSource):
|
|
|
527
567
|
return
|
|
528
568
|
|
|
529
569
|
self._transcripts = {}
|
|
570
|
+
self._video_id_by_key = {}
|
|
571
|
+
self._assets_by_video_id = {}
|
|
572
|
+
self._media_attempted = set()
|
|
530
573
|
video_ids = self._resolve_target_video_ids()
|
|
531
574
|
logger.info("Resolved %d YouTube video(s) to extract", len(video_ids))
|
|
532
575
|
|
|
@@ -557,18 +600,28 @@ class YouTubeSource(BaseSource):
|
|
|
557
600
|
return hash_id(type_value, asset_id)
|
|
558
601
|
|
|
559
602
|
async def fetch_content(self, asset_id: str) -> tuple[str, str] | None:
|
|
560
|
-
"""
|
|
603
|
+
"""Lazily process and return transcript plus on-screen text."""
|
|
604
|
+
video_id = self._video_id_by_key.get(asset_id)
|
|
605
|
+
if video_id is not None:
|
|
606
|
+
import asyncio
|
|
561
607
|
|
|
562
|
-
|
|
563
|
-
produce an asset + metadata but skip Phase 2 (detectors).
|
|
564
|
-
"""
|
|
608
|
+
await asyncio.to_thread(self._populate_video_content, video_id)
|
|
565
609
|
text = self._transcripts.get(asset_id)
|
|
566
610
|
if not text:
|
|
567
611
|
return None
|
|
568
612
|
return text, text
|
|
569
613
|
|
|
570
614
|
def evict_asset_cache(self, asset_hash: str) -> None:
|
|
615
|
+
video_id = self._video_id_by_key.pop(asset_hash, None)
|
|
571
616
|
self._transcripts.pop(asset_hash, None)
|
|
617
|
+
if video_id:
|
|
618
|
+
asset = self._assets_by_video_id.pop(video_id, None)
|
|
619
|
+
if asset:
|
|
620
|
+
self._transcripts.pop(asset.external_url, None)
|
|
621
|
+
self._video_id_by_key.pop(asset.external_url, None)
|
|
622
|
+
self._transcripts.pop(video_id, None)
|
|
623
|
+
self._video_id_by_key.pop(video_id, None)
|
|
624
|
+
self._media_attempted.discard(video_id)
|
|
572
625
|
|
|
573
626
|
def enrich_finding_location(
|
|
574
627
|
self,
|
|
@@ -590,3 +643,6 @@ class YouTubeSource(BaseSource):
|
|
|
590
643
|
pass
|
|
591
644
|
self._cookiefile = None
|
|
592
645
|
self._transcripts.clear()
|
|
646
|
+
self._video_id_by_key.clear()
|
|
647
|
+
self._assets_by_video_id.clear()
|
|
648
|
+
self._media_attempted.clear()
|
|
@@ -67,11 +67,4 @@ def recipe_uv_groups(recipe: dict[str, Any]) -> set[str]:
|
|
|
67
67
|
detector_type = str(detector.get("type", "")).upper()
|
|
68
68
|
groups |= DETECTOR_TYPE_GROUPS.get(detector_type, set())
|
|
69
69
|
|
|
70
|
-
sampling = recipe.get("sampling")
|
|
71
|
-
if isinstance(sampling, dict):
|
|
72
|
-
if sampling.get("enable_ocr"):
|
|
73
|
-
groups.add("ocr")
|
|
74
|
-
if sampling.get("enable_transcription"):
|
|
75
|
-
groups.add("transcription")
|
|
76
|
-
|
|
77
70
|
return {group for group in groups if group}
|