gather-engine 2.5.0__tar.gz → 2.6.0__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.
- {gather_engine-2.5.0 → gather_engine-2.6.0}/PKG-INFO +27 -4
- gather_engine-2.5.0/src/gather_engine.egg-info/PKG-INFO → gather_engine-2.6.0/README.md +22 -38
- {gather_engine-2.5.0 → gather_engine-2.6.0}/pyproject.toml +4 -3
- {gather_engine-2.5.0 → gather_engine-2.6.0}/src/gather/__init__.py +1 -1
- {gather_engine-2.5.0 → gather_engine-2.6.0}/src/gather/cli_ext.py +4 -0
- gather_engine-2.6.0/src/gather/decompose_binary.py +127 -0
- gather_engine-2.6.0/src/gather/decompose_cmd.py +80 -0
- gather_engine-2.6.0/src/gather/decompose_content.py +164 -0
- gather_engine-2.6.0/src/gather/decompose_document.py +98 -0
- gather_engine-2.6.0/src/gather/decompose_dsp.py +156 -0
- gather_engine-2.6.0/src/gather/decompose_image.py +129 -0
- gather_engine-2.6.0/src/gather/decompose_pcm.py +166 -0
- gather_engine-2.6.0/src/gather/decompose_rebuild.py +130 -0
- gather_engine-2.6.0/src/gather/decompose_run.py +116 -0
- gather_engine-2.6.0/src/gather/decompose_text.py +150 -0
- gather_engine-2.6.0/src/gather/decompose_tree.py +154 -0
- gather_engine-2.6.0/src/gather/decompose_zip.py +141 -0
- gather_engine-2.6.0/src/gather/frame_sheets.py +121 -0
- gather_engine-2.6.0/src/gather/media_probe.py +110 -0
- gather_engine-2.6.0/src/gather/media_probe_audio.py +118 -0
- gather_engine-2.6.0/src/gather/media_probe_bmff.py +116 -0
- gather_engine-2.6.0/src/gather/media_probe_ebml.py +132 -0
- gather_engine-2.6.0/src/gather/media_probe_image.py +73 -0
- {gather_engine-2.5.0 → gather_engine-2.6.0}/src/gather/pacing.py +44 -0
- gather_engine-2.6.0/src/gather/saves_cmd.py +68 -0
- gather_engine-2.6.0/src/gather/saves_entities.py +123 -0
- gather_engine-2.6.0/src/gather/saves_lang.py +106 -0
- gather_engine-2.6.0/src/gather/saves_sidecar.py +197 -0
- gather_engine-2.6.0/src/gather/saves_tables.py +209 -0
- gather_engine-2.6.0/src/gather/saves_write.py +55 -0
- gather_engine-2.6.0/src/gather/storyboard.py +118 -0
- {gather_engine-2.5.0 → gather_engine-2.6.0}/src/gather/video_bundle.py +6 -1
- {gather_engine-2.5.0 → gather_engine-2.6.0}/src/gather/video_bundle_cmd.py +38 -4
- {gather_engine-2.5.0 → gather_engine-2.6.0}/src/gather/video_bundle_run.py +114 -13
- gather_engine-2.6.0/src/gather/video_frames.py +106 -0
- {gather_engine-2.5.0 → gather_engine-2.6.0}/src/gather/video_source.py +5 -1
- gather_engine-2.5.0/README.md → gather_engine-2.6.0/src/gather_engine.egg-info/PKG-INFO +61 -3
- {gather_engine-2.5.0 → gather_engine-2.6.0}/src/gather_engine.egg-info/SOURCES.txt +33 -0
- {gather_engine-2.5.0 → gather_engine-2.6.0}/src/gather_engine.egg-info/requires.txt +5 -0
- gather_engine-2.6.0/tests/test_decompose.py +258 -0
- gather_engine-2.6.0/tests/test_decompose_content.py +236 -0
- gather_engine-2.6.0/tests/test_saves.py +181 -0
- gather_engine-2.6.0/tests/test_saves_entities.py +70 -0
- gather_engine-2.6.0/tests/test_saves_media.py +124 -0
- gather_engine-2.6.0/tests/test_storyboard.py +108 -0
- gather_engine-2.6.0/tests/test_video_frames.py +246 -0
- {gather_engine-2.5.0 → gather_engine-2.6.0}/LICENSE +0 -0
- {gather_engine-2.5.0 → gather_engine-2.6.0}/NOTICE +0 -0
- {gather_engine-2.5.0 → gather_engine-2.6.0}/setup.cfg +0 -0
- {gather_engine-2.5.0 → gather_engine-2.6.0}/src/gather/__main__.py +0 -0
- {gather_engine-2.5.0 → gather_engine-2.6.0}/src/gather/_vendor/__init__.py +0 -0
- {gather_engine-2.5.0 → gather_engine-2.6.0}/src/gather/_vendor/safe_spawn.py +0 -0
- {gather_engine-2.5.0 → gather_engine-2.6.0}/src/gather/api.py +0 -0
- {gather_engine-2.5.0 → gather_engine-2.6.0}/src/gather/arxiv.py +0 -0
- {gather_engine-2.5.0 → gather_engine-2.6.0}/src/gather/availability.py +0 -0
- {gather_engine-2.5.0 → gather_engine-2.6.0}/src/gather/backends.py +0 -0
- {gather_engine-2.5.0 → gather_engine-2.6.0}/src/gather/backends_browser.py +0 -0
- {gather_engine-2.5.0 → gather_engine-2.6.0}/src/gather/benchmark.py +0 -0
- {gather_engine-2.5.0 → gather_engine-2.6.0}/src/gather/browser.py +0 -0
- {gather_engine-2.5.0 → gather_engine-2.6.0}/src/gather/browser_evidence.py +0 -0
- {gather_engine-2.5.0 → gather_engine-2.6.0}/src/gather/cache.py +0 -0
- {gather_engine-2.5.0 → gather_engine-2.6.0}/src/gather/captions.py +0 -0
- {gather_engine-2.5.0 → gather_engine-2.6.0}/src/gather/channel.py +0 -0
- {gather_engine-2.5.0 → gather_engine-2.6.0}/src/gather/channel_ledger.py +0 -0
- {gather_engine-2.5.0 → gather_engine-2.6.0}/src/gather/citecheck.py +0 -0
- {gather_engine-2.5.0 → gather_engine-2.6.0}/src/gather/cli.py +0 -0
- {gather_engine-2.5.0 → gather_engine-2.6.0}/src/gather/client_mcp.py +0 -0
- {gather_engine-2.5.0 → gather_engine-2.6.0}/src/gather/client_network.py +0 -0
- {gather_engine-2.5.0 → gather_engine-2.6.0}/src/gather/commands.py +0 -0
- {gather_engine-2.5.0 → gather_engine-2.6.0}/src/gather/comment_balance.py +0 -0
- {gather_engine-2.5.0 → gather_engine-2.6.0}/src/gather/comment_report.py +0 -0
- {gather_engine-2.5.0 → gather_engine-2.6.0}/src/gather/comment_signals.py +0 -0
- {gather_engine-2.5.0 → gather_engine-2.6.0}/src/gather/context.py +0 -0
- {gather_engine-2.5.0 → gather_engine-2.6.0}/src/gather/corpus_cmd.py +0 -0
- {gather_engine-2.5.0 → gather_engine-2.6.0}/src/gather/crawl.py +0 -0
- {gather_engine-2.5.0 → gather_engine-2.6.0}/src/gather/credentials.py +0 -0
- {gather_engine-2.5.0 → gather_engine-2.6.0}/src/gather/derive.py +0 -0
- {gather_engine-2.5.0 → gather_engine-2.6.0}/src/gather/digest.py +0 -0
- {gather_engine-2.5.0 → gather_engine-2.6.0}/src/gather/docs.py +0 -0
- {gather_engine-2.5.0 → gather_engine-2.6.0}/src/gather/docs_tool.py +0 -0
- {gather_engine-2.5.0 → gather_engine-2.6.0}/src/gather/dom.py +0 -0
- {gather_engine-2.5.0 → gather_engine-2.6.0}/src/gather/export.py +0 -0
- {gather_engine-2.5.0 → gather_engine-2.6.0}/src/gather/extract.py +0 -0
- {gather_engine-2.5.0 → gather_engine-2.6.0}/src/gather/fastparse.py +0 -0
- {gather_engine-2.5.0 → gather_engine-2.6.0}/src/gather/federation.py +0 -0
- {gather_engine-2.5.0 → gather_engine-2.6.0}/src/gather/federation_cmd.py +0 -0
- {gather_engine-2.5.0 → gather_engine-2.6.0}/src/gather/federation_policy.py +0 -0
- {gather_engine-2.5.0 → gather_engine-2.6.0}/src/gather/federation_receipt.py +0 -0
- {gather_engine-2.5.0 → gather_engine-2.6.0}/src/gather/feed.py +0 -0
- {gather_engine-2.5.0 → gather_engine-2.6.0}/src/gather/fetch.py +0 -0
- {gather_engine-2.5.0 → gather_engine-2.6.0}/src/gather/filter_ledger.py +0 -0
- {gather_engine-2.5.0 → gather_engine-2.6.0}/src/gather/flagship.py +0 -0
- {gather_engine-2.5.0 → gather_engine-2.6.0}/src/gather/grant_checks.py +0 -0
- {gather_engine-2.5.0 → gather_engine-2.6.0}/src/gather/grants.py +0 -0
- {gather_engine-2.5.0 → gather_engine-2.6.0}/src/gather/http_call.py +0 -0
- {gather_engine-2.5.0 → gather_engine-2.6.0}/src/gather/interop.py +0 -0
- {gather_engine-2.5.0 → gather_engine-2.6.0}/src/gather/item.py +0 -0
- {gather_engine-2.5.0 → gather_engine-2.6.0}/src/gather/ledger_cmd.py +0 -0
- {gather_engine-2.5.0 → gather_engine-2.6.0}/src/gather/ledger_verify.py +0 -0
- {gather_engine-2.5.0 → gather_engine-2.6.0}/src/gather/local_model.py +0 -0
- {gather_engine-2.5.0 → gather_engine-2.6.0}/src/gather/localpath.py +0 -0
- {gather_engine-2.5.0 → gather_engine-2.6.0}/src/gather/mcp.py +0 -0
- {gather_engine-2.5.0 → gather_engine-2.6.0}/src/gather/method.py +0 -0
- {gather_engine-2.5.0 → gather_engine-2.6.0}/src/gather/model.py +0 -0
- {gather_engine-2.5.0 → gather_engine-2.6.0}/src/gather/monitor.py +0 -0
- {gather_engine-2.5.0 → gather_engine-2.6.0}/src/gather/net.py +0 -0
- {gather_engine-2.5.0 → gather_engine-2.6.0}/src/gather/ocr.py +0 -0
- {gather_engine-2.5.0 → gather_engine-2.6.0}/src/gather/payloads.py +0 -0
- {gather_engine-2.5.0 → gather_engine-2.6.0}/src/gather/pdf.py +0 -0
- {gather_engine-2.5.0 → gather_engine-2.6.0}/src/gather/pilot.py +0 -0
- {gather_engine-2.5.0 → gather_engine-2.6.0}/src/gather/pilot_bundle.py +0 -0
- {gather_engine-2.5.0 → gather_engine-2.6.0}/src/gather/pilot_commands.py +0 -0
- {gather_engine-2.5.0 → gather_engine-2.6.0}/src/gather/pilot_manifest.py +0 -0
- {gather_engine-2.5.0 → gather_engine-2.6.0}/src/gather/pilot_report.py +0 -0
- {gather_engine-2.5.0 → gather_engine-2.6.0}/src/gather/pilot_sources.py +0 -0
- {gather_engine-2.5.0 → gather_engine-2.6.0}/src/gather/provenance.py +0 -0
- {gather_engine-2.5.0 → gather_engine-2.6.0}/src/gather/recall.py +0 -0
- {gather_engine-2.5.0 → gather_engine-2.6.0}/src/gather/reddit.py +0 -0
- {gather_engine-2.5.0 → gather_engine-2.6.0}/src/gather/reddit_cmd.py +0 -0
- {gather_engine-2.5.0 → gather_engine-2.6.0}/src/gather/reddit_items.py +0 -0
- {gather_engine-2.5.0 → gather_engine-2.6.0}/src/gather/report.py +0 -0
- {gather_engine-2.5.0 → gather_engine-2.6.0}/src/gather/report_cmd.py +0 -0
- {gather_engine-2.5.0 → gather_engine-2.6.0}/src/gather/route.py +0 -0
- {gather_engine-2.5.0 → gather_engine-2.6.0}/src/gather/run.py +0 -0
- {gather_engine-2.5.0 → gather_engine-2.6.0}/src/gather/run_config.py +0 -0
- {gather_engine-2.5.0 → gather_engine-2.6.0}/src/gather/schema_extract.py +0 -0
- {gather_engine-2.5.0 → gather_engine-2.6.0}/src/gather/scholar.py +0 -0
- {gather_engine-2.5.0 → gather_engine-2.6.0}/src/gather/scope.py +0 -0
- {gather_engine-2.5.0 → gather_engine-2.6.0}/src/gather/search.py +0 -0
- {gather_engine-2.5.0 → gather_engine-2.6.0}/src/gather/source.py +0 -0
- {gather_engine-2.5.0 → gather_engine-2.6.0}/src/gather/spawn.py +0 -0
- {gather_engine-2.5.0 → gather_engine-2.6.0}/src/gather/store.py +0 -0
- {gather_engine-2.5.0 → gather_engine-2.6.0}/src/gather/stream.py +0 -0
- {gather_engine-2.5.0 → gather_engine-2.6.0}/src/gather/track.py +0 -0
- {gather_engine-2.5.0 → gather_engine-2.6.0}/src/gather/transcribe.py +0 -0
- {gather_engine-2.5.0 → gather_engine-2.6.0}/src/gather/video.py +0 -0
- {gather_engine-2.5.0 → gather_engine-2.6.0}/src/gather/video_cmd.py +0 -0
- {gather_engine-2.5.0 → gather_engine-2.6.0}/src/gather/web.py +0 -0
- {gather_engine-2.5.0 → gather_engine-2.6.0}/src/gather/web_commands.py +0 -0
- {gather_engine-2.5.0 → gather_engine-2.6.0}/src/gather/youtube_api.py +0 -0
- {gather_engine-2.5.0 → gather_engine-2.6.0}/src/gather/youtube_cmd.py +0 -0
- {gather_engine-2.5.0 → gather_engine-2.6.0}/src/gather/youtube_probe.py +0 -0
- {gather_engine-2.5.0 → gather_engine-2.6.0}/src/gather/youtube_route.py +0 -0
- {gather_engine-2.5.0 → gather_engine-2.6.0}/src/gather/ytdlp.py +0 -0
- {gather_engine-2.5.0 → gather_engine-2.6.0}/src/gather_engine.egg-info/dependency_links.txt +0 -0
- {gather_engine-2.5.0 → gather_engine-2.6.0}/src/gather_engine.egg-info/entry_points.txt +0 -0
- {gather_engine-2.5.0 → gather_engine-2.6.0}/src/gather_engine.egg-info/top_level.txt +0 -0
- {gather_engine-2.5.0 → gather_engine-2.6.0}/tests/test_api.py +0 -0
- {gather_engine-2.5.0 → gather_engine-2.6.0}/tests/test_arxiv.py +0 -0
- {gather_engine-2.5.0 → gather_engine-2.6.0}/tests/test_availability.py +0 -0
- {gather_engine-2.5.0 → gather_engine-2.6.0}/tests/test_backends.py +0 -0
- {gather_engine-2.5.0 → gather_engine-2.6.0}/tests/test_backends_browser.py +0 -0
- {gather_engine-2.5.0 → gather_engine-2.6.0}/tests/test_backends_card.py +0 -0
- {gather_engine-2.5.0 → gather_engine-2.6.0}/tests/test_benchmark.py +0 -0
- {gather_engine-2.5.0 → gather_engine-2.6.0}/tests/test_browser_evidence.py +0 -0
- {gather_engine-2.5.0 → gather_engine-2.6.0}/tests/test_cache.py +0 -0
- {gather_engine-2.5.0 → gather_engine-2.6.0}/tests/test_captions.py +0 -0
- {gather_engine-2.5.0 → gather_engine-2.6.0}/tests/test_channel.py +0 -0
- {gather_engine-2.5.0 → gather_engine-2.6.0}/tests/test_channel_ledger.py +0 -0
- {gather_engine-2.5.0 → gather_engine-2.6.0}/tests/test_citecheck.py +0 -0
- {gather_engine-2.5.0 → gather_engine-2.6.0}/tests/test_cli.py +0 -0
- {gather_engine-2.5.0 → gather_engine-2.6.0}/tests/test_cli_web.py +0 -0
- {gather_engine-2.5.0 → gather_engine-2.6.0}/tests/test_client_disclosure.py +0 -0
- {gather_engine-2.5.0 → gather_engine-2.6.0}/tests/test_client_network.py +0 -0
- {gather_engine-2.5.0 → gather_engine-2.6.0}/tests/test_client_package.py +0 -0
- {gather_engine-2.5.0 → gather_engine-2.6.0}/tests/test_client_profile.py +0 -0
- {gather_engine-2.5.0 → gather_engine-2.6.0}/tests/test_client_setup.py +0 -0
- {gather_engine-2.5.0 → gather_engine-2.6.0}/tests/test_client_vendored.py +0 -0
- {gather_engine-2.5.0 → gather_engine-2.6.0}/tests/test_comment_balance.py +0 -0
- {gather_engine-2.5.0 → gather_engine-2.6.0}/tests/test_context.py +0 -0
- {gather_engine-2.5.0 → gather_engine-2.6.0}/tests/test_context_mountinfo.py +0 -0
- {gather_engine-2.5.0 → gather_engine-2.6.0}/tests/test_crawl.py +0 -0
- {gather_engine-2.5.0 → gather_engine-2.6.0}/tests/test_credentials.py +0 -0
- {gather_engine-2.5.0 → gather_engine-2.6.0}/tests/test_demo.py +0 -0
- {gather_engine-2.5.0 → gather_engine-2.6.0}/tests/test_derive.py +0 -0
- {gather_engine-2.5.0 → gather_engine-2.6.0}/tests/test_device_names.py +0 -0
- {gather_engine-2.5.0 → gather_engine-2.6.0}/tests/test_digest.py +0 -0
- {gather_engine-2.5.0 → gather_engine-2.6.0}/tests/test_docs.py +0 -0
- {gather_engine-2.5.0 → gather_engine-2.6.0}/tests/test_dom.py +0 -0
- {gather_engine-2.5.0 → gather_engine-2.6.0}/tests/test_export.py +0 -0
- {gather_engine-2.5.0 → gather_engine-2.6.0}/tests/test_extract.py +0 -0
- {gather_engine-2.5.0 → gather_engine-2.6.0}/tests/test_fastparse.py +0 -0
- {gather_engine-2.5.0 → gather_engine-2.6.0}/tests/test_federation.py +0 -0
- {gather_engine-2.5.0 → gather_engine-2.6.0}/tests/test_federation_cli.py +0 -0
- {gather_engine-2.5.0 → gather_engine-2.6.0}/tests/test_federation_receipt.py +0 -0
- {gather_engine-2.5.0 → gather_engine-2.6.0}/tests/test_feed.py +0 -0
- {gather_engine-2.5.0 → gather_engine-2.6.0}/tests/test_fetch.py +0 -0
- {gather_engine-2.5.0 → gather_engine-2.6.0}/tests/test_filter_ledger.py +0 -0
- {gather_engine-2.5.0 → gather_engine-2.6.0}/tests/test_flagship_cli.py +0 -0
- {gather_engine-2.5.0 → gather_engine-2.6.0}/tests/test_grants.py +0 -0
- {gather_engine-2.5.0 → gather_engine-2.6.0}/tests/test_hard_sources.py +0 -0
- {gather_engine-2.5.0 → gather_engine-2.6.0}/tests/test_http_call.py +0 -0
- {gather_engine-2.5.0 → gather_engine-2.6.0}/tests/test_interop.py +0 -0
- {gather_engine-2.5.0 → gather_engine-2.6.0}/tests/test_item.py +0 -0
- {gather_engine-2.5.0 → gather_engine-2.6.0}/tests/test_localpath.py +0 -0
- {gather_engine-2.5.0 → gather_engine-2.6.0}/tests/test_mcp.py +0 -0
- {gather_engine-2.5.0 → gather_engine-2.6.0}/tests/test_mcp_annotations.py +0 -0
- {gather_engine-2.5.0 → gather_engine-2.6.0}/tests/test_method.py +0 -0
- {gather_engine-2.5.0 → gather_engine-2.6.0}/tests/test_model.py +0 -0
- {gather_engine-2.5.0 → gather_engine-2.6.0}/tests/test_monitor.py +0 -0
- {gather_engine-2.5.0 → gather_engine-2.6.0}/tests/test_net.py +0 -0
- {gather_engine-2.5.0 → gather_engine-2.6.0}/tests/test_nonlocal_paths.py +0 -0
- {gather_engine-2.5.0 → gather_engine-2.6.0}/tests/test_pacing.py +0 -0
- {gather_engine-2.5.0 → gather_engine-2.6.0}/tests/test_package.py +0 -0
- {gather_engine-2.5.0 → gather_engine-2.6.0}/tests/test_pdf.py +0 -0
- {gather_engine-2.5.0 → gather_engine-2.6.0}/tests/test_pilot.py +0 -0
- {gather_engine-2.5.0 → gather_engine-2.6.0}/tests/test_pilot_bundle.py +0 -0
- {gather_engine-2.5.0 → gather_engine-2.6.0}/tests/test_pilot_cli.py +0 -0
- {gather_engine-2.5.0 → gather_engine-2.6.0}/tests/test_pilot_manifest.py +0 -0
- {gather_engine-2.5.0 → gather_engine-2.6.0}/tests/test_pilot_refresh.py +0 -0
- {gather_engine-2.5.0 → gather_engine-2.6.0}/tests/test_pilot_report.py +0 -0
- {gather_engine-2.5.0 → gather_engine-2.6.0}/tests/test_pilot_sample.py +0 -0
- {gather_engine-2.5.0 → gather_engine-2.6.0}/tests/test_pilot_showcase.py +0 -0
- {gather_engine-2.5.0 → gather_engine-2.6.0}/tests/test_pilot_sources.py +0 -0
- {gather_engine-2.5.0 → gather_engine-2.6.0}/tests/test_provenance.py +0 -0
- {gather_engine-2.5.0 → gather_engine-2.6.0}/tests/test_recall.py +0 -0
- {gather_engine-2.5.0 → gather_engine-2.6.0}/tests/test_reddit.py +0 -0
- {gather_engine-2.5.0 → gather_engine-2.6.0}/tests/test_reddit_grants.py +0 -0
- {gather_engine-2.5.0 → gather_engine-2.6.0}/tests/test_release_workflow.py +0 -0
- {gather_engine-2.5.0 → gather_engine-2.6.0}/tests/test_repo_art.py +0 -0
- {gather_engine-2.5.0 → gather_engine-2.6.0}/tests/test_repo_card.py +0 -0
- {gather_engine-2.5.0 → gather_engine-2.6.0}/tests/test_report.py +0 -0
- {gather_engine-2.5.0 → gather_engine-2.6.0}/tests/test_report_quotes.py +0 -0
- {gather_engine-2.5.0 → gather_engine-2.6.0}/tests/test_route.py +0 -0
- {gather_engine-2.5.0 → gather_engine-2.6.0}/tests/test_run.py +0 -0
- {gather_engine-2.5.0 → gather_engine-2.6.0}/tests/test_schema_extract.py +0 -0
- {gather_engine-2.5.0 → gather_engine-2.6.0}/tests/test_scholar.py +0 -0
- {gather_engine-2.5.0 → gather_engine-2.6.0}/tests/test_scope.py +0 -0
- {gather_engine-2.5.0 → gather_engine-2.6.0}/tests/test_search.py +0 -0
- {gather_engine-2.5.0 → gather_engine-2.6.0}/tests/test_spawn_children.py +0 -0
- {gather_engine-2.5.0 → gather_engine-2.6.0}/tests/test_spawn_working_folder.py +0 -0
- {gather_engine-2.5.0 → gather_engine-2.6.0}/tests/test_store.py +0 -0
- {gather_engine-2.5.0 → gather_engine-2.6.0}/tests/test_stream.py +0 -0
- {gather_engine-2.5.0 → gather_engine-2.6.0}/tests/test_track.py +0 -0
- {gather_engine-2.5.0 → gather_engine-2.6.0}/tests/test_vendored.py +0 -0
- {gather_engine-2.5.0 → gather_engine-2.6.0}/tests/test_verify_pilot_cli.py +0 -0
- {gather_engine-2.5.0 → gather_engine-2.6.0}/tests/test_video.py +0 -0
- {gather_engine-2.5.0 → gather_engine-2.6.0}/tests/test_video_bundle.py +0 -0
- {gather_engine-2.5.0 → gather_engine-2.6.0}/tests/test_video_source.py +0 -0
- {gather_engine-2.5.0 → gather_engine-2.6.0}/tests/test_video_spawn.py +0 -0
- {gather_engine-2.5.0 → gather_engine-2.6.0}/tests/test_web.py +0 -0
- {gather_engine-2.5.0 → gather_engine-2.6.0}/tests/test_web_refine.py +0 -0
- {gather_engine-2.5.0 → gather_engine-2.6.0}/tests/test_xml_safety.py +0 -0
- {gather_engine-2.5.0 → gather_engine-2.6.0}/tests/test_youtube_api.py +0 -0
- {gather_engine-2.5.0 → gather_engine-2.6.0}/tests/test_youtube_grants.py +0 -0
- {gather_engine-2.5.0 → gather_engine-2.6.0}/tests/test_youtube_probe.py +0 -0
- {gather_engine-2.5.0 → gather_engine-2.6.0}/tests/test_youtube_route.py +0 -0
- {gather_engine-2.5.0 → gather_engine-2.6.0}/tests/test_ytdlp.py +0 -0
- {gather_engine-2.5.0 → gather_engine-2.6.0}/tests/test_ytdlp_real.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: gather-engine
|
|
3
|
-
Version: 2.
|
|
3
|
+
Version: 2.6.0
|
|
4
4
|
Summary: Collect web pages, papers, APIs, PDFs, scans, audio and video into a local corpus where every item records how it arrived.
|
|
5
5
|
Author-email: Zain Dana Harper <zaindharper@gmail.com>
|
|
6
6
|
License-Expression: AGPL-3.0-only
|
|
@@ -24,13 +24,17 @@ Requires-Dist: pytest>=8; extra == "dev"
|
|
|
24
24
|
Requires-Dist: pytest-cov>=5; extra == "dev"
|
|
25
25
|
Requires-Dist: ruff>=0.6; extra == "dev"
|
|
26
26
|
Requires-Dist: mypy>=1.10; extra == "dev"
|
|
27
|
+
Requires-Dist: Pillow>=10.1; extra == "dev"
|
|
27
28
|
Provides-Extra: fast
|
|
28
29
|
Requires-Dist: lxml>=5; extra == "fast"
|
|
29
30
|
Provides-Extra: browser
|
|
30
31
|
Requires-Dist: playwright>=1.4; extra == "browser"
|
|
32
|
+
Provides-Extra: frames
|
|
33
|
+
Requires-Dist: Pillow>=10.1; extra == "frames"
|
|
31
34
|
Provides-Extra: all
|
|
32
35
|
Requires-Dist: lxml>=5; extra == "all"
|
|
33
36
|
Requires-Dist: playwright>=1.4; extra == "all"
|
|
37
|
+
Requires-Dist: Pillow>=10.1; extra == "all"
|
|
34
38
|
Dynamic: license-file
|
|
35
39
|
|
|
36
40
|
<picture>
|
|
@@ -46,11 +50,11 @@ Gather is for researchers, journalists and developers who feed sources to a pers
|
|
|
46
50
|
|
|
47
51
|
## Install and first run
|
|
48
52
|
|
|
49
|
-
Python 3.11 or newer. Version 2.
|
|
53
|
+
Python 3.11 or newer. Version 2.6.0 is on PyPI as `gather-engine`.
|
|
50
54
|
|
|
51
55
|
```bash
|
|
52
56
|
pip install gather-engine
|
|
53
|
-
gather --version # gather 2.
|
|
57
|
+
gather --version # gather 2.6.0
|
|
54
58
|
```
|
|
55
59
|
|
|
56
60
|
Extract a saved page into hashed blocks, each tied to its place in the page:
|
|
@@ -124,6 +128,7 @@ fabricated field cannot reach the corpus by being plausible.
|
|
|
124
128
|
- **Hard-source adapters behind one shape.** Video with captions and comments (`yt-dlp` first, with the official YouTube Data API as a fallback when you supply your own key; each item records which path served it and how fast), Reddit through its official Data API with your own app credentials, static web, RSS/Atom feeds, local docs, arXiv, PDFs (`pdftotext`), authenticated JSON APIs (token from env, never logged), JS-rendered pages (headless Chromium), scanned images (`tesseract`), and audio (`whisper`). Each external tool is optional and only needed for its own adapter.
|
|
125
129
|
- **Cited reports, checked in code.** `gather report` asks a model on your own machine to answer from fixed excerpts with exact quotes, then checks every quote against the excerpt it cites and marks any it cannot verify. An answer that cites an excerpt without quoting it is sent back for a rewrite, up to twice.
|
|
126
130
|
- **Filter ledgers.** `--scope TERMS --ledger DIR` writes one row per dropped item with its reason code, plus the input, so `gather ledger verify` can recompute every count.
|
|
131
|
+
- **A video as one checkable bundle, with frames.** `gather video-bundle <url> --out DIR` saves the metadata and chapters, the caption track and transcripts, the comment tree, a balanced comment synthesis, and contact sheets of 24 to 96 frames across the whole video with the time printed on each. The frames come from the site's preview storyboard, so no video file is downloaded; they are small (320 by 180 pixels at best on YouTube) and the bundle records their real size. `--fast` reads captions and frames while the comments are still being read and keeps requests 1 second apart: a 234-second video that took 162 seconds with 3-second sleeps took 37 seconds. Frame sheets need the optional `frames` extra (Pillow). See [USAGE.md](https://github.com/HarperZ9/gather/blob/main/USAGE.md).
|
|
127
132
|
- **Channel and playlist intake.** `gather channel <url> --store DIR` lists a channel's videos, shorts, and streams tabs (or one playlist) and gathers every entry with bounded concurrency, resumable from a per-pass ledger. Metadata and comments run as one pass and captions as another (`--no-captions`, `--captions-only`), with polite pacing, bounded backoff on HTTP 429 and session rate limits, a stop at the first bot check (never retried or answered), one caption track per video, and a run summary that counts every outcome by reason.
|
|
128
133
|
- **Scholarly-graph federation.** `gather scholar` queries OpenAlex, Semantic Scholar, and Crossref in one call, dedupes results by normalized DOI (never a fuzzy title match), and can capture citation edges as first-class records with `--edges`.
|
|
129
134
|
- **A durable local corpus.** Any fetch command takes `--store DIR`: bodies are content-addressed and deduped by hash, new writes preserve exact UTF-8 source bytes, and `gather corpus list|verify|digest|runs|search|stats|prune|availability|context` inspects, re-checks, and queries what you stored.
|
|
@@ -220,6 +225,24 @@ gather docs ./research-notes --scope "rubik,group theory"
|
|
|
220
225
|
gather corpus verify ./corpus # re-hash every stored body; non-zero exit on corruption
|
|
221
226
|
```
|
|
222
227
|
|
|
228
|
+
Turn a folder of posts saved by the CMMNS browser extension into tables, offline and by fixed rules:
|
|
229
|
+
|
|
230
|
+
```bash
|
|
231
|
+
gather saves ./saves --out ./saves-tables # items, entities and media as CSV and JSON Lines
|
|
232
|
+
gather saves --probe ./clip.mp4 # container, size, duration and audio fields of one file
|
|
233
|
+
```
|
|
234
|
+
|
|
235
|
+
The same folder gives byte-identical tables on every run. The language column is a rule and the entities are regular expressions; [USAGE.md](https://github.com/HarperZ9/gather/blob/main/USAGE.md#saved-posts-as-tables) lists the columns and the limits.
|
|
236
|
+
|
|
237
|
+
Take any local file apart into a tree of its parts, then rebuild it byte for byte:
|
|
238
|
+
|
|
239
|
+
```bash
|
|
240
|
+
gather decompose ./clip.wav --out ./clip-tree # tree.json, parts/, and measurement tables
|
|
241
|
+
gather rebuild ./clip-tree --out ./clip-again.wav # same SHA-256, every node checked
|
|
242
|
+
```
|
|
243
|
+
|
|
244
|
+
ZIP, PNG, WAV, JSON, text and CMMNS saves get structural handlers; every other file is kept as one byte range and still rebuilds exactly. PCM WAV files also get loudness (BS.1770), tempo, key, spectrum and decay estimates. [USAGE.md](https://github.com/HarperZ9/gather/blob/main/USAGE.md#take-a-file-apart-and-put-it-back) lists the record, the handlers and the limits.
|
|
245
|
+
|
|
223
246
|
Offline demo, no install of extra tools, nothing downloaded:
|
|
224
247
|
|
|
225
248
|
```bash
|
|
@@ -302,7 +325,7 @@ The `web` adapter reads static HTML and does not run JavaScript; a client-render
|
|
|
302
325
|
- [docs/WEB-ENGINE-UPLIFT.md](https://github.com/HarperZ9/gather/blob/main/docs/WEB-ENGINE-UPLIFT.md): the web-data engine roadmap and benchmarks.
|
|
303
326
|
- [docs/ENTERPRISE-READINESS.md](https://github.com/HarperZ9/gather/blob/main/docs/ENTERPRISE-READINESS.md): context envelopes, action receipts, and host-neutral operation for unattended agents.
|
|
304
327
|
- [docs/PILOT.md](https://github.com/HarperZ9/gather/blob/main/docs/PILOT.md): the accountable pilot evidence engine, its manifest boundary, and the private/shared evidence split.
|
|
305
|
-
- [CHANGELOG.md](https://github.com/HarperZ9/gather/blob/main/CHANGELOG.md): version history. Current release: 2.
|
|
328
|
+
- [CHANGELOG.md](https://github.com/HarperZ9/gather/blob/main/CHANGELOG.md): version history. Current release: 2.6.0.
|
|
306
329
|
|
|
307
330
|
Peer projects: [crucible](https://github.com/HarperZ9/crucible) (judgment), [index](https://github.com/HarperZ9/index) (code maps), [forum](https://github.com/HarperZ9/forum) (orchestration), [telos](https://github.com/HarperZ9/telos) (the engine).
|
|
308
331
|
|
|
@@ -1,38 +1,3 @@
|
|
|
1
|
-
Metadata-Version: 2.4
|
|
2
|
-
Name: gather-engine
|
|
3
|
-
Version: 2.5.0
|
|
4
|
-
Summary: Collect web pages, papers, APIs, PDFs, scans, audio and video into a local corpus where every item records how it arrived.
|
|
5
|
-
Author-email: Zain Dana Harper <zaindharper@gmail.com>
|
|
6
|
-
License-Expression: AGPL-3.0-only
|
|
7
|
-
Project-URL: Homepage, https://harperz9.github.io/gather.html
|
|
8
|
-
Project-URL: Repository, https://github.com/HarperZ9/gather
|
|
9
|
-
Project-URL: Issues, https://github.com/HarperZ9/gather/issues
|
|
10
|
-
Project-URL: Changelog, https://github.com/HarperZ9/gather/blob/main/CHANGELOG.md
|
|
11
|
-
Keywords: research,intake,ingestion,provenance,transcripts,arxiv,accountability,ocr,transcription,headless-browser,synthesis,web-scraping,ssrf-safe
|
|
12
|
-
Classifier: Development Status :: 4 - Beta
|
|
13
|
-
Classifier: Environment :: Console
|
|
14
|
-
Classifier: Intended Audience :: Developers
|
|
15
|
-
Classifier: Programming Language :: Python :: 3
|
|
16
|
-
Classifier: Programming Language :: Python :: 3 :: Only
|
|
17
|
-
Classifier: Topic :: Scientific/Engineering :: Artificial Intelligence
|
|
18
|
-
Requires-Python: >=3.11
|
|
19
|
-
Description-Content-Type: text/markdown
|
|
20
|
-
License-File: LICENSE
|
|
21
|
-
License-File: NOTICE
|
|
22
|
-
Provides-Extra: dev
|
|
23
|
-
Requires-Dist: pytest>=8; extra == "dev"
|
|
24
|
-
Requires-Dist: pytest-cov>=5; extra == "dev"
|
|
25
|
-
Requires-Dist: ruff>=0.6; extra == "dev"
|
|
26
|
-
Requires-Dist: mypy>=1.10; extra == "dev"
|
|
27
|
-
Provides-Extra: fast
|
|
28
|
-
Requires-Dist: lxml>=5; extra == "fast"
|
|
29
|
-
Provides-Extra: browser
|
|
30
|
-
Requires-Dist: playwright>=1.4; extra == "browser"
|
|
31
|
-
Provides-Extra: all
|
|
32
|
-
Requires-Dist: lxml>=5; extra == "all"
|
|
33
|
-
Requires-Dist: playwright>=1.4; extra == "all"
|
|
34
|
-
Dynamic: license-file
|
|
35
|
-
|
|
36
1
|
<picture>
|
|
37
2
|
<source media="(prefers-color-scheme: dark)" srcset="https://raw.githubusercontent.com/HarperZ9/gather/main/docs/art/hero-dark.svg">
|
|
38
3
|
<img src="https://raw.githubusercontent.com/HarperZ9/gather/main/docs/art/hero-light.svg" alt="gather: Research intake for web pages, papers, scanned PDFs, audio and video. Five bundles of lines, solid, dashed, dotted, wavy and stepped, sweep in from the right and pass through a ring into one bright core. One line stops at the ring with a short cross mark." width="100%">
|
|
@@ -46,11 +11,11 @@ Gather is for researchers, journalists and developers who feed sources to a pers
|
|
|
46
11
|
|
|
47
12
|
## Install and first run
|
|
48
13
|
|
|
49
|
-
Python 3.11 or newer. Version 2.
|
|
14
|
+
Python 3.11 or newer. Version 2.6.0 is on PyPI as `gather-engine`.
|
|
50
15
|
|
|
51
16
|
```bash
|
|
52
17
|
pip install gather-engine
|
|
53
|
-
gather --version # gather 2.
|
|
18
|
+
gather --version # gather 2.6.0
|
|
54
19
|
```
|
|
55
20
|
|
|
56
21
|
Extract a saved page into hashed blocks, each tied to its place in the page:
|
|
@@ -124,6 +89,7 @@ fabricated field cannot reach the corpus by being plausible.
|
|
|
124
89
|
- **Hard-source adapters behind one shape.** Video with captions and comments (`yt-dlp` first, with the official YouTube Data API as a fallback when you supply your own key; each item records which path served it and how fast), Reddit through its official Data API with your own app credentials, static web, RSS/Atom feeds, local docs, arXiv, PDFs (`pdftotext`), authenticated JSON APIs (token from env, never logged), JS-rendered pages (headless Chromium), scanned images (`tesseract`), and audio (`whisper`). Each external tool is optional and only needed for its own adapter.
|
|
125
90
|
- **Cited reports, checked in code.** `gather report` asks a model on your own machine to answer from fixed excerpts with exact quotes, then checks every quote against the excerpt it cites and marks any it cannot verify. An answer that cites an excerpt without quoting it is sent back for a rewrite, up to twice.
|
|
126
91
|
- **Filter ledgers.** `--scope TERMS --ledger DIR` writes one row per dropped item with its reason code, plus the input, so `gather ledger verify` can recompute every count.
|
|
92
|
+
- **A video as one checkable bundle, with frames.** `gather video-bundle <url> --out DIR` saves the metadata and chapters, the caption track and transcripts, the comment tree, a balanced comment synthesis, and contact sheets of 24 to 96 frames across the whole video with the time printed on each. The frames come from the site's preview storyboard, so no video file is downloaded; they are small (320 by 180 pixels at best on YouTube) and the bundle records their real size. `--fast` reads captions and frames while the comments are still being read and keeps requests 1 second apart: a 234-second video that took 162 seconds with 3-second sleeps took 37 seconds. Frame sheets need the optional `frames` extra (Pillow). See [USAGE.md](https://github.com/HarperZ9/gather/blob/main/USAGE.md).
|
|
127
93
|
- **Channel and playlist intake.** `gather channel <url> --store DIR` lists a channel's videos, shorts, and streams tabs (or one playlist) and gathers every entry with bounded concurrency, resumable from a per-pass ledger. Metadata and comments run as one pass and captions as another (`--no-captions`, `--captions-only`), with polite pacing, bounded backoff on HTTP 429 and session rate limits, a stop at the first bot check (never retried or answered), one caption track per video, and a run summary that counts every outcome by reason.
|
|
128
94
|
- **Scholarly-graph federation.** `gather scholar` queries OpenAlex, Semantic Scholar, and Crossref in one call, dedupes results by normalized DOI (never a fuzzy title match), and can capture citation edges as first-class records with `--edges`.
|
|
129
95
|
- **A durable local corpus.** Any fetch command takes `--store DIR`: bodies are content-addressed and deduped by hash, new writes preserve exact UTF-8 source bytes, and `gather corpus list|verify|digest|runs|search|stats|prune|availability|context` inspects, re-checks, and queries what you stored.
|
|
@@ -220,6 +186,24 @@ gather docs ./research-notes --scope "rubik,group theory"
|
|
|
220
186
|
gather corpus verify ./corpus # re-hash every stored body; non-zero exit on corruption
|
|
221
187
|
```
|
|
222
188
|
|
|
189
|
+
Turn a folder of posts saved by the CMMNS browser extension into tables, offline and by fixed rules:
|
|
190
|
+
|
|
191
|
+
```bash
|
|
192
|
+
gather saves ./saves --out ./saves-tables # items, entities and media as CSV and JSON Lines
|
|
193
|
+
gather saves --probe ./clip.mp4 # container, size, duration and audio fields of one file
|
|
194
|
+
```
|
|
195
|
+
|
|
196
|
+
The same folder gives byte-identical tables on every run. The language column is a rule and the entities are regular expressions; [USAGE.md](https://github.com/HarperZ9/gather/blob/main/USAGE.md#saved-posts-as-tables) lists the columns and the limits.
|
|
197
|
+
|
|
198
|
+
Take any local file apart into a tree of its parts, then rebuild it byte for byte:
|
|
199
|
+
|
|
200
|
+
```bash
|
|
201
|
+
gather decompose ./clip.wav --out ./clip-tree # tree.json, parts/, and measurement tables
|
|
202
|
+
gather rebuild ./clip-tree --out ./clip-again.wav # same SHA-256, every node checked
|
|
203
|
+
```
|
|
204
|
+
|
|
205
|
+
ZIP, PNG, WAV, JSON, text and CMMNS saves get structural handlers; every other file is kept as one byte range and still rebuilds exactly. PCM WAV files also get loudness (BS.1770), tempo, key, spectrum and decay estimates. [USAGE.md](https://github.com/HarperZ9/gather/blob/main/USAGE.md#take-a-file-apart-and-put-it-back) lists the record, the handlers and the limits.
|
|
206
|
+
|
|
223
207
|
Offline demo, no install of extra tools, nothing downloaded:
|
|
224
208
|
|
|
225
209
|
```bash
|
|
@@ -302,7 +286,7 @@ The `web` adapter reads static HTML and does not run JavaScript; a client-render
|
|
|
302
286
|
- [docs/WEB-ENGINE-UPLIFT.md](https://github.com/HarperZ9/gather/blob/main/docs/WEB-ENGINE-UPLIFT.md): the web-data engine roadmap and benchmarks.
|
|
303
287
|
- [docs/ENTERPRISE-READINESS.md](https://github.com/HarperZ9/gather/blob/main/docs/ENTERPRISE-READINESS.md): context envelopes, action receipts, and host-neutral operation for unattended agents.
|
|
304
288
|
- [docs/PILOT.md](https://github.com/HarperZ9/gather/blob/main/docs/PILOT.md): the accountable pilot evidence engine, its manifest boundary, and the private/shared evidence split.
|
|
305
|
-
- [CHANGELOG.md](https://github.com/HarperZ9/gather/blob/main/CHANGELOG.md): version history. Current release: 2.
|
|
289
|
+
- [CHANGELOG.md](https://github.com/HarperZ9/gather/blob/main/CHANGELOG.md): version history. Current release: 2.6.0.
|
|
306
290
|
|
|
307
291
|
Peer projects: [crucible](https://github.com/HarperZ9/crucible) (judgment), [index](https://github.com/HarperZ9/index) (code maps), [forum](https://github.com/HarperZ9/forum) (orchestration), [telos](https://github.com/HarperZ9/telos) (the engine).
|
|
308
292
|
|
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "gather-engine"
|
|
7
|
-
version = "2.
|
|
7
|
+
version = "2.6.0"
|
|
8
8
|
description = "Collect web pages, papers, APIs, PDFs, scans, audio and video into a local corpus where every item records how it arrived."
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
requires-python = ">=3.11"
|
|
@@ -26,12 +26,13 @@ classifiers = [
|
|
|
26
26
|
dependencies = []
|
|
27
27
|
|
|
28
28
|
[project.optional-dependencies]
|
|
29
|
-
dev = ["pytest>=8", "pytest-cov>=5", "ruff>=0.6", "mypy>=1.10"]
|
|
29
|
+
dev = ["pytest>=8", "pytest-cov>=5", "ruff>=0.6", "mypy>=1.10", "Pillow>=10.1"]
|
|
30
30
|
# Capability backends (wedge 5 parity layer). The core stays zero-dep; these are
|
|
31
31
|
# opt-in edges. Missing edge => honest UNVERIFIABLE or stdlib fallback, never a fake.
|
|
32
32
|
fast = ["lxml>=5"] # native-speed parsing (2x on large docs); stdlib is the fallback
|
|
33
33
|
browser = ["playwright>=1.4"] # JS render (needs `playwright install chromium`)
|
|
34
|
-
|
|
34
|
+
frames = ["Pillow>=10.1"] # contact sheets of video frames (gather video-bundle); without it the sheets are not cut
|
|
35
|
+
all = ["lxml>=5", "playwright>=1.4", "Pillow>=10.1"]
|
|
35
36
|
|
|
36
37
|
[project.scripts]
|
|
37
38
|
gather = "gather.cli:main"
|
|
@@ -28,7 +28,7 @@ from gather.scope import filter_scope, in_scope
|
|
|
28
28
|
from gather.source import Catalog, Source
|
|
29
29
|
from gather.store import Corpus
|
|
30
30
|
|
|
31
|
-
__version__ = "2.
|
|
31
|
+
__version__ = "2.6.0"
|
|
32
32
|
|
|
33
33
|
__all__ = [
|
|
34
34
|
"Catalog", "Corpus", "CorpusRootDescriptor", "CorpusRootIdentity", "Digest", "Item", "NullProvenanceProvider", "NullSynthesizer",
|
|
@@ -6,9 +6,11 @@ from collections.abc import Callable
|
|
|
6
6
|
|
|
7
7
|
|
|
8
8
|
def add_extension_parsers(sub, add_common: Callable) -> None:
|
|
9
|
+
from gather.decompose_cmd import add_decompose_parsers
|
|
9
10
|
from gather.ledger_cmd import add_ledger_parser
|
|
10
11
|
from gather.reddit_cmd import add_reddit_parser
|
|
11
12
|
from gather.report_cmd import add_report_parsers
|
|
13
|
+
from gather.saves_cmd import add_saves_parser
|
|
12
14
|
from gather.video_bundle_cmd import add_bundle_parsers
|
|
13
15
|
from gather.video_cmd import add_channel_parser
|
|
14
16
|
from gather.youtube_cmd import add_video_parsers
|
|
@@ -19,3 +21,5 @@ def add_extension_parsers(sub, add_common: Callable) -> None:
|
|
|
19
21
|
add_reddit_parser(sub, add_common)
|
|
20
22
|
add_report_parsers(sub, add_common)
|
|
21
23
|
add_ledger_parser(sub)
|
|
24
|
+
add_saves_parser(sub)
|
|
25
|
+
add_decompose_parsers(sub)
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
"""Structural handlers for PNG and WAV, and the raw fallback.
|
|
2
|
+
|
|
3
|
+
Each ``build_*`` takes a ``Maker`` and returns the root node for the whole source. A handler
|
|
4
|
+
raises ``ValueError`` or ``struct.error`` when the bytes are not what it expects, and the front
|
|
5
|
+
door then tries the next handler. Bytes after the last understood chunk stay in the tree as
|
|
6
|
+
``raw`` nodes.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
import struct
|
|
12
|
+
import zlib
|
|
13
|
+
|
|
14
|
+
from gather.decompose_tree import Maker
|
|
15
|
+
|
|
16
|
+
PNG_SIGNATURE = b"\x89PNG\r\n\x1a\n"
|
|
17
|
+
_MAX_CHUNKS = 200000
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def build_raw(m: Maker, ctx, depth: int) -> dict:
|
|
21
|
+
"""The whole source as one byte range."""
|
|
22
|
+
return m.leaf("raw", 0, m.src.size)
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def _crc(m: Maker, start: int, length: int) -> int:
|
|
26
|
+
value = 0
|
|
27
|
+
for data in m.src.chunks(start, length):
|
|
28
|
+
value = zlib.crc32(data, value)
|
|
29
|
+
return value
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def _ihdr(data: bytes) -> dict:
|
|
33
|
+
width, height, depth, colour, compression, filtering, interlace = struct.unpack(">IIBBBBB", data[:13])
|
|
34
|
+
return {"width": width, "height": height, "bit_depth": depth, "colour_type": colour, "interlace": interlace}
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def _png_chunk(m: Maker, pos: int) -> dict | None:
|
|
38
|
+
"""One chunk at ``pos`` as length, type, data and CRC fields, or None when it does not fit."""
|
|
39
|
+
head = m.src.read(pos, 8)
|
|
40
|
+
if len(head) < 8:
|
|
41
|
+
return None
|
|
42
|
+
length, kind = struct.unpack(">I4s", head)
|
|
43
|
+
if pos + 12 + length > m.src.size:
|
|
44
|
+
return None
|
|
45
|
+
stored = struct.unpack(">I", m.src.read(pos + 8 + length, 4))[0]
|
|
46
|
+
name = kind.decode("latin-1")
|
|
47
|
+
fields = [m.leaf("length", pos, 4, value=length), m.leaf("type", pos + 4, 4, value=name)]
|
|
48
|
+
if length:
|
|
49
|
+
fields.append(m.leaf("data", pos + 8, length))
|
|
50
|
+
ok = _crc(m, pos + 4, 4 + length) == stored
|
|
51
|
+
fields.append(m.leaf("crc", pos + 8 + length, 4, value=f"{stored:08x}", matches=ok))
|
|
52
|
+
params = {"type": name, "crc_ok": ok}
|
|
53
|
+
if kind == b"IHDR" and length >= 13:
|
|
54
|
+
params.update(_ihdr(m.src.read(pos + 8, 13)))
|
|
55
|
+
return m.group("png-chunk", fields, **params)
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def build_png(m: Maker, ctx, depth: int) -> dict:
|
|
59
|
+
if m.src.read(0, 8) != PNG_SIGNATURE:
|
|
60
|
+
raise ValueError("not a PNG signature")
|
|
61
|
+
parts = [m.leaf("png-signature", 0, 8)]
|
|
62
|
+
pos = 8
|
|
63
|
+
for _ in range(_MAX_CHUNKS):
|
|
64
|
+
chunk = _png_chunk(m, pos)
|
|
65
|
+
if chunk is None:
|
|
66
|
+
break
|
|
67
|
+
parts.append(chunk)
|
|
68
|
+
pos += chunk["length"]
|
|
69
|
+
return m.cover("png", 0, m.src.size, parts, chunks=len(parts) - 1)
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def _fmt(data: bytes) -> dict:
|
|
73
|
+
tag, channels, rate, byte_rate, align, bits = struct.unpack("<HHIIHH", data[:16])
|
|
74
|
+
return {"format_tag": tag, "channels": channels, "sample_rate": rate, "byte_rate": byte_rate,
|
|
75
|
+
"block_align": align, "bits_per_sample": bits}
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
def _wav_data(m: Maker, start: int, length: int, fmt: dict) -> list[dict]:
|
|
79
|
+
"""The data chunk's payload as sample frames, plus any bytes short of a whole frame."""
|
|
80
|
+
align = fmt.get("block_align", 0)
|
|
81
|
+
if not align or not length:
|
|
82
|
+
return [m.leaf("data", start, length)] if length else []
|
|
83
|
+
whole = length - length % align
|
|
84
|
+
parts = [m.leaf("pcm-frames", start, whole, unit="sample frame", unit_size=align, unit_count=whole // align,
|
|
85
|
+
channels=fmt["channels"], bits_per_sample=fmt["bits_per_sample"])] if whole else []
|
|
86
|
+
if length - whole:
|
|
87
|
+
parts.append(m.leaf("raw", start + whole, length - whole))
|
|
88
|
+
return parts
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
def _wav_chunk(m: Maker, pos: int, fmt: dict) -> dict | None:
|
|
92
|
+
head = m.src.read(pos, 8)
|
|
93
|
+
if len(head) < 8:
|
|
94
|
+
return None
|
|
95
|
+
ident, length = struct.unpack("<4sI", head)
|
|
96
|
+
if pos + 8 + length > m.src.size:
|
|
97
|
+
return None
|
|
98
|
+
name = ident.decode("latin-1")
|
|
99
|
+
fields = [m.leaf("id", pos, 4, value=name), m.leaf("size", pos + 4, 4, value=length)]
|
|
100
|
+
params: dict = {"id": name}
|
|
101
|
+
if ident == b"fmt " and length >= 16:
|
|
102
|
+
fmt.update(_fmt(m.src.read(pos + 8, 16)))
|
|
103
|
+
params.update(fmt)
|
|
104
|
+
if ident == b"data":
|
|
105
|
+
fields += _wav_data(m, pos + 8, length, fmt)
|
|
106
|
+
elif length:
|
|
107
|
+
fields.append(m.leaf("data", pos + 8, length))
|
|
108
|
+
if length & 1 and pos + 8 + length < m.src.size:
|
|
109
|
+
fields.append(m.leaf("pad", pos + 8 + length, 1))
|
|
110
|
+
return m.group("riff-chunk", fields, **params)
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
def build_wav(m: Maker, ctx, depth: int) -> dict:
|
|
114
|
+
head = m.src.read(0, 12)
|
|
115
|
+
if len(head) < 12 or head[:4] != b"RIFF" or head[8:12] != b"WAVE":
|
|
116
|
+
raise ValueError("not a RIFF WAVE header")
|
|
117
|
+
parts = [m.leaf("riff-id", 0, 4, value="RIFF"), m.leaf("riff-size", 4, 4, value=struct.unpack("<I", head[4:8])[0]),
|
|
118
|
+
m.leaf("form", 8, 4, value="WAVE")]
|
|
119
|
+
fmt: dict = {}
|
|
120
|
+
pos = 12
|
|
121
|
+
for _ in range(_MAX_CHUNKS):
|
|
122
|
+
chunk = _wav_chunk(m, pos, fmt)
|
|
123
|
+
if chunk is None:
|
|
124
|
+
break
|
|
125
|
+
parts.append(chunk)
|
|
126
|
+
pos += chunk["length"]
|
|
127
|
+
return m.cover("wav", 0, m.src.size, parts, **fmt)
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
"""CLI glue for ``gather decompose`` and ``gather rebuild``.
|
|
2
|
+
|
|
3
|
+
``gather decompose FILE --out DIR`` takes one local file apart into a tree (``tree.json`` plus
|
|
4
|
+
``parts/``) by fixed rules: the handler is chosen from the file's bytes, every byte is kept, and
|
|
5
|
+
the same file gives the same tree. ``gather rebuild DIR --out FILE`` puts the file back together
|
|
6
|
+
byte for byte and checks every node; ``--verify`` checks without writing.
|
|
7
|
+
|
|
8
|
+
Exit status: 0 on success; 1 when a rebuild or a verify finds a node that fails its check; 2 on
|
|
9
|
+
bad options, an input that is not a local file, or a tree that cannot be read.
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
from __future__ import annotations
|
|
13
|
+
|
|
14
|
+
import json
|
|
15
|
+
import sys
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def add_decompose_parsers(sub) -> None:
|
|
19
|
+
d = sub.add_parser("decompose", help="take one local file apart into a tree of its parts, offline, by fixed rules")
|
|
20
|
+
d.add_argument("file", help="a local file; it is read as data and never executed")
|
|
21
|
+
d.add_argument("--out", required=True, metavar="DIR", help="an empty or new folder for tree.json and parts/")
|
|
22
|
+
d.add_argument("--no-content", action="store_true", help="write the structural tree only, no measurement tables")
|
|
23
|
+
d.add_argument("--json", action="store_true", help="print the summary as JSON")
|
|
24
|
+
d.set_defaults(func=cmd_decompose)
|
|
25
|
+
|
|
26
|
+
r = sub.add_parser("rebuild", help="rebuild a file byte for byte from a decompose tree, checking every node")
|
|
27
|
+
r.add_argument("tree", help="a folder written by gather decompose")
|
|
28
|
+
r.add_argument("--out", metavar="FILE", help="the file to write")
|
|
29
|
+
r.add_argument("--verify", action="store_true", help="check the tree without writing a file")
|
|
30
|
+
r.add_argument("--companion", metavar="NAME", help="rebuild a companion file recorded in the tree instead")
|
|
31
|
+
r.add_argument("--json", action="store_true", help="print the report as JSON")
|
|
32
|
+
r.set_defaults(func=cmd_rebuild)
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def summary(doc: dict) -> dict:
|
|
36
|
+
"""The facts about a tree worth printing: no node list, no local path."""
|
|
37
|
+
return {"format": doc["format"], "source": doc["source"], "handler": doc["handler"], "declined": doc["declined"],
|
|
38
|
+
"rebuild": doc["rebuild"], "unparsed_bytes": sum(r["length"] for r in doc["unparsed"]),
|
|
39
|
+
"companions": [c["name"] for c in doc.get("companions", [])],
|
|
40
|
+
"content": {row["component"]: row["status"] for row in doc.get("content", [])}}
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def cmd_decompose(args) -> int:
|
|
44
|
+
from gather.decompose_run import decompose_file
|
|
45
|
+
from gather.localpath import require_local
|
|
46
|
+
|
|
47
|
+
try:
|
|
48
|
+
doc = decompose_file(require_local(args.file, label="file"), args.out, content=not args.no_content)
|
|
49
|
+
except (OSError, ValueError) as exc:
|
|
50
|
+
print(f"decompose: {exc}", file=sys.stderr)
|
|
51
|
+
return 2
|
|
52
|
+
facts = summary(doc)
|
|
53
|
+
if args.json:
|
|
54
|
+
print(json.dumps(facts, indent=2))
|
|
55
|
+
else:
|
|
56
|
+
print(f"decompose: {facts['source']['name']} ({facts['source']['bytes']} bytes) by {facts['handler']}; "
|
|
57
|
+
f"rebuild {facts['rebuild']}; {facts['unparsed_bytes']} bytes kept as raw ranges; "
|
|
58
|
+
f"{len(facts['content'])} content components; tree in {args.out}")
|
|
59
|
+
return 0
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def cmd_rebuild(args) -> int:
|
|
63
|
+
from gather.decompose_rebuild import rebuild
|
|
64
|
+
|
|
65
|
+
if bool(args.out) == bool(args.verify):
|
|
66
|
+
print("rebuild: give --out FILE to rebuild, or --verify to check only", file=sys.stderr)
|
|
67
|
+
return 2
|
|
68
|
+
try:
|
|
69
|
+
report = rebuild(args.tree, args.out, args.companion)
|
|
70
|
+
except (OSError, ValueError) as exc:
|
|
71
|
+
print(f"rebuild: {exc}", file=sys.stderr)
|
|
72
|
+
return 2
|
|
73
|
+
if args.json:
|
|
74
|
+
print(json.dumps(report, indent=2))
|
|
75
|
+
else:
|
|
76
|
+
print(f"rebuild: {report['bytes']} bytes, sha256 {report['sha256']}, "
|
|
77
|
+
f"{'matches the original' if report['ok'] else 'DOES NOT match'}")
|
|
78
|
+
for problem in report["problems"]:
|
|
79
|
+
print(f" {problem['node']} ({problem['kind']}): {problem['problem']}", file=sys.stderr)
|
|
80
|
+
return 0 if report["ok"] else 1
|
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
"""The content level of ``gather decompose``: measurements and estimates, kept apart from the tree.
|
|
2
|
+
|
|
3
|
+
``content_stage(path, out)`` writes ``components.csv`` and ``components.jsonl`` (one row per
|
|
4
|
+
component: what it is, the table that holds it, how it was produced, whether it is deterministic,
|
|
5
|
+
and its status) plus one table per kind. A component that needs a decoder or a tool that is not
|
|
6
|
+
here gets a row that says so; nothing is guessed and nothing is fetched. No child process is
|
|
7
|
+
started: an installed transcriber or stem separator is only detected, and its row reads
|
|
8
|
+
``planned``.
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
from __future__ import annotations
|
|
12
|
+
|
|
13
|
+
import hashlib
|
|
14
|
+
import importlib.util
|
|
15
|
+
import os
|
|
16
|
+
|
|
17
|
+
from gather import __version__
|
|
18
|
+
from gather.decompose_document import document_tables, is_pdf
|
|
19
|
+
from gather.decompose_dsp import decay_time, key, spectrum, tempo
|
|
20
|
+
from gather.decompose_image import PALETTE_RULE, png_palette
|
|
21
|
+
from gather.decompose_pcm import WINDOW_RULE, integrated_loudness, levels, read_pcm, sig
|
|
22
|
+
from gather.media_probe import probe
|
|
23
|
+
from gather.saves_write import csv_bytes, jsonl_bytes
|
|
24
|
+
from gather.spawn import find_tool
|
|
25
|
+
|
|
26
|
+
COLUMNS = ("kind", "component", "path", "produced_by", "tool", "tool_version", "deterministic", "status", "sha256")
|
|
27
|
+
NO_DECODER = "not run (needs a local decoder; none in the standard library)"
|
|
28
|
+
NO_TOOL = "not run (tool not installed)"
|
|
29
|
+
MEASURED = "yes (fixed rules, rounded to 6 significant digits)"
|
|
30
|
+
MODEL = "no (model output; repeatable only with pinned weights, a fixed seed and one device)"
|
|
31
|
+
_AUDIO = (("sample peak", "largest absolute sample"), ("RMS level", "root mean square of all samples"),
|
|
32
|
+
("crest factor", "sample peak minus RMS level"), ("DC offset", "mean of all samples"),
|
|
33
|
+
("integrated loudness", "ITU-R BS.1770 K-weighting with absolute and relative gates"),
|
|
34
|
+
("tempo estimate", "onset-envelope autocorrelation, 60 to 200 BPM"),
|
|
35
|
+
("key estimate", "Goertzel chroma matched to Krumhansl and Schmuckler profiles"),
|
|
36
|
+
("spectrum summary", "averaged 4096-point FFT: octave-band energy and centroid"),
|
|
37
|
+
("decay time estimate", "Schroeder integration after the loudest frame, -5 to -25 dB fit; "
|
|
38
|
+
"an estimate from programme material, not a room measurement"))
|
|
39
|
+
_VIDEO = ("shot boundaries", "keyframes", "frame rate", "colour summary per shot")
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def _row(kind: str, component: str, path: str = "", how: str = "", deterministic: str = "yes",
|
|
43
|
+
status: str = "ok", tool: str = "gather") -> dict:
|
|
44
|
+
return {"kind": kind, "component": component, "path": path, "produced_by": how, "tool": tool,
|
|
45
|
+
"tool_version": __version__ if tool == "gather" else "", "deterministic": deterministic,
|
|
46
|
+
"status": status, "sha256": ""}
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def _measure(name: str, value, unit: str, method: str) -> dict:
|
|
50
|
+
shown = sig(value) if isinstance(value, float) else value
|
|
51
|
+
return {"measure": name, "value": shown, "unit": unit, "method": method, "window": WINDOW_RULE}
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def _audio_tables(path: str) -> tuple[dict, dict]:
|
|
55
|
+
"""(tables, status per component) for a PCM WAV. Raises ValueError when the PCM cannot be read."""
|
|
56
|
+
pcm = read_pcm(path)
|
|
57
|
+
mono = pcm.mono()
|
|
58
|
+
level, loud = levels(pcm), integrated_loudness(pcm)
|
|
59
|
+
bpm, tonic, spec = tempo(mono, pcm.rate), key(mono, pcm.rate), spectrum(mono, pcm.rate)
|
|
60
|
+
decay = decay_time(mono, pcm.rate)
|
|
61
|
+
how = dict(_AUDIO)
|
|
62
|
+
measures = [
|
|
63
|
+
_measure("frames_analysed", pcm.frames, "frames", "count"), _measure("frames_total", pcm.frames_total, "frames", "count"),
|
|
64
|
+
_measure("sample_peak", level["sample_peak_dbfs"], "dBFS", how["sample peak"]),
|
|
65
|
+
_measure("rms_level", level["rms_dbfs"], "dBFS", how["RMS level"]),
|
|
66
|
+
_measure("crest_factor", level["crest_factor_db"], "dB", how["crest factor"]),
|
|
67
|
+
_measure("dc_offset", level["dc_offset"], "full scale", how["DC offset"]),
|
|
68
|
+
_measure("integrated_loudness", loud, "LUFS", how["integrated loudness"]),
|
|
69
|
+
_measure("tempo_estimate", bpm, "BPM", how["tempo estimate"]),
|
|
70
|
+
_measure("key_estimate", tonic[0] if tonic else None, "key", how["key estimate"]),
|
|
71
|
+
_measure("key_profile_correlation", tonic[1] if tonic else None, "r", how["key estimate"]),
|
|
72
|
+
_measure("spectral_centroid", spec["centroid_hz"], "Hz", how["spectrum summary"]),
|
|
73
|
+
_measure("decay_time_60db_estimate", decay, "s", how["decay time estimate"])]
|
|
74
|
+
bands = [{"band_centre_hz": centre, "energy_db_rel_total": None if value is None else sig(value)}
|
|
75
|
+
for centre, value in spec["bands"]]
|
|
76
|
+
missing = {"integrated loudness": (loud, "no 400 ms block passes the gates"),
|
|
77
|
+
"tempo estimate": (bpm, "under 4 s of audio, or no onsets"), "key estimate": (tonic, "too short or silent"),
|
|
78
|
+
"decay time estimate": (decay, "the tail does not fall 25 dB"),
|
|
79
|
+
"sample peak": (level["sample_peak_dbfs"], "silent"), "RMS level": (level["rms_dbfs"], "silent"),
|
|
80
|
+
"crest factor": (level["crest_factor_db"], "silent"), "spectrum summary": (spec["centroid_hz"], "silent")}
|
|
81
|
+
status = {name: f"not run ({why})" for name, (value, why) in missing.items() if value is None}
|
|
82
|
+
return {"audio_measurements.csv": (("measure", "value", "unit", "method", "window"), measures),
|
|
83
|
+
"audio_spectrum.csv": (("band_centre_hz", "energy_db_rel_total"), bands)}, status
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
def _audio_rows(path: str, container: str, tables: dict) -> list[dict]:
|
|
87
|
+
if container != "wav":
|
|
88
|
+
return [_row("audio", name, how=how, deterministic=MEASURED, status=NO_DECODER) for name, how in _AUDIO]
|
|
89
|
+
try:
|
|
90
|
+
made, status = _audio_tables(path)
|
|
91
|
+
except ValueError as exc:
|
|
92
|
+
return [_row("audio", name, how=how, deterministic=MEASURED, status=f"not run ({exc})") for name, how in _AUDIO]
|
|
93
|
+
tables.update(made)
|
|
94
|
+
return [_row("audio", name, "audio_spectrum.csv" if name == "spectrum summary" else "audio_measurements.csv", how,
|
|
95
|
+
MEASURED, status.get(name, "ok")) for name, how in _AUDIO]
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
def _tool_rows(kinds: set[str]) -> list[dict]:
|
|
99
|
+
"""Rows for components that need a local tool: detected only, never started here."""
|
|
100
|
+
rows = []
|
|
101
|
+
if kinds & {"audio", "video"}:
|
|
102
|
+
stems = importlib.util.find_spec("remastering") is not None and bool(os.environ.get("REMASTER_MODELS"))
|
|
103
|
+
rows.append(_row("audio", "stems", how="stem separation by the remastering package (htdemucs_ft)",
|
|
104
|
+
deterministic=MODEL, status="planned" if stems else NO_TOOL, tool="remastering"))
|
|
105
|
+
rows.append(_row("speech", "transcript with timestamps", how="local speech-to-text through gather.transcribe",
|
|
106
|
+
deterministic=MODEL, status="planned" if find_tool("whisper") else NO_TOOL, tool="whisper"))
|
|
107
|
+
if "image" in kinds:
|
|
108
|
+
rows.append(_row("image", "embedded text", how="local text recognition through gather.ocr",
|
|
109
|
+
deterministic="no (recognition engine output; version dependent)",
|
|
110
|
+
status="planned" if find_tool("tesseract") else NO_TOOL, tool="tesseract"))
|
|
111
|
+
return rows
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
def _image_rows(path: str, container: str, tables: dict) -> list[dict]:
|
|
115
|
+
how = f"pixel count per colour bin: {PALETTE_RULE}"
|
|
116
|
+
if container != "png":
|
|
117
|
+
status = "planned" if container == "gif" else NO_DECODER
|
|
118
|
+
return [_row("image", "palette", how=how, status=status)]
|
|
119
|
+
try:
|
|
120
|
+
tables["image_palette.csv"] = (("rank", "colour", "pixels", "share"), png_palette(path))
|
|
121
|
+
except ValueError as exc:
|
|
122
|
+
return [_row("image", "palette", how=how, status=f"not run ({exc})")]
|
|
123
|
+
return [_row("image", "palette", "image_palette.csv", how)]
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
def _rows(path: str, tables: dict) -> list[dict]:
|
|
127
|
+
facts, _ = probe(path, with_hash=False)
|
|
128
|
+
container, kind = facts["container"], facts["kind"]
|
|
129
|
+
if container == "unknown":
|
|
130
|
+
if is_pdf(path):
|
|
131
|
+
return [_row("document", "text", how="local text extraction through gather.pdf", tool="pdftotext",
|
|
132
|
+
deterministic="no (tool output; version dependent)",
|
|
133
|
+
status="planned" if find_tool("pdftotext") else NO_TOOL)]
|
|
134
|
+
made = document_tables(path)
|
|
135
|
+
tables.update(made)
|
|
136
|
+
return [_row("document", name[len("document_"):-len(".csv")].replace("_", " "), name,
|
|
137
|
+
"fixed rules over the decoded text") for name in made]
|
|
138
|
+
tables["container.csv"] = (("field", "value"), [{"field": k, "value": v} for k, v in facts.items() if k != "sha256"])
|
|
139
|
+
rows = [_row(kind, "container facts", "container.csv", "magic bytes and the container's own header fields")]
|
|
140
|
+
if kind == "image":
|
|
141
|
+
rows += _image_rows(path, container, tables)
|
|
142
|
+
if kind == "video":
|
|
143
|
+
rows += [_row("video", name, how="needs decoded frames", status=NO_DECODER) for name in _VIDEO]
|
|
144
|
+
if kind in ("audio", "video"):
|
|
145
|
+
rows += _audio_rows(path, container, tables)
|
|
146
|
+
return rows + _tool_rows({kind})
|
|
147
|
+
|
|
148
|
+
|
|
149
|
+
def content_stage(path: str, out: str) -> list[dict]:
|
|
150
|
+
"""Write the content tables for ``path`` into ``out`` and return the component rows."""
|
|
151
|
+
tables: dict = {}
|
|
152
|
+
rows = _rows(path, tables)
|
|
153
|
+
digests = {}
|
|
154
|
+
for name, (columns, table_rows) in sorted(tables.items()):
|
|
155
|
+
data = csv_bytes(columns, table_rows)
|
|
156
|
+
with open(os.path.join(out, name), "wb") as f:
|
|
157
|
+
f.write(data)
|
|
158
|
+
digests[name] = hashlib.sha256(data).hexdigest()
|
|
159
|
+
for row in rows:
|
|
160
|
+
row["sha256"] = digests.get(row["path"], "")
|
|
161
|
+
for name, encode in (("components.csv", csv_bytes), ("components.jsonl", jsonl_bytes)):
|
|
162
|
+
with open(os.path.join(out, name), "wb") as f:
|
|
163
|
+
f.write(encode(COLUMNS, rows))
|
|
164
|
+
return rows
|