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.
Files changed (249) hide show
  1. {gather_engine-2.5.0 → gather_engine-2.6.0}/PKG-INFO +27 -4
  2. gather_engine-2.5.0/src/gather_engine.egg-info/PKG-INFO → gather_engine-2.6.0/README.md +22 -38
  3. {gather_engine-2.5.0 → gather_engine-2.6.0}/pyproject.toml +4 -3
  4. {gather_engine-2.5.0 → gather_engine-2.6.0}/src/gather/__init__.py +1 -1
  5. {gather_engine-2.5.0 → gather_engine-2.6.0}/src/gather/cli_ext.py +4 -0
  6. gather_engine-2.6.0/src/gather/decompose_binary.py +127 -0
  7. gather_engine-2.6.0/src/gather/decompose_cmd.py +80 -0
  8. gather_engine-2.6.0/src/gather/decompose_content.py +164 -0
  9. gather_engine-2.6.0/src/gather/decompose_document.py +98 -0
  10. gather_engine-2.6.0/src/gather/decompose_dsp.py +156 -0
  11. gather_engine-2.6.0/src/gather/decompose_image.py +129 -0
  12. gather_engine-2.6.0/src/gather/decompose_pcm.py +166 -0
  13. gather_engine-2.6.0/src/gather/decompose_rebuild.py +130 -0
  14. gather_engine-2.6.0/src/gather/decompose_run.py +116 -0
  15. gather_engine-2.6.0/src/gather/decompose_text.py +150 -0
  16. gather_engine-2.6.0/src/gather/decompose_tree.py +154 -0
  17. gather_engine-2.6.0/src/gather/decompose_zip.py +141 -0
  18. gather_engine-2.6.0/src/gather/frame_sheets.py +121 -0
  19. gather_engine-2.6.0/src/gather/media_probe.py +110 -0
  20. gather_engine-2.6.0/src/gather/media_probe_audio.py +118 -0
  21. gather_engine-2.6.0/src/gather/media_probe_bmff.py +116 -0
  22. gather_engine-2.6.0/src/gather/media_probe_ebml.py +132 -0
  23. gather_engine-2.6.0/src/gather/media_probe_image.py +73 -0
  24. {gather_engine-2.5.0 → gather_engine-2.6.0}/src/gather/pacing.py +44 -0
  25. gather_engine-2.6.0/src/gather/saves_cmd.py +68 -0
  26. gather_engine-2.6.0/src/gather/saves_entities.py +123 -0
  27. gather_engine-2.6.0/src/gather/saves_lang.py +106 -0
  28. gather_engine-2.6.0/src/gather/saves_sidecar.py +197 -0
  29. gather_engine-2.6.0/src/gather/saves_tables.py +209 -0
  30. gather_engine-2.6.0/src/gather/saves_write.py +55 -0
  31. gather_engine-2.6.0/src/gather/storyboard.py +118 -0
  32. {gather_engine-2.5.0 → gather_engine-2.6.0}/src/gather/video_bundle.py +6 -1
  33. {gather_engine-2.5.0 → gather_engine-2.6.0}/src/gather/video_bundle_cmd.py +38 -4
  34. {gather_engine-2.5.0 → gather_engine-2.6.0}/src/gather/video_bundle_run.py +114 -13
  35. gather_engine-2.6.0/src/gather/video_frames.py +106 -0
  36. {gather_engine-2.5.0 → gather_engine-2.6.0}/src/gather/video_source.py +5 -1
  37. gather_engine-2.5.0/README.md → gather_engine-2.6.0/src/gather_engine.egg-info/PKG-INFO +61 -3
  38. {gather_engine-2.5.0 → gather_engine-2.6.0}/src/gather_engine.egg-info/SOURCES.txt +33 -0
  39. {gather_engine-2.5.0 → gather_engine-2.6.0}/src/gather_engine.egg-info/requires.txt +5 -0
  40. gather_engine-2.6.0/tests/test_decompose.py +258 -0
  41. gather_engine-2.6.0/tests/test_decompose_content.py +236 -0
  42. gather_engine-2.6.0/tests/test_saves.py +181 -0
  43. gather_engine-2.6.0/tests/test_saves_entities.py +70 -0
  44. gather_engine-2.6.0/tests/test_saves_media.py +124 -0
  45. gather_engine-2.6.0/tests/test_storyboard.py +108 -0
  46. gather_engine-2.6.0/tests/test_video_frames.py +246 -0
  47. {gather_engine-2.5.0 → gather_engine-2.6.0}/LICENSE +0 -0
  48. {gather_engine-2.5.0 → gather_engine-2.6.0}/NOTICE +0 -0
  49. {gather_engine-2.5.0 → gather_engine-2.6.0}/setup.cfg +0 -0
  50. {gather_engine-2.5.0 → gather_engine-2.6.0}/src/gather/__main__.py +0 -0
  51. {gather_engine-2.5.0 → gather_engine-2.6.0}/src/gather/_vendor/__init__.py +0 -0
  52. {gather_engine-2.5.0 → gather_engine-2.6.0}/src/gather/_vendor/safe_spawn.py +0 -0
  53. {gather_engine-2.5.0 → gather_engine-2.6.0}/src/gather/api.py +0 -0
  54. {gather_engine-2.5.0 → gather_engine-2.6.0}/src/gather/arxiv.py +0 -0
  55. {gather_engine-2.5.0 → gather_engine-2.6.0}/src/gather/availability.py +0 -0
  56. {gather_engine-2.5.0 → gather_engine-2.6.0}/src/gather/backends.py +0 -0
  57. {gather_engine-2.5.0 → gather_engine-2.6.0}/src/gather/backends_browser.py +0 -0
  58. {gather_engine-2.5.0 → gather_engine-2.6.0}/src/gather/benchmark.py +0 -0
  59. {gather_engine-2.5.0 → gather_engine-2.6.0}/src/gather/browser.py +0 -0
  60. {gather_engine-2.5.0 → gather_engine-2.6.0}/src/gather/browser_evidence.py +0 -0
  61. {gather_engine-2.5.0 → gather_engine-2.6.0}/src/gather/cache.py +0 -0
  62. {gather_engine-2.5.0 → gather_engine-2.6.0}/src/gather/captions.py +0 -0
  63. {gather_engine-2.5.0 → gather_engine-2.6.0}/src/gather/channel.py +0 -0
  64. {gather_engine-2.5.0 → gather_engine-2.6.0}/src/gather/channel_ledger.py +0 -0
  65. {gather_engine-2.5.0 → gather_engine-2.6.0}/src/gather/citecheck.py +0 -0
  66. {gather_engine-2.5.0 → gather_engine-2.6.0}/src/gather/cli.py +0 -0
  67. {gather_engine-2.5.0 → gather_engine-2.6.0}/src/gather/client_mcp.py +0 -0
  68. {gather_engine-2.5.0 → gather_engine-2.6.0}/src/gather/client_network.py +0 -0
  69. {gather_engine-2.5.0 → gather_engine-2.6.0}/src/gather/commands.py +0 -0
  70. {gather_engine-2.5.0 → gather_engine-2.6.0}/src/gather/comment_balance.py +0 -0
  71. {gather_engine-2.5.0 → gather_engine-2.6.0}/src/gather/comment_report.py +0 -0
  72. {gather_engine-2.5.0 → gather_engine-2.6.0}/src/gather/comment_signals.py +0 -0
  73. {gather_engine-2.5.0 → gather_engine-2.6.0}/src/gather/context.py +0 -0
  74. {gather_engine-2.5.0 → gather_engine-2.6.0}/src/gather/corpus_cmd.py +0 -0
  75. {gather_engine-2.5.0 → gather_engine-2.6.0}/src/gather/crawl.py +0 -0
  76. {gather_engine-2.5.0 → gather_engine-2.6.0}/src/gather/credentials.py +0 -0
  77. {gather_engine-2.5.0 → gather_engine-2.6.0}/src/gather/derive.py +0 -0
  78. {gather_engine-2.5.0 → gather_engine-2.6.0}/src/gather/digest.py +0 -0
  79. {gather_engine-2.5.0 → gather_engine-2.6.0}/src/gather/docs.py +0 -0
  80. {gather_engine-2.5.0 → gather_engine-2.6.0}/src/gather/docs_tool.py +0 -0
  81. {gather_engine-2.5.0 → gather_engine-2.6.0}/src/gather/dom.py +0 -0
  82. {gather_engine-2.5.0 → gather_engine-2.6.0}/src/gather/export.py +0 -0
  83. {gather_engine-2.5.0 → gather_engine-2.6.0}/src/gather/extract.py +0 -0
  84. {gather_engine-2.5.0 → gather_engine-2.6.0}/src/gather/fastparse.py +0 -0
  85. {gather_engine-2.5.0 → gather_engine-2.6.0}/src/gather/federation.py +0 -0
  86. {gather_engine-2.5.0 → gather_engine-2.6.0}/src/gather/federation_cmd.py +0 -0
  87. {gather_engine-2.5.0 → gather_engine-2.6.0}/src/gather/federation_policy.py +0 -0
  88. {gather_engine-2.5.0 → gather_engine-2.6.0}/src/gather/federation_receipt.py +0 -0
  89. {gather_engine-2.5.0 → gather_engine-2.6.0}/src/gather/feed.py +0 -0
  90. {gather_engine-2.5.0 → gather_engine-2.6.0}/src/gather/fetch.py +0 -0
  91. {gather_engine-2.5.0 → gather_engine-2.6.0}/src/gather/filter_ledger.py +0 -0
  92. {gather_engine-2.5.0 → gather_engine-2.6.0}/src/gather/flagship.py +0 -0
  93. {gather_engine-2.5.0 → gather_engine-2.6.0}/src/gather/grant_checks.py +0 -0
  94. {gather_engine-2.5.0 → gather_engine-2.6.0}/src/gather/grants.py +0 -0
  95. {gather_engine-2.5.0 → gather_engine-2.6.0}/src/gather/http_call.py +0 -0
  96. {gather_engine-2.5.0 → gather_engine-2.6.0}/src/gather/interop.py +0 -0
  97. {gather_engine-2.5.0 → gather_engine-2.6.0}/src/gather/item.py +0 -0
  98. {gather_engine-2.5.0 → gather_engine-2.6.0}/src/gather/ledger_cmd.py +0 -0
  99. {gather_engine-2.5.0 → gather_engine-2.6.0}/src/gather/ledger_verify.py +0 -0
  100. {gather_engine-2.5.0 → gather_engine-2.6.0}/src/gather/local_model.py +0 -0
  101. {gather_engine-2.5.0 → gather_engine-2.6.0}/src/gather/localpath.py +0 -0
  102. {gather_engine-2.5.0 → gather_engine-2.6.0}/src/gather/mcp.py +0 -0
  103. {gather_engine-2.5.0 → gather_engine-2.6.0}/src/gather/method.py +0 -0
  104. {gather_engine-2.5.0 → gather_engine-2.6.0}/src/gather/model.py +0 -0
  105. {gather_engine-2.5.0 → gather_engine-2.6.0}/src/gather/monitor.py +0 -0
  106. {gather_engine-2.5.0 → gather_engine-2.6.0}/src/gather/net.py +0 -0
  107. {gather_engine-2.5.0 → gather_engine-2.6.0}/src/gather/ocr.py +0 -0
  108. {gather_engine-2.5.0 → gather_engine-2.6.0}/src/gather/payloads.py +0 -0
  109. {gather_engine-2.5.0 → gather_engine-2.6.0}/src/gather/pdf.py +0 -0
  110. {gather_engine-2.5.0 → gather_engine-2.6.0}/src/gather/pilot.py +0 -0
  111. {gather_engine-2.5.0 → gather_engine-2.6.0}/src/gather/pilot_bundle.py +0 -0
  112. {gather_engine-2.5.0 → gather_engine-2.6.0}/src/gather/pilot_commands.py +0 -0
  113. {gather_engine-2.5.0 → gather_engine-2.6.0}/src/gather/pilot_manifest.py +0 -0
  114. {gather_engine-2.5.0 → gather_engine-2.6.0}/src/gather/pilot_report.py +0 -0
  115. {gather_engine-2.5.0 → gather_engine-2.6.0}/src/gather/pilot_sources.py +0 -0
  116. {gather_engine-2.5.0 → gather_engine-2.6.0}/src/gather/provenance.py +0 -0
  117. {gather_engine-2.5.0 → gather_engine-2.6.0}/src/gather/recall.py +0 -0
  118. {gather_engine-2.5.0 → gather_engine-2.6.0}/src/gather/reddit.py +0 -0
  119. {gather_engine-2.5.0 → gather_engine-2.6.0}/src/gather/reddit_cmd.py +0 -0
  120. {gather_engine-2.5.0 → gather_engine-2.6.0}/src/gather/reddit_items.py +0 -0
  121. {gather_engine-2.5.0 → gather_engine-2.6.0}/src/gather/report.py +0 -0
  122. {gather_engine-2.5.0 → gather_engine-2.6.0}/src/gather/report_cmd.py +0 -0
  123. {gather_engine-2.5.0 → gather_engine-2.6.0}/src/gather/route.py +0 -0
  124. {gather_engine-2.5.0 → gather_engine-2.6.0}/src/gather/run.py +0 -0
  125. {gather_engine-2.5.0 → gather_engine-2.6.0}/src/gather/run_config.py +0 -0
  126. {gather_engine-2.5.0 → gather_engine-2.6.0}/src/gather/schema_extract.py +0 -0
  127. {gather_engine-2.5.0 → gather_engine-2.6.0}/src/gather/scholar.py +0 -0
  128. {gather_engine-2.5.0 → gather_engine-2.6.0}/src/gather/scope.py +0 -0
  129. {gather_engine-2.5.0 → gather_engine-2.6.0}/src/gather/search.py +0 -0
  130. {gather_engine-2.5.0 → gather_engine-2.6.0}/src/gather/source.py +0 -0
  131. {gather_engine-2.5.0 → gather_engine-2.6.0}/src/gather/spawn.py +0 -0
  132. {gather_engine-2.5.0 → gather_engine-2.6.0}/src/gather/store.py +0 -0
  133. {gather_engine-2.5.0 → gather_engine-2.6.0}/src/gather/stream.py +0 -0
  134. {gather_engine-2.5.0 → gather_engine-2.6.0}/src/gather/track.py +0 -0
  135. {gather_engine-2.5.0 → gather_engine-2.6.0}/src/gather/transcribe.py +0 -0
  136. {gather_engine-2.5.0 → gather_engine-2.6.0}/src/gather/video.py +0 -0
  137. {gather_engine-2.5.0 → gather_engine-2.6.0}/src/gather/video_cmd.py +0 -0
  138. {gather_engine-2.5.0 → gather_engine-2.6.0}/src/gather/web.py +0 -0
  139. {gather_engine-2.5.0 → gather_engine-2.6.0}/src/gather/web_commands.py +0 -0
  140. {gather_engine-2.5.0 → gather_engine-2.6.0}/src/gather/youtube_api.py +0 -0
  141. {gather_engine-2.5.0 → gather_engine-2.6.0}/src/gather/youtube_cmd.py +0 -0
  142. {gather_engine-2.5.0 → gather_engine-2.6.0}/src/gather/youtube_probe.py +0 -0
  143. {gather_engine-2.5.0 → gather_engine-2.6.0}/src/gather/youtube_route.py +0 -0
  144. {gather_engine-2.5.0 → gather_engine-2.6.0}/src/gather/ytdlp.py +0 -0
  145. {gather_engine-2.5.0 → gather_engine-2.6.0}/src/gather_engine.egg-info/dependency_links.txt +0 -0
  146. {gather_engine-2.5.0 → gather_engine-2.6.0}/src/gather_engine.egg-info/entry_points.txt +0 -0
  147. {gather_engine-2.5.0 → gather_engine-2.6.0}/src/gather_engine.egg-info/top_level.txt +0 -0
  148. {gather_engine-2.5.0 → gather_engine-2.6.0}/tests/test_api.py +0 -0
  149. {gather_engine-2.5.0 → gather_engine-2.6.0}/tests/test_arxiv.py +0 -0
  150. {gather_engine-2.5.0 → gather_engine-2.6.0}/tests/test_availability.py +0 -0
  151. {gather_engine-2.5.0 → gather_engine-2.6.0}/tests/test_backends.py +0 -0
  152. {gather_engine-2.5.0 → gather_engine-2.6.0}/tests/test_backends_browser.py +0 -0
  153. {gather_engine-2.5.0 → gather_engine-2.6.0}/tests/test_backends_card.py +0 -0
  154. {gather_engine-2.5.0 → gather_engine-2.6.0}/tests/test_benchmark.py +0 -0
  155. {gather_engine-2.5.0 → gather_engine-2.6.0}/tests/test_browser_evidence.py +0 -0
  156. {gather_engine-2.5.0 → gather_engine-2.6.0}/tests/test_cache.py +0 -0
  157. {gather_engine-2.5.0 → gather_engine-2.6.0}/tests/test_captions.py +0 -0
  158. {gather_engine-2.5.0 → gather_engine-2.6.0}/tests/test_channel.py +0 -0
  159. {gather_engine-2.5.0 → gather_engine-2.6.0}/tests/test_channel_ledger.py +0 -0
  160. {gather_engine-2.5.0 → gather_engine-2.6.0}/tests/test_citecheck.py +0 -0
  161. {gather_engine-2.5.0 → gather_engine-2.6.0}/tests/test_cli.py +0 -0
  162. {gather_engine-2.5.0 → gather_engine-2.6.0}/tests/test_cli_web.py +0 -0
  163. {gather_engine-2.5.0 → gather_engine-2.6.0}/tests/test_client_disclosure.py +0 -0
  164. {gather_engine-2.5.0 → gather_engine-2.6.0}/tests/test_client_network.py +0 -0
  165. {gather_engine-2.5.0 → gather_engine-2.6.0}/tests/test_client_package.py +0 -0
  166. {gather_engine-2.5.0 → gather_engine-2.6.0}/tests/test_client_profile.py +0 -0
  167. {gather_engine-2.5.0 → gather_engine-2.6.0}/tests/test_client_setup.py +0 -0
  168. {gather_engine-2.5.0 → gather_engine-2.6.0}/tests/test_client_vendored.py +0 -0
  169. {gather_engine-2.5.0 → gather_engine-2.6.0}/tests/test_comment_balance.py +0 -0
  170. {gather_engine-2.5.0 → gather_engine-2.6.0}/tests/test_context.py +0 -0
  171. {gather_engine-2.5.0 → gather_engine-2.6.0}/tests/test_context_mountinfo.py +0 -0
  172. {gather_engine-2.5.0 → gather_engine-2.6.0}/tests/test_crawl.py +0 -0
  173. {gather_engine-2.5.0 → gather_engine-2.6.0}/tests/test_credentials.py +0 -0
  174. {gather_engine-2.5.0 → gather_engine-2.6.0}/tests/test_demo.py +0 -0
  175. {gather_engine-2.5.0 → gather_engine-2.6.0}/tests/test_derive.py +0 -0
  176. {gather_engine-2.5.0 → gather_engine-2.6.0}/tests/test_device_names.py +0 -0
  177. {gather_engine-2.5.0 → gather_engine-2.6.0}/tests/test_digest.py +0 -0
  178. {gather_engine-2.5.0 → gather_engine-2.6.0}/tests/test_docs.py +0 -0
  179. {gather_engine-2.5.0 → gather_engine-2.6.0}/tests/test_dom.py +0 -0
  180. {gather_engine-2.5.0 → gather_engine-2.6.0}/tests/test_export.py +0 -0
  181. {gather_engine-2.5.0 → gather_engine-2.6.0}/tests/test_extract.py +0 -0
  182. {gather_engine-2.5.0 → gather_engine-2.6.0}/tests/test_fastparse.py +0 -0
  183. {gather_engine-2.5.0 → gather_engine-2.6.0}/tests/test_federation.py +0 -0
  184. {gather_engine-2.5.0 → gather_engine-2.6.0}/tests/test_federation_cli.py +0 -0
  185. {gather_engine-2.5.0 → gather_engine-2.6.0}/tests/test_federation_receipt.py +0 -0
  186. {gather_engine-2.5.0 → gather_engine-2.6.0}/tests/test_feed.py +0 -0
  187. {gather_engine-2.5.0 → gather_engine-2.6.0}/tests/test_fetch.py +0 -0
  188. {gather_engine-2.5.0 → gather_engine-2.6.0}/tests/test_filter_ledger.py +0 -0
  189. {gather_engine-2.5.0 → gather_engine-2.6.0}/tests/test_flagship_cli.py +0 -0
  190. {gather_engine-2.5.0 → gather_engine-2.6.0}/tests/test_grants.py +0 -0
  191. {gather_engine-2.5.0 → gather_engine-2.6.0}/tests/test_hard_sources.py +0 -0
  192. {gather_engine-2.5.0 → gather_engine-2.6.0}/tests/test_http_call.py +0 -0
  193. {gather_engine-2.5.0 → gather_engine-2.6.0}/tests/test_interop.py +0 -0
  194. {gather_engine-2.5.0 → gather_engine-2.6.0}/tests/test_item.py +0 -0
  195. {gather_engine-2.5.0 → gather_engine-2.6.0}/tests/test_localpath.py +0 -0
  196. {gather_engine-2.5.0 → gather_engine-2.6.0}/tests/test_mcp.py +0 -0
  197. {gather_engine-2.5.0 → gather_engine-2.6.0}/tests/test_mcp_annotations.py +0 -0
  198. {gather_engine-2.5.0 → gather_engine-2.6.0}/tests/test_method.py +0 -0
  199. {gather_engine-2.5.0 → gather_engine-2.6.0}/tests/test_model.py +0 -0
  200. {gather_engine-2.5.0 → gather_engine-2.6.0}/tests/test_monitor.py +0 -0
  201. {gather_engine-2.5.0 → gather_engine-2.6.0}/tests/test_net.py +0 -0
  202. {gather_engine-2.5.0 → gather_engine-2.6.0}/tests/test_nonlocal_paths.py +0 -0
  203. {gather_engine-2.5.0 → gather_engine-2.6.0}/tests/test_pacing.py +0 -0
  204. {gather_engine-2.5.0 → gather_engine-2.6.0}/tests/test_package.py +0 -0
  205. {gather_engine-2.5.0 → gather_engine-2.6.0}/tests/test_pdf.py +0 -0
  206. {gather_engine-2.5.0 → gather_engine-2.6.0}/tests/test_pilot.py +0 -0
  207. {gather_engine-2.5.0 → gather_engine-2.6.0}/tests/test_pilot_bundle.py +0 -0
  208. {gather_engine-2.5.0 → gather_engine-2.6.0}/tests/test_pilot_cli.py +0 -0
  209. {gather_engine-2.5.0 → gather_engine-2.6.0}/tests/test_pilot_manifest.py +0 -0
  210. {gather_engine-2.5.0 → gather_engine-2.6.0}/tests/test_pilot_refresh.py +0 -0
  211. {gather_engine-2.5.0 → gather_engine-2.6.0}/tests/test_pilot_report.py +0 -0
  212. {gather_engine-2.5.0 → gather_engine-2.6.0}/tests/test_pilot_sample.py +0 -0
  213. {gather_engine-2.5.0 → gather_engine-2.6.0}/tests/test_pilot_showcase.py +0 -0
  214. {gather_engine-2.5.0 → gather_engine-2.6.0}/tests/test_pilot_sources.py +0 -0
  215. {gather_engine-2.5.0 → gather_engine-2.6.0}/tests/test_provenance.py +0 -0
  216. {gather_engine-2.5.0 → gather_engine-2.6.0}/tests/test_recall.py +0 -0
  217. {gather_engine-2.5.0 → gather_engine-2.6.0}/tests/test_reddit.py +0 -0
  218. {gather_engine-2.5.0 → gather_engine-2.6.0}/tests/test_reddit_grants.py +0 -0
  219. {gather_engine-2.5.0 → gather_engine-2.6.0}/tests/test_release_workflow.py +0 -0
  220. {gather_engine-2.5.0 → gather_engine-2.6.0}/tests/test_repo_art.py +0 -0
  221. {gather_engine-2.5.0 → gather_engine-2.6.0}/tests/test_repo_card.py +0 -0
  222. {gather_engine-2.5.0 → gather_engine-2.6.0}/tests/test_report.py +0 -0
  223. {gather_engine-2.5.0 → gather_engine-2.6.0}/tests/test_report_quotes.py +0 -0
  224. {gather_engine-2.5.0 → gather_engine-2.6.0}/tests/test_route.py +0 -0
  225. {gather_engine-2.5.0 → gather_engine-2.6.0}/tests/test_run.py +0 -0
  226. {gather_engine-2.5.0 → gather_engine-2.6.0}/tests/test_schema_extract.py +0 -0
  227. {gather_engine-2.5.0 → gather_engine-2.6.0}/tests/test_scholar.py +0 -0
  228. {gather_engine-2.5.0 → gather_engine-2.6.0}/tests/test_scope.py +0 -0
  229. {gather_engine-2.5.0 → gather_engine-2.6.0}/tests/test_search.py +0 -0
  230. {gather_engine-2.5.0 → gather_engine-2.6.0}/tests/test_spawn_children.py +0 -0
  231. {gather_engine-2.5.0 → gather_engine-2.6.0}/tests/test_spawn_working_folder.py +0 -0
  232. {gather_engine-2.5.0 → gather_engine-2.6.0}/tests/test_store.py +0 -0
  233. {gather_engine-2.5.0 → gather_engine-2.6.0}/tests/test_stream.py +0 -0
  234. {gather_engine-2.5.0 → gather_engine-2.6.0}/tests/test_track.py +0 -0
  235. {gather_engine-2.5.0 → gather_engine-2.6.0}/tests/test_vendored.py +0 -0
  236. {gather_engine-2.5.0 → gather_engine-2.6.0}/tests/test_verify_pilot_cli.py +0 -0
  237. {gather_engine-2.5.0 → gather_engine-2.6.0}/tests/test_video.py +0 -0
  238. {gather_engine-2.5.0 → gather_engine-2.6.0}/tests/test_video_bundle.py +0 -0
  239. {gather_engine-2.5.0 → gather_engine-2.6.0}/tests/test_video_source.py +0 -0
  240. {gather_engine-2.5.0 → gather_engine-2.6.0}/tests/test_video_spawn.py +0 -0
  241. {gather_engine-2.5.0 → gather_engine-2.6.0}/tests/test_web.py +0 -0
  242. {gather_engine-2.5.0 → gather_engine-2.6.0}/tests/test_web_refine.py +0 -0
  243. {gather_engine-2.5.0 → gather_engine-2.6.0}/tests/test_xml_safety.py +0 -0
  244. {gather_engine-2.5.0 → gather_engine-2.6.0}/tests/test_youtube_api.py +0 -0
  245. {gather_engine-2.5.0 → gather_engine-2.6.0}/tests/test_youtube_grants.py +0 -0
  246. {gather_engine-2.5.0 → gather_engine-2.6.0}/tests/test_youtube_probe.py +0 -0
  247. {gather_engine-2.5.0 → gather_engine-2.6.0}/tests/test_youtube_route.py +0 -0
  248. {gather_engine-2.5.0 → gather_engine-2.6.0}/tests/test_ytdlp.py +0 -0
  249. {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.5.0
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.5.0 is on PyPI as `gather-engine`.
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.5.0
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.5.0.
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.5.0 is on PyPI as `gather-engine`.
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.5.0
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.5.0.
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.5.0"
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
- all = ["lxml>=5", "playwright>=1.4"]
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.5.0"
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