Sync to v0.2.2 (github.com/iscc/iscc-sct @ bf39b3a)
Browse filesThis view is limited to 50 files because it contains too many changes. See raw diff
- .gitattributes +2 -0
- .pre-commit-config.yaml +36 -0
- CHANGELOG.md +108 -1
- LICENSE +197 -6
- README.md +26 -7
- app.py +2 -2
- docs/CNAME +1 -0
- docs/assets/favicon.png +0 -0
- docs/assets/logo_dark.png +3 -0
- docs/assets/logo_light.png +3 -0
- docs/conventions.md +26 -0
- docs/explanation/how-it-works.md +118 -0
- docs/howto/command-line.md +123 -0
- docs/howto/compare-texts.md +122 -0
- docs/howto/configuration.md +126 -0
- docs/howto/granular-features.md +156 -0
- docs/includes/abbreviations.md +12 -0
- docs/index.md +157 -0
- docs/javascripts/copilot.js +27 -0
- docs/javascripts/copypage.js +200 -0
- docs/llms.txt +20 -0
- docs/overrides/main.html +28 -0
- docs/reference/api.md +112 -0
- docs/reference/for-coding-agents.md +283 -0
- docs/robots.txt +2 -0
- docs/stylesheets/copilot.css +119 -0
- docs/stylesheets/extra.css +289 -0
- docs/tutorials/getting-started.md +138 -0
- iscc_sct/__init__.py +1 -1
- iscc_sct/cli.py +18 -6
- iscc_sct/code_semantic_text.py +339 -50
- iscc_sct/demo.py +6 -5
- iscc_sct/dev.py +1 -1
- iscc_sct/doctor.py +217 -0
- iscc_sct/models.py +1 -0
- iscc_sct/options.py +18 -0
- iscc_sct/utils.py +61 -4
- poetry.lock +0 -0
- pyproject.toml +103 -58
- requirements.txt +35 -11
- scripts/gen_llms_full.py +106 -0
- space.yml +1 -1
- tests/chunking_vectors.json +153 -0
- tests/test_chunking_vectors.py +150 -0
- tests/test_cli.py +27 -8
- tests/test_doctor.py +246 -0
- tests/test_gen_llms_full.py +49 -0
- tests/test_iscc_sct.py +295 -42
- tests/test_main.py +1 -0
- tests/test_models.py +1 -0
.gitattributes
CHANGED
|
@@ -33,3 +33,5 @@ saved_model/**/* filter=lfs diff=lfs merge=lfs -text
|
|
| 33 |
*.zip filter=lfs diff=lfs merge=lfs -text
|
| 34 |
*.zst filter=lfs diff=lfs merge=lfs -text
|
| 35 |
*tfevents* filter=lfs diff=lfs merge=lfs -text
|
|
|
|
|
|
|
|
|
| 33 |
*.zip filter=lfs diff=lfs merge=lfs -text
|
| 34 |
*.zst filter=lfs diff=lfs merge=lfs -text
|
| 35 |
*tfevents* filter=lfs diff=lfs merge=lfs -text
|
| 36 |
+
docs/assets/logo_dark.png filter=lfs diff=lfs merge=lfs -text
|
| 37 |
+
docs/assets/logo_light.png filter=lfs diff=lfs merge=lfs -text
|
.pre-commit-config.yaml
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
repos:
|
| 2 |
+
- repo: https://github.com/pre-commit/pre-commit-hooks
|
| 3 |
+
rev: v6.0.0
|
| 4 |
+
hooks:
|
| 5 |
+
- id: trailing-whitespace
|
| 6 |
+
exclude: ^(iscc_sct/tokenizer\.json|tests/.*\.txt)$
|
| 7 |
+
- id: check-added-large-files
|
| 8 |
+
exclude: ^(uv\.lock|iscc_sct/tokenizer\.json)$
|
| 9 |
+
- id: end-of-file-fixer
|
| 10 |
+
exclude: ^(iscc_sct/tokenizer\.json|tests/.*\.txt)$
|
| 11 |
+
- id: fix-byte-order-marker
|
| 12 |
+
exclude: ^tests/.*\.txt$
|
| 13 |
+
- id: check-json
|
| 14 |
+
- id: check-toml
|
| 15 |
+
- id: check-yaml
|
| 16 |
+
- id: mixed-line-ending
|
| 17 |
+
args: [--fix=lf]
|
| 18 |
+
exclude: ^(iscc_sct/tokenizer\.json|tests/.*\.txt)$
|
| 19 |
+
- repo: https://github.com/astral-sh/ruff-pre-commit
|
| 20 |
+
rev: v0.15.16
|
| 21 |
+
hooks:
|
| 22 |
+
- id: ruff-format
|
| 23 |
+
- id: ruff-check
|
| 24 |
+
args: [--fix]
|
| 25 |
+
- repo: https://github.com/executablebooks/mdformat
|
| 26 |
+
rev: 1.0.0
|
| 27 |
+
hooks:
|
| 28 |
+
- id: mdformat
|
| 29 |
+
args: [--wrap=100, --end-of-line=lf]
|
| 30 |
+
# api.md uses mkdocstrings ::: directives that mdformat would collapse and break.
|
| 31 |
+
exclude: ^docs/reference/api\.md$
|
| 32 |
+
additional_dependencies:
|
| 33 |
+
# mdformat-mkdocs[recommended] bundles mdformat-gfm and frontmatter support and adds
|
| 34 |
+
# mkdocs admonition handling (!!!/???); gfm-alerts keeps the README's GitHub alerts
|
| 35 |
+
- "mdformat-mkdocs[recommended]"
|
| 36 |
+
- mdformat-gfm-alerts
|
CHANGELOG.md
CHANGED
|
@@ -1,17 +1,124 @@
|
|
| 1 |
# Changelog
|
| 2 |
|
| 3 |
-
## [0.
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 4 |
|
| 5 |
## [0.1.2] - 2024-08-19
|
|
|
|
| 6 |
- Encode granular features with base64
|
| 7 |
- Refactor result format to generic ISCC data model
|
| 8 |
- Add optional gradio GUI demo
|
| 9 |
|
| 10 |
## [0.1.1] - 2024-06-25
|
|
|
|
| 11 |
- Handle text decoding errors gracefully
|
| 12 |
- Handle feature bit-lengths independently
|
| 13 |
- Improve model load time
|
| 14 |
- Improve memory use with batched embedding
|
| 15 |
|
| 16 |
## [0.1.0] - 2024-06-25
|
|
|
|
| 17 |
- Initial pre-release
|
|
|
|
| 1 |
# Changelog
|
| 2 |
|
| 3 |
+
## [0.2.2] - 2026-08-03
|
| 4 |
+
|
| 5 |
+
- Changed the default inference batch size from a fixed 100 to auto (`batch_size=0`), which resolves
|
| 6 |
+
to 1 chunk per batch on CPU and 100 on CUDA (#25). The vendored tokenizer pads each batch to its
|
| 7 |
+
longest chunk and attention cost is quadratic in sequence length, so on CPU large batches mostly
|
| 8 |
+
pay for padding. Measured on a 2238-chunk book: ~1.4x faster and 1529 MB → 1110 MB steady RSS
|
| 9 |
+
single-process; on a 12-worker pool 1.45x throughput and 18.5 GB → 13.4 GB total PSS. Generated
|
| 10 |
+
ISCC codes are unchanged (verified bit-identical at 256 bits across ~50 configurations, and
|
| 11 |
+
identical between the CPU and CUDA providers). A GPU scales the opposite way, which is why the
|
| 12 |
+
default is provider-aware rather than one global value: on a GTX 1080, throughput rises from 384
|
| 13 |
+
chunks/s at batch 1 to 668 at batch 100 and then plateaus (batch 200 is no faster and nearly
|
| 14 |
+
doubles VRAM), so the CUDA path keeps batch 100. Host RSS on the GPU path is unaffected by batch
|
| 15 |
+
size — those activations live in VRAM
|
| 16 |
+
- Added the `batch_size` option (`ISCC_SCT_BATCH_SIZE`) to override the batch size per call or
|
| 17 |
+
globally, and `intra_op_threads` (`ISCC_SCT_INTRA_OP_THREADS`) to set the ONNX Runtime intra-op
|
| 18 |
+
thread count. Both default to 0, meaning auto. `intra_op_threads` configures the process-wide
|
| 19 |
+
inference session, so it only takes effect through the global options before the session is
|
| 20 |
+
created — worker pools with one process per core should set it to 1 to avoid thread
|
| 21 |
+
oversubscription (measured 1.7x higher total throughput on a 12-worker pool)
|
| 22 |
+
- The cached text splitters are now keyed only on the chunking options (`max_tokens`, `overlap`,
|
| 23 |
+
`trim`). Previously every distinct combination of unrelated per-call options (e.g. `bits`, or
|
| 24 |
+
the batch size added above) retained an additional tokenizer-backed `TextSplitter` instance
|
| 25 |
+
- Added the `ISCC_SCT_MODEL_DIR` environment variable to override the embedding model storage
|
| 26 |
+
directory (defaults to the platform-specific user data directory), enabling container images, CI
|
| 27 |
+
caches, and shared model directories (#19)
|
| 28 |
+
- Fixed `__version__`, which still reported `0.2.0` in the 0.2.1 release. The version test now
|
| 29 |
+
compares against the installed package metadata so the two cannot drift apart silently
|
| 30 |
+
|
| 31 |
+
## [0.2.1] - 2026-06-16
|
| 32 |
+
|
| 33 |
+
- Disabled truncation on the tokenizer used for chunk sizing (new `chunking_tokenizer()`, separate
|
| 34 |
+
from the embedding `tokenizer()`). The vendored tokenizer truncates to 128 tokens, which made
|
| 35 |
+
the `tokenizers` >=0.23 chunk sizer emit one overflow encoding per 128 tokens, so sizing a large
|
| 36 |
+
probe string cost O(length) — the dominant cause of the issue #24 super-linear chunking. Sizing
|
| 37 |
+
the full text yields identical token counts and therefore identical chunk boundaries (verified
|
| 38 |
+
against `tests/chunking_vectors.json`) and unchanged ISCC codes, while further reducing the
|
| 39 |
+
guarded worst-case chunking time for PDF-extracted text. The token sizer is simplified
|
| 40 |
+
accordingly (no overflow summing; `count_nonpad_ids` removed)
|
| 41 |
+
- Updated `semantic-text-splitter` to `>=0.32.0`, which adds the upstream "avoid sizing whole
|
| 42 |
+
distant split sections" fix (benbrandt/text-splitter#1184) and releases the GIL during native
|
| 43 |
+
chunking. The upstream fix probes lower-level semantic boundaries, so it speeds up texts with
|
| 44 |
+
distant but present separators; it does **not** cover spans with no intermediate separator at
|
| 45 |
+
all (the issue #24 PDF shape and spaceless CJK), where native chunking stays super-linear, so
|
| 46 |
+
the chunking guard is retained
|
| 47 |
+
|
| 48 |
+
## [0.2.0] - 2026-06-14
|
| 49 |
+
|
| 50 |
+
- Optimized the ONNX embedding model (`iscc-sct-v0.2.0.onnx`): the transformer graph is now fused
|
| 51 |
+
offline via `onnxruntime.transformers.optimizer` (attention, embedding layer norm, skip layer
|
| 52 |
+
norm, bias GELU). CPU inference is 1.25x-1.6x faster depending on hardware; generated ISCC codes
|
| 53 |
+
are unchanged (validated bit-identical at 256 bits across AVX2, AVX512-VNNI and CUDA on 1900+
|
| 54 |
+
chunks). The model weights are identical to v0.1.0 — only the graph structure changed
|
| 55 |
+
- Fixed super-linear `split_text` runtime on texts without regular paragraph breaks (typical for
|
| 56 |
+
PDF-extracted text): such inputs now chunk via a guarded Python token sizer that skips
|
| 57 |
+
tokenizing huge splitter probes (114s → 7s for a 600KB book, #24). Chunk boundaries are
|
| 58 |
+
unchanged and now frozen by test vectors in `tests/chunking_vectors.json`
|
| 59 |
+
- **BREAKING**: `onnxruntime` is no longer a base dependency — install `iscc-sct[cpu]` or
|
| 60 |
+
`iscc-sct[gpu]` instead. Previously the `[gpu]` extra was a silent no-op because the
|
| 61 |
+
unconditional CPU package shadowed `onnxruntime-gpu` (#23)
|
| 62 |
+
- **BREAKING**: dropped Python 3.10 support and added Python 3.14 — `requires-python` is now
|
| 63 |
+
`>=3.11`. `onnxruntime` stopped shipping 3.10 wheels (last was 1.23.2) and added 3.14 wheels, so
|
| 64 |
+
the supported range tracks the runtime
|
| 65 |
+
- **BREAKING**: renamed the CLI command from `sct` to `iscc-sct` (package name = CLI command is the
|
| 66 |
+
standard across all iscc projects); enables `uvx "iscc-sct[cpu]" <file>` one-liners
|
| 67 |
+
- Added an `iscc-sct doctor` command that diagnoses the ONNX runtime (missing, or a CPU package
|
| 68 |
+
shadowing the GPU build), recommends the right `cpu`/`gpu` extra, and installs it on
|
| 69 |
+
confirmation
|
| 70 |
+
- Import `onnxruntime` lazily on first model use, so `import iscc_sct` and the `iscc-sct` CLI load
|
| 71 |
+
without a runtime installed; the instructive `ImportError` now fires on first code generation
|
| 72 |
+
- Warn at runtime when `onnxruntime-gpu` is installed but shadowed by the CPU package
|
| 73 |
+
- Call `onnxruntime.preload_dlls()` before CUDA session creation so pip-provided NVIDIA libraries
|
| 74 |
+
are found without a system-wide CUDA install
|
| 75 |
+
- Migrated project tooling from Poetry to [uv](https://docs.astral.sh/uv/) (uv_build backend)
|
| 76 |
+
- Generate `requirements.txt` from `uv.lock` via `poe export-requirements`
|
| 77 |
+
- Include LICENSE file in built distributions via `license-files`
|
| 78 |
+
- CI: pin uv version, enforce `uv sync --locked`, verify `requirements.txt` matches `uv.lock`
|
| 79 |
+
- CI: derive ONNX model cache directory from `iscc_sct.utils.MODEL_PATH`
|
| 80 |
+
- CI: test matrix now covers Python 3.11–3.14 (dropped 3.10, added 3.14)
|
| 81 |
+
- CI: added a release workflow that re-runs the full test matrix and publishes to PyPI on a
|
| 82 |
+
published GitHub Release, gated by a tag/version guard
|
| 83 |
+
- Updated Hugging Face Space to Gradio 5.26.0 (matches locked version)
|
| 84 |
+
- Fixed `format_yml` glob pattern that only worked on Windows
|
| 85 |
+
- Updated dependencies (Gradio 6, pytest 9, pytest-cov 7, coverage 7.14)
|
| 86 |
+
- Fixed CLI subprocess coverage measurement for pytest-cov 7 via coverage `patch = ["subprocess"]`
|
| 87 |
+
- Adapted Gradio demo to Gradio 6: `theme` and `css` are now passed to `launch()`
|
| 88 |
+
- Added [prek](https://github.com/j178/prek)-based pre-commit hooks (file hygiene checks, ruff
|
| 89 |
+
format/lint, mdformat)
|
| 90 |
+
- Added a Zensical documentation site deployed to GitHub Pages at
|
| 91 |
+
[sct.iscc.codes](https://sct.iscc.codes), including a *For Coding Agents* reference page and
|
| 92 |
+
`llms.txt`/`llms-full.txt` for machine consumption
|
| 93 |
+
|
| 94 |
+
## [0.1.4] - 2025-04-24
|
| 95 |
+
|
| 96 |
+
- Added `bytes_offsets` option to generate UTF-8 byte positions instead of character positions
|
| 97 |
+
- Updated dependencies
|
| 98 |
+
|
| 99 |
+
## [0.1.3] - 2025-04-02
|
| 100 |
+
|
| 101 |
+
- Update license, dependencies, and project metadata.
|
| 102 |
+
- Update dependencies to the latest versions and workflow configurations (Poetry and GitHub
|
| 103 |
+
Actions).
|
| 104 |
+
- Update CLI tests to use dynamic SCT command execution.
|
| 105 |
+
- Add UTF-32BE chunk retrieval test for semantic text code.
|
| 106 |
+
- Fix Python 3.13 support by removing Python 3.9 compatibility and updating version constraints.
|
| 107 |
+
- Enhance documentation in demo.py.
|
| 108 |
|
| 109 |
## [0.1.2] - 2024-08-19
|
| 110 |
+
|
| 111 |
- Encode granular features with base64
|
| 112 |
- Refactor result format to generic ISCC data model
|
| 113 |
- Add optional gradio GUI demo
|
| 114 |
|
| 115 |
## [0.1.1] - 2024-06-25
|
| 116 |
+
|
| 117 |
- Handle text decoding errors gracefully
|
| 118 |
- Handle feature bit-lengths independently
|
| 119 |
- Improve model load time
|
| 120 |
- Improve memory use with batched embedding
|
| 121 |
|
| 122 |
## [0.1.0] - 2024-06-25
|
| 123 |
+
|
| 124 |
- Initial pre-release
|
LICENSE
CHANGED
|
@@ -1,10 +1,201 @@
|
|
| 1 |
-
|
|
|
|
|
|
|
| 2 |
|
| 3 |
-
|
| 4 |
|
| 5 |
-
|
| 6 |
-
Creative Commons, PO Box 1866, Mountain View, CA 94042, USA.
|
| 7 |
|
| 8 |
-
|
|
|
|
| 9 |
|
| 10 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
Apache License
|
| 2 |
+
Version 2.0, January 2004
|
| 3 |
+
http://www.apache.org/licenses/
|
| 4 |
|
| 5 |
+
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
| 6 |
|
| 7 |
+
1. Definitions.
|
|
|
|
| 8 |
|
| 9 |
+
"License" shall mean the terms and conditions for use, reproduction,
|
| 10 |
+
and distribution as defined by Sections 1 through 9 of this document.
|
| 11 |
|
| 12 |
+
"Licensor" shall mean the copyright owner or entity authorized by
|
| 13 |
+
the copyright owner that is granting the License.
|
| 14 |
+
|
| 15 |
+
"Legal Entity" shall mean the union of the acting entity and all
|
| 16 |
+
other entities that control, are controlled by, or are under common
|
| 17 |
+
control with that entity. For the purposes of this definition,
|
| 18 |
+
"control" means (i) the power, direct or indirect, to cause the
|
| 19 |
+
direction or management of such entity, whether by contract or
|
| 20 |
+
otherwise, or (ii) ownership of fifty percent (50%) or more of the
|
| 21 |
+
outstanding shares, or (iii) beneficial ownership of such entity.
|
| 22 |
+
|
| 23 |
+
"You" (or "Your") shall mean an individual or Legal Entity
|
| 24 |
+
exercising permissions granted by this License.
|
| 25 |
+
|
| 26 |
+
"Source" form shall mean the preferred form for making modifications,
|
| 27 |
+
including but not limited to software source code, documentation
|
| 28 |
+
source, and configuration files.
|
| 29 |
+
|
| 30 |
+
"Object" form shall mean any form resulting from mechanical
|
| 31 |
+
transformation or translation of a Source form, including but
|
| 32 |
+
not limited to compiled object code, generated documentation,
|
| 33 |
+
and conversions to other media types.
|
| 34 |
+
|
| 35 |
+
"Work" shall mean the work of authorship, whether in Source or
|
| 36 |
+
Object form, made available under the License, as indicated by a
|
| 37 |
+
copyright notice that is included in or attached to the work
|
| 38 |
+
(an example is provided in the Appendix below).
|
| 39 |
+
|
| 40 |
+
"Derivative Works" shall mean any work, whether in Source or Object
|
| 41 |
+
form, that is based on (or derived from) the Work and for which the
|
| 42 |
+
editorial revisions, annotations, elaborations, or other modifications
|
| 43 |
+
represent, as a whole, an original work of authorship. For the purposes
|
| 44 |
+
of this License, Derivative Works shall not include works that remain
|
| 45 |
+
separable from, or merely link (or bind by name) to the interfaces of,
|
| 46 |
+
the Work and Derivative Works thereof.
|
| 47 |
+
|
| 48 |
+
"Contribution" shall mean any work of authorship, including
|
| 49 |
+
the original version of the Work and any modifications or additions
|
| 50 |
+
to that Work or Derivative Works thereof, that is intentionally
|
| 51 |
+
submitted to Licensor for inclusion in the Work by the copyright owner
|
| 52 |
+
or by an individual or Legal Entity authorized to submit on behalf of
|
| 53 |
+
the copyright owner. For the purposes of this definition, "submitted"
|
| 54 |
+
means any form of electronic, verbal, or written communication sent
|
| 55 |
+
to the Licensor or its representatives, including but not limited to
|
| 56 |
+
communication on electronic mailing lists, source code control systems,
|
| 57 |
+
and issue tracking systems that are managed by, or on behalf of, the
|
| 58 |
+
Licensor for the purpose of discussing and improving the Work, but
|
| 59 |
+
excluding communication that is conspicuously marked or otherwise
|
| 60 |
+
designated in writing by the copyright owner as "Not a Contribution."
|
| 61 |
+
|
| 62 |
+
"Contributor" shall mean Licensor and any individual or Legal Entity
|
| 63 |
+
on behalf of whom a Contribution has been received by Licensor and
|
| 64 |
+
subsequently incorporated within the Work.
|
| 65 |
+
|
| 66 |
+
2. Grant of Copyright License. Subject to the terms and conditions of
|
| 67 |
+
this License, each Contributor hereby grants to You a perpetual,
|
| 68 |
+
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
| 69 |
+
copyright license to reproduce, prepare Derivative Works of,
|
| 70 |
+
publicly display, publicly perform, sublicense, and distribute the
|
| 71 |
+
Work and such Derivative Works in Source or Object form.
|
| 72 |
+
|
| 73 |
+
3. Grant of Patent License. Subject to the terms and conditions of
|
| 74 |
+
this License, each Contributor hereby grants to You a perpetual,
|
| 75 |
+
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
| 76 |
+
(except as stated in this section) patent license to make, have made,
|
| 77 |
+
use, offer to sell, sell, import, and otherwise transfer the Work,
|
| 78 |
+
where such license applies only to those patent claims licensable
|
| 79 |
+
by such Contributor that are necessarily infringed by their
|
| 80 |
+
Contribution(s) alone or by combination of their Contribution(s)
|
| 81 |
+
with the Work to which such Contribution(s) was submitted. If You
|
| 82 |
+
institute patent litigation against any entity (including a
|
| 83 |
+
cross-claim or counterclaim in a lawsuit) alleging that the Work
|
| 84 |
+
or a Contribution incorporated within the Work constitutes direct
|
| 85 |
+
or contributory patent infringement, then any patent licenses
|
| 86 |
+
granted to You under this License for that Work shall terminate
|
| 87 |
+
as of the date such litigation is filed.
|
| 88 |
+
|
| 89 |
+
4. Redistribution. You may reproduce and distribute copies of the
|
| 90 |
+
Work or Derivative Works thereof in any medium, with or without
|
| 91 |
+
modifications, and in Source or Object form, provided that You
|
| 92 |
+
meet the following conditions:
|
| 93 |
+
|
| 94 |
+
(a) You must give any other recipients of the Work or
|
| 95 |
+
Derivative Works a copy of this License; and
|
| 96 |
+
|
| 97 |
+
(b) You must cause any modified files to carry prominent notices
|
| 98 |
+
stating that You changed the files; and
|
| 99 |
+
|
| 100 |
+
(c) You must retain, in the Source form of any Derivative Works
|
| 101 |
+
that You distribute, all copyright, patent, trademark, and
|
| 102 |
+
attribution notices from the Source form of the Work,
|
| 103 |
+
excluding those notices that do not pertain to any part of
|
| 104 |
+
the Derivative Works; and
|
| 105 |
+
|
| 106 |
+
(d) If the Work includes a "NOTICE" text file as part of its
|
| 107 |
+
distribution, then any Derivative Works that You distribute must
|
| 108 |
+
include a readable copy of the attribution notices contained
|
| 109 |
+
within such NOTICE file, excluding those notices that do not
|
| 110 |
+
pertain to any part of the Derivative Works, in at least one
|
| 111 |
+
of the following places: within a NOTICE text file distributed
|
| 112 |
+
as part of the Derivative Works; within the Source form or
|
| 113 |
+
documentation, if provided along with the Derivative Works; or,
|
| 114 |
+
within a display generated by the Derivative Works, if and
|
| 115 |
+
wherever such third-party notices normally appear. The contents
|
| 116 |
+
of the NOTICE file are for informational purposes only and
|
| 117 |
+
do not modify the License. You may add Your own attribution
|
| 118 |
+
notices within Derivative Works that You distribute, alongside
|
| 119 |
+
or as an addendum to the NOTICE text from the Work, provided
|
| 120 |
+
that such additional attribution notices cannot be construed
|
| 121 |
+
as modifying the License.
|
| 122 |
+
|
| 123 |
+
You may add Your own copyright statement to Your modifications and
|
| 124 |
+
may provide additional or different license terms and conditions
|
| 125 |
+
for use, reproduction, or distribution of Your modifications, or
|
| 126 |
+
for any such Derivative Works as a whole, provided Your use,
|
| 127 |
+
reproduction, and distribution of the Work otherwise complies with
|
| 128 |
+
the conditions stated in this License.
|
| 129 |
+
|
| 130 |
+
5. Submission of Contributions. Unless You explicitly state otherwise,
|
| 131 |
+
any Contribution intentionally submitted for inclusion in the Work
|
| 132 |
+
by You to the Licensor shall be under the terms and conditions of
|
| 133 |
+
this License, without any additional terms or conditions.
|
| 134 |
+
Notwithstanding the above, nothing herein shall supersede or modify
|
| 135 |
+
the terms of any separate license agreement you may have executed
|
| 136 |
+
with Licensor regarding such Contributions.
|
| 137 |
+
|
| 138 |
+
6. Trademarks. This License does not grant permission to use the trade
|
| 139 |
+
names, trademarks, service marks, or product names of the Licensor,
|
| 140 |
+
except as required for reasonable and customary use in describing the
|
| 141 |
+
origin of the Work and reproducing the content of the NOTICE file.
|
| 142 |
+
|
| 143 |
+
7. Disclaimer of Warranty. Unless required by applicable law or
|
| 144 |
+
agreed to in writing, Licensor provides the Work (and each
|
| 145 |
+
Contributor provides its Contributions) on an "AS IS" BASIS,
|
| 146 |
+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
| 147 |
+
implied, including, without limitation, any warranties or conditions
|
| 148 |
+
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
|
| 149 |
+
PARTICULAR PURPOSE. You are solely responsible for determining the
|
| 150 |
+
appropriateness of using or redistributing the Work and assume any
|
| 151 |
+
risks associated with Your exercise of permissions under this License.
|
| 152 |
+
|
| 153 |
+
8. Limitation of Liability. In no event and under no legal theory,
|
| 154 |
+
whether in tort (including negligence), contract, or otherwise,
|
| 155 |
+
unless required by applicable law (such as deliberate and grossly
|
| 156 |
+
negligent acts) or agreed to in writing, shall any Contributor be
|
| 157 |
+
liable to You for damages, including any direct, indirect, special,
|
| 158 |
+
incidental, or consequential damages of any character arising as a
|
| 159 |
+
result of this License or out of the use or inability to use the
|
| 160 |
+
Work (including but not limited to damages for loss of goodwill,
|
| 161 |
+
work stoppage, computer failure or malfunction, or any and all
|
| 162 |
+
other commercial damages or losses), even if such Contributor
|
| 163 |
+
has been advised of the possibility of such damages.
|
| 164 |
+
|
| 165 |
+
9. Accepting Warranty or Additional Liability. While redistributing
|
| 166 |
+
the Work or Derivative Works thereof, You may choose to offer,
|
| 167 |
+
and charge a fee for, acceptance of support, warranty, indemnity,
|
| 168 |
+
or other liability obligations and/or rights consistent with this
|
| 169 |
+
License. However, in accepting such obligations, You may act only
|
| 170 |
+
on Your own behalf and on Your sole responsibility, not on behalf
|
| 171 |
+
of any other Contributor, and only if You agree to indemnify,
|
| 172 |
+
defend, and hold each Contributor harmless for any liability
|
| 173 |
+
incurred by, or claims asserted against, such Contributor by reason
|
| 174 |
+
of your accepting any such warranty or additional liability.
|
| 175 |
+
|
| 176 |
+
END OF TERMS AND CONDITIONS
|
| 177 |
+
|
| 178 |
+
APPENDIX: How to apply the Apache License to your work.
|
| 179 |
+
|
| 180 |
+
To apply the Apache License to your work, attach the following
|
| 181 |
+
boilerplate notice, with the fields enclosed by brackets "[]"
|
| 182 |
+
replaced with your own identifying information. (Don't include
|
| 183 |
+
the brackets!) The text should be enclosed in the appropriate
|
| 184 |
+
comment syntax for the file format. We also recommend that a
|
| 185 |
+
file or class name and description of purpose be included on the
|
| 186 |
+
same "printed page" as the copyright notice for easier
|
| 187 |
+
identification within third-party archives.
|
| 188 |
+
|
| 189 |
+
Copyright 2024-2026 Titusz Pan
|
| 190 |
+
|
| 191 |
+
Licensed under the Apache License, Version 2.0 (the "License");
|
| 192 |
+
you may not use this file except in compliance with the License.
|
| 193 |
+
You may obtain a copy of the License at
|
| 194 |
+
|
| 195 |
+
http://www.apache.org/licenses/LICENSE-2.0
|
| 196 |
+
|
| 197 |
+
Unless required by applicable law or agreed to in writing, software
|
| 198 |
+
distributed under the License is distributed on an "AS IS" BASIS,
|
| 199 |
+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
| 200 |
+
See the License for the specific language governing permissions and
|
| 201 |
+
limitations under the License.
|
README.md
CHANGED
|
@@ -1,12 +1,31 @@
|
|
| 1 |
---
|
| 2 |
-
title:
|
| 3 |
-
emoji:
|
| 4 |
-
colorFrom:
|
| 5 |
-
colorTo:
|
| 6 |
sdk: gradio
|
| 7 |
-
sdk_version:
|
|
|
|
| 8 |
app_file: app.py
|
| 9 |
-
pinned:
|
|
|
|
|
|
|
| 10 |
---
|
| 11 |
|
| 12 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
---
|
| 2 |
+
title: ISCC-LAB - Semantic-Code Text
|
| 3 |
+
emoji: 🔮
|
| 4 |
+
colorFrom: red
|
| 5 |
+
colorTo: blue
|
| 6 |
sdk: gradio
|
| 7 |
+
sdk_version: 6.22.0
|
| 8 |
+
python_version: "3.12"
|
| 9 |
app_file: app.py
|
| 10 |
+
pinned: true
|
| 11 |
+
license: cc-by-nc-sa-4.0
|
| 12 |
+
short_description: Cross Lingual Similarity Preserving Text Simprints
|
| 13 |
---
|
| 14 |
|
| 15 |
+
# ISCC Semantic Text-Code Demo
|
| 16 |
+
|
| 17 |
+
Interactive demo for [iscc-sct](https://github.com/iscc/iscc-sct) — an experimental **Semantic-Code
|
| 18 |
+
Text** implementation for the [ISCC](https://core.iscc.codes) (*International Standard Content
|
| 19 |
+
Code*). It generates compact, similarity-preserving binary codes from text: semantically similar
|
| 20 |
+
texts — including translations — produce codes with low hamming distance.
|
| 21 |
+
|
| 22 |
+
Enter two texts (e.g. an original and its translation) to compare their Semantic Text-Codes,
|
| 23 |
+
per-chunk simprints, and similarity.
|
| 24 |
+
|
| 25 |
+
> **This is a proof of concept.** The algorithm is experimental and not part of the official
|
| 26 |
+
> [ISO 24138:2024](https://www.iso.org/standard/77899.html) standard. Releases below v1.0.0 may
|
| 27 |
+
> produce incompatible codes.
|
| 28 |
+
|
| 29 |
+
- Documentation: [sct.iscc.codes](https://sct.iscc.codes)
|
| 30 |
+
- Source code: [github.com/iscc/iscc-sct](https://github.com/iscc/iscc-sct)
|
| 31 |
+
- Package: [pypi.org/project/iscc-sct](https://pypi.org/project/iscc-sct/)
|
app.py
CHANGED
|
@@ -1,4 +1,4 @@
|
|
| 1 |
-
from iscc_sct.demo import demo
|
| 2 |
|
| 3 |
if __name__ == "__main__":
|
| 4 |
-
demo.launch()
|
|
|
|
| 1 |
+
from iscc_sct.demo import demo, launch_kwargs
|
| 2 |
|
| 3 |
if __name__ == "__main__":
|
| 4 |
+
demo.launch(**launch_kwargs)
|
docs/CNAME
ADDED
|
@@ -0,0 +1 @@
|
|
|
|
|
|
|
| 1 |
+
sct.iscc.codes
|
docs/assets/favicon.png
ADDED
|
|
docs/assets/logo_dark.png
ADDED
|
Git LFS Details
|
docs/assets/logo_light.png
ADDED
|
Git LFS Details
|
docs/conventions.md
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Coding Convetions
|
| 2 |
+
|
| 3 |
+
- Prefer httpx over requests for making http requests!
|
| 4 |
+
- Write pragmatic, easily testable, and performant code!
|
| 5 |
+
- Prefer short and pure functions where possible!
|
| 6 |
+
- Keep the number of function arguments below 4!
|
| 7 |
+
- Don´t use nested functions!
|
| 8 |
+
- Write concise and to-the-point docstrings for all functions!
|
| 9 |
+
- Write type comments style (PEP 484) instead of function annotations (PEP 3107)
|
| 10 |
+
- Always add a correct PEP 484 style type comment as the first line after the function definition!
|
| 11 |
+
- Use built-in collection types as generic types for annotations (PEP 585)!
|
| 12 |
+
- Use the | (pipe) operator for writing union types (PEP 604)!
|
| 13 |
+
|
| 14 |
+
Example function definition with (PEP 484) type comment and docstring:
|
| 15 |
+
|
| 16 |
+
```python
|
| 17 |
+
def tokenize_chunks(chunks, max_len=None):
|
| 18 |
+
# type: (list[str], int|None) -> dict
|
| 19 |
+
"""
|
| 20 |
+
Tokenize text chunks into model-compatible formats.
|
| 21 |
+
|
| 22 |
+
:param chunks: Text chunks to tokenize.
|
| 23 |
+
:param max_len: Truncates chunks above max_len characters
|
| 24 |
+
:return: Dictionary of tokenized data including input IDs, attention masks, and type IDs.
|
| 25 |
+
"""
|
| 26 |
+
```
|
docs/explanation/how-it-works.md
ADDED
|
@@ -0,0 +1,118 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
---
|
| 2 |
+
icon: lucide/lightbulb
|
| 3 |
+
description: How iscc-sct turns text into a similarity-preserving binary code, and why the codes match across languages.
|
| 4 |
+
---
|
| 5 |
+
|
| 6 |
+
# How it works
|
| 7 |
+
|
| 8 |
+
This page explains how a Semantic Text-Code is built and why semantically similar texts — including
|
| 9 |
+
translations — produce codes with low Hamming distance.
|
| 10 |
+
|
| 11 |
+
## The problem
|
| 12 |
+
|
| 13 |
+
The standard ISCC Content-Code for text matches on lexical similarity: it compares the words that
|
| 14 |
+
appear in a document. That works well for near-duplicates, but it cannot tell that a German
|
| 15 |
+
translation carries the same meaning as its English original — the two share almost no words.
|
| 16 |
+
|
| 17 |
+
The Semantic Text-Code targets meaning instead of wording. It is built so that texts about the same
|
| 18 |
+
thing land close together in code space, whatever language they are written in.
|
| 19 |
+
|
| 20 |
+
## The pipeline
|
| 21 |
+
|
| 22 |
+
```mermaid
|
| 23 |
+
graph LR
|
| 24 |
+
A[Text] --> B[Split into chunks]
|
| 25 |
+
B --> C[Embed each chunk]
|
| 26 |
+
C --> D[Mean-pool to one vector]
|
| 27 |
+
D --> E[Binarize]
|
| 28 |
+
E --> F[ISCC header + base32]
|
| 29 |
+
```
|
| 30 |
+
|
| 31 |
+
Each stage has a specific job.
|
| 32 |
+
|
| 33 |
+
### Split
|
| 34 |
+
|
| 35 |
+
A document is first split into overlapping chunks at sensible boundaries (up to 127 tokens each,
|
| 36 |
+
with up to 48 tokens of overlap). Overlap keeps a sentence that straddles a boundary from being lost
|
| 37 |
+
to both chunks.
|
| 38 |
+
|
| 39 |
+
Text without regular paragraph breaks — such as text extracted from print-layout PDFs — takes a
|
| 40 |
+
guarded code path that produces the same chunks without the super-linear cost the naive splitter
|
| 41 |
+
would incur on those inputs.
|
| 42 |
+
|
| 43 |
+
### Embed
|
| 44 |
+
|
| 45 |
+
Each chunk is run through a multilingual sentence-transformer model
|
| 46 |
+
([paraphrase-multilingual-MiniLM-L12-v2](https://huggingface.co/sentence-transformers/paraphrase-multilingual-MiniLM-L12-v2))
|
| 47 |
+
exported to ONNX. The model turns a chunk into a 384-dimensional embedding: a vector of numbers that
|
| 48 |
+
encodes the chunk's meaning. The model was trained so that texts with similar meaning — across more
|
| 49 |
+
than 60 languages — map to nearby vectors.
|
| 50 |
+
|
| 51 |
+
The per-token outputs are combined into one chunk vector by attention-mask pooling, then normalized.
|
| 52 |
+
|
| 53 |
+
### Aggregate
|
| 54 |
+
|
| 55 |
+
The chunk vectors are averaged into a single document vector and normalized again. This is the one
|
| 56 |
+
vector that represents the whole document's meaning.
|
| 57 |
+
|
| 58 |
+
### Binarize
|
| 59 |
+
|
| 60 |
+
Each component of the document vector becomes one bit: positive values become `1`, the rest become
|
| 61 |
+
`0`. The full vector yields a 384-bit digest.
|
| 62 |
+
|
| 63 |
+
The digest is truncated to the requested length (`bits`, up to 256), prefixed with a 2-byte ISCC
|
| 64 |
+
header that marks it as a Semantic Text-Code, and encoded as base32 with the `ISCC:` prefix.
|
| 65 |
+
|
| 66 |
+
## Why binarized vectors still match
|
| 67 |
+
|
| 68 |
+
Two texts with similar meaning produce similar embeddings — vectors pointing in nearly the same
|
| 69 |
+
direction. Nearly-aligned vectors agree on the sign of most of their components, so their bit
|
| 70 |
+
patterns agree on most bits, giving a low Hamming distance.
|
| 71 |
+
|
| 72 |
+
Unrelated texts produce roughly perpendicular vectors, which agree on only about half their signs —
|
| 73 |
+
so their codes differ in about half their bits. That gap between "few bits differ" and "about half
|
| 74 |
+
the bits differ" is what makes a similarity threshold meaningful.
|
| 75 |
+
|
| 76 |
+
| Property | Behavior |
|
| 77 |
+
| ----------------- | ------------------------------------------------------- |
|
| 78 |
+
| Similar meaning | Few differing bits (low Hamming distance) |
|
| 79 |
+
| Unrelated content | About 50% of bits differ |
|
| 80 |
+
| Translation | Treated like similar meaning — a near-match |
|
| 81 |
+
| Longer `bits` | Wider spread between near-matches and unrelated content |
|
| 82 |
+
|
| 83 |
+
`iscc_distance()` measures similarity by stripping the `ISCC:` prefix and the 2-byte header, then
|
| 84 |
+
counting the differing bits between the two code bodies. `cosine_similarity()` rescales that
|
| 85 |
+
distance to a `-100`–`+100` score. See [comparing texts](../howto/compare-texts.md).
|
| 86 |
+
|
| 87 |
+
## Cross-lingual matching
|
| 88 |
+
|
| 89 |
+
Cross-lingual matching is a direct consequence of the embedding model. Because the model was trained
|
| 90 |
+
to place a sentence and its translation near each other in vector space, the document vectors stay
|
| 91 |
+
close, the sign patterns stay mostly equal, and the codes stay a few bits apart.
|
| 92 |
+
|
| 93 |
+
No machine translation happens at code time. The shared meaning is captured once, by the model,
|
| 94 |
+
during embedding.
|
| 95 |
+
|
| 96 |
+
## Granular features
|
| 97 |
+
|
| 98 |
+
Alongside the document code, `iscc-sct` can emit a simprint for each chunk. Because chunks carry
|
| 99 |
+
their offsets, you can locate matching passages within documents and align similar passages across
|
| 100 |
+
two documents — even when they sit at different positions. See
|
| 101 |
+
[granular features](../howto/granular-features.md).
|
| 102 |
+
|
| 103 |
+
## Relation to the ISCC standard
|
| 104 |
+
|
| 105 |
+
The Semantic Text-Code is a planned, experimental ISCC-UNIT (MainType SEMANTIC, SubType TEXT). It is
|
| 106 |
+
a proof of concept and **not** part of [ISO 24138:2024](https://www.iso.org/standard/77899.html).
|
| 107 |
+
The codes it produces may change between releases below v1.0.0.
|
| 108 |
+
|
| 109 |
+
The codes are still structurally compatible with the ISCC framework: a standard 2-byte ISCC header
|
| 110 |
+
identifies the unit type and length, so a Semantic Text-Code slots in alongside the other
|
| 111 |
+
ISCC-UNITs.
|
| 112 |
+
|
| 113 |
+
## Further reading
|
| 114 |
+
|
| 115 |
+
- **[Getting started](../tutorials/getting-started.md)** — Try the pipeline hands-on.
|
| 116 |
+
- **[Compare texts](../howto/compare-texts.md)** — Use the distance in practice.
|
| 117 |
+
- **[For Coding Agents](../reference/for-coding-agents.md)** — The exact algorithm, constants, and
|
| 118 |
+
invariants.
|
docs/howto/command-line.md
ADDED
|
@@ -0,0 +1,123 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
---
|
| 2 |
+
icon: lucide/terminal
|
| 3 |
+
description: Generate Semantic Text-Codes from the command line, process files in bulk, run the runtime doctor, and launch the demo.
|
| 4 |
+
---
|
| 5 |
+
|
| 6 |
+
# Command line
|
| 7 |
+
|
| 8 |
+
This guide covers the `iscc-sct` command-line tool: generating codes from files, processing many
|
| 9 |
+
files at once, checking the ONNX runtime, and launching the demo.
|
| 10 |
+
|
| 11 |
+
The CLI is installed with the package. Run `iscc-sct --help` to see every option:
|
| 12 |
+
|
| 13 |
+
```text
|
| 14 |
+
usage: iscc-sct [-h] [-b BITS] [-g] [-d] [-y] [path]
|
| 15 |
+
|
| 16 |
+
Generate Semantic Text-Codes for text files.
|
| 17 |
+
|
| 18 |
+
positional arguments:
|
| 19 |
+
path Path to text files (glob patterns), 'doctor' to check the
|
| 20 |
+
ONNX runtime, or 'gui' for the demo.
|
| 21 |
+
|
| 22 |
+
options:
|
| 23 |
+
-h, --help show this help message and exit
|
| 24 |
+
-b, --bits BITS Bit-Length of Code (default 256)
|
| 25 |
+
-g, --granular Activate granular processing.
|
| 26 |
+
-d, --debug Show debugging messages.
|
| 27 |
+
-y, --yes Auto-confirm the 'doctor' runtime install.
|
| 28 |
+
```
|
| 29 |
+
|
| 30 |
+
## Generate a code from a file
|
| 31 |
+
|
| 32 |
+
Pass a path to print its Semantic Text-Code:
|
| 33 |
+
|
| 34 |
+
```bash
|
| 35 |
+
iscc-sct article.txt
|
| 36 |
+
# ISCC:CADV3GG6JH3XEVRNSVYGCLJ7AAV3BOT5J7EHEZKPFXEGRJ2CTWACGZI
|
| 37 |
+
```
|
| 38 |
+
|
| 39 |
+
The CLI defaults to **256-bit** codes. Set a different length with `--bits`:
|
| 40 |
+
|
| 41 |
+
```bash
|
| 42 |
+
iscc-sct --bits 64 article.txt
|
| 43 |
+
```
|
| 44 |
+
|
| 45 |
+
!!! note "The CLI default differs from the library"
|
| 46 |
+
|
| 47 |
+
`iscc-sct` defaults to 256 bits, but `create()` in Python defaults to 64. Pass `--bits` (or the
|
| 48 |
+
`bits` argument) explicitly when the length matters. See [configuration](configuration.md).
|
| 49 |
+
|
| 50 |
+
## Process multiple files
|
| 51 |
+
|
| 52 |
+
The `path` argument accepts a glob pattern. Quote it so `iscc-sct` expands the pattern itself rather
|
| 53 |
+
than the shell:
|
| 54 |
+
|
| 55 |
+
```bash
|
| 56 |
+
iscc-sct "texts/*.txt"
|
| 57 |
+
```
|
| 58 |
+
|
| 59 |
+
Each matching file prints its code. Files that cannot be decoded as UTF-8 are decoded with a
|
| 60 |
+
detected character set, and empty files are skipped.
|
| 61 |
+
|
| 62 |
+
## Granular output
|
| 63 |
+
|
| 64 |
+
Add `--granular` to print the full metadata — document code plus per-chunk simprints, offsets,
|
| 65 |
+
sizes, and contents — as JSON:
|
| 66 |
+
|
| 67 |
+
```bash
|
| 68 |
+
iscc-sct --granular article.txt
|
| 69 |
+
```
|
| 70 |
+
|
| 71 |
+
See [granular features](granular-features.md) for what the fields mean.
|
| 72 |
+
|
| 73 |
+
## Run without installing
|
| 74 |
+
|
| 75 |
+
With [`uv`](https://docs.astral.sh/uv/), run the CLI in one line without a permanent install:
|
| 76 |
+
|
| 77 |
+
```bash
|
| 78 |
+
uvx "iscc-sct[cpu]" article.txt
|
| 79 |
+
```
|
| 80 |
+
|
| 81 |
+
## Check the ONNX runtime
|
| 82 |
+
|
| 83 |
+
`iscc-sct` needs exactly one ONNX runtime (`cpu` or `gpu` extra). The `doctor` command inspects your
|
| 84 |
+
environment, names the problem, and recommends the right extra:
|
| 85 |
+
|
| 86 |
+
```bash
|
| 87 |
+
iscc-sct doctor
|
| 88 |
+
```
|
| 89 |
+
|
| 90 |
+
```text
|
| 91 |
+
iscc-sct ONNX runtime check
|
| 92 |
+
|
| 93 |
+
ONNX runtime: not installed
|
| 94 |
+
CUDA provider: no
|
| 95 |
+
NVIDIA GPU: no
|
| 96 |
+
|
| 97 |
+
Status: no ONNX runtime installed.
|
| 98 |
+
Recommended: pip install "iscc-sct[cpu]"
|
| 99 |
+
```
|
| 100 |
+
|
| 101 |
+
When a fix is available, `doctor` offers to run it. Add `--yes` to install without the prompt:
|
| 102 |
+
|
| 103 |
+
```bash
|
| 104 |
+
iscc-sct doctor --yes
|
| 105 |
+
```
|
| 106 |
+
|
| 107 |
+
`doctor` also detects the case where `onnxruntime-gpu` is installed but a plain `onnxruntime`
|
| 108 |
+
package has shadowed it (issue #23), and reinstalls only the GPU build.
|
| 109 |
+
|
| 110 |
+
## Launch the demo
|
| 111 |
+
|
| 112 |
+
The `gui` command launches the interactive Gradio demo in your browser. It needs the `demo` extra:
|
| 113 |
+
|
| 114 |
+
```bash
|
| 115 |
+
pip install "iscc-sct[cpu,demo]"
|
| 116 |
+
iscc-sct gui
|
| 117 |
+
```
|
| 118 |
+
|
| 119 |
+
## Related pages
|
| 120 |
+
|
| 121 |
+
- **[Getting started](../tutorials/getting-started.md)** — Install and first code.
|
| 122 |
+
- **[Configuration](configuration.md)** — Options and environment variables.
|
| 123 |
+
- **[How it works](../explanation/how-it-works.md)** — What happens behind a code.
|
docs/howto/compare-texts.md
ADDED
|
@@ -0,0 +1,122 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
---
|
| 2 |
+
icon: lucide/git-compare
|
| 3 |
+
description: Measure semantic and cross-lingual similarity between two texts using ISCC Semantic Text-Codes.
|
| 4 |
+
---
|
| 5 |
+
|
| 6 |
+
# Compare texts
|
| 7 |
+
|
| 8 |
+
This guide shows how to measure how similar two texts are in meaning — including across languages —
|
| 9 |
+
by comparing their Semantic Text-Codes.
|
| 10 |
+
|
| 11 |
+
Similarity is computed from the codes, not the original text. You can store or share the codes and
|
| 12 |
+
still compare content without keeping the source documents.
|
| 13 |
+
|
| 14 |
+
## Compare two whole documents
|
| 15 |
+
|
| 16 |
+
Generate a code for each text, then measure the Hamming distance between them with
|
| 17 |
+
`iscc_distance()`. It returns the number of differing bits — lower means more similar:
|
| 18 |
+
|
| 19 |
+
```python
|
| 20 |
+
import iscc_sct as sct
|
| 21 |
+
|
| 22 |
+
english = (
|
| 23 |
+
"An ISCC applies to a specific digital asset and is a data-descriptor deterministically "
|
| 24 |
+
"constructed from multiple hash digests using the algorithms and rules in this document. "
|
| 25 |
+
"This document does not provide information on registration of ISCCs."
|
| 26 |
+
)
|
| 27 |
+
german = (
|
| 28 |
+
"Ein ISCC bezieht sich auf ein bestimmtes digitales Gut und ist ein Daten-Deskriptor, der "
|
| 29 |
+
"deterministisch aus mehreren Hash-Digests unter Verwendung der Algorithmen und Regeln in "
|
| 30 |
+
"diesem Dokument erstellt wird. Dieses Dokument enthält keine Informationen über die "
|
| 31 |
+
"Registrierung von ISCCs."
|
| 32 |
+
)
|
| 33 |
+
|
| 34 |
+
a = sct.create(english)
|
| 35 |
+
b = sct.create(german)
|
| 36 |
+
|
| 37 |
+
print(sct.iscc_distance(a.iscc, b.iscc))
|
| 38 |
+
# 3
|
| 39 |
+
```
|
| 40 |
+
|
| 41 |
+
`iscc_distance()` strips the `ISCC:` prefix and the 2-byte header, then compares the code bodies.
|
| 42 |
+
The two codes must be the same bit-length, or it raises `ValueError`.
|
| 43 |
+
|
| 44 |
+
## Read the distance
|
| 45 |
+
|
| 46 |
+
Distance is measured in bits, so the scale depends on the code length. Compare a translation against
|
| 47 |
+
an unrelated text at the same length to see the spread:
|
| 48 |
+
|
| 49 |
+
```python
|
| 50 |
+
import iscc_sct as sct
|
| 51 |
+
|
| 52 |
+
unrelated = (
|
| 53 |
+
"The recipe calls for two cups of flour, a pinch of salt, and three ripe bananas mashed "
|
| 54 |
+
"until smooth before folding the mixture into the buttered baking tin."
|
| 55 |
+
)
|
| 56 |
+
u = sct.create(unrelated)
|
| 57 |
+
|
| 58 |
+
print(sct.iscc_distance(a.iscc, b.iscc)) # translation
|
| 59 |
+
# 3
|
| 60 |
+
print(sct.iscc_distance(a.iscc, u.iscc)) # unrelated
|
| 61 |
+
# 33
|
| 62 |
+
```
|
| 63 |
+
|
| 64 |
+
A translation sits a few bits apart; unrelated content sits near half the bit-length away (random
|
| 65 |
+
codes differ in about 50% of their bits).
|
| 66 |
+
|
| 67 |
+
## Choose a bit-length
|
| 68 |
+
|
| 69 |
+
Longer codes spread similar and dissimilar texts further apart, which makes a threshold easier to
|
| 70 |
+
pick. The same three texts at 256 bits:
|
| 71 |
+
|
| 72 |
+
```python
|
| 73 |
+
import iscc_sct as sct
|
| 74 |
+
|
| 75 |
+
a256 = sct.create(english, bits=256)
|
| 76 |
+
b256 = sct.create(german, bits=256)
|
| 77 |
+
u256 = sct.create(unrelated, bits=256)
|
| 78 |
+
|
| 79 |
+
print(sct.iscc_distance(a256.iscc, b256.iscc)) # translation
|
| 80 |
+
# 23
|
| 81 |
+
print(sct.iscc_distance(a256.iscc, u256.iscc)) # unrelated
|
| 82 |
+
# 123
|
| 83 |
+
```
|
| 84 |
+
|
| 85 |
+
| Goal | Suggested length |
|
| 86 |
+
| ------------------------------------------ | ---------------- |
|
| 87 |
+
| Compact storage, coarse matching | 64 bits |
|
| 88 |
+
| Balanced precision | 128 bits |
|
| 89 |
+
| Finest discrimination between near-matches | 256 bits |
|
| 90 |
+
|
| 91 |
+
Both codes in a comparison must use the same `bits` value. See [configuration](configuration.md) for
|
| 92 |
+
how to set the default.
|
| 93 |
+
|
| 94 |
+
## Score similarity from raw digests
|
| 95 |
+
|
| 96 |
+
`iscc_distance()` works on code strings. To compare raw digests directly — for example the output of
|
| 97 |
+
`soft_hash_text_semantic()` — use `cosine_similarity()`, which scales the distance to a `-100` to
|
| 98 |
+
`+100` score:
|
| 99 |
+
|
| 100 |
+
```python
|
| 101 |
+
import iscc_sct as sct
|
| 102 |
+
|
| 103 |
+
a = sct.soft_hash_text_semantic("An ISCC applies to a specific digital asset.")
|
| 104 |
+
b = sct.soft_hash_text_semantic("Ein ISCC bezieht sich auf ein bestimmtes digitales Gut.")
|
| 105 |
+
|
| 106 |
+
print(sct.cosine_similarity(a, b))
|
| 107 |
+
# 71
|
| 108 |
+
```
|
| 109 |
+
|
| 110 |
+
`hamming_distance()` returns the raw bit distance between two equal-length digests if you want the
|
| 111 |
+
unscaled value. Both functions raise `ValueError` when the inputs differ in length.
|
| 112 |
+
|
| 113 |
+
## Match passages instead of whole documents
|
| 114 |
+
|
| 115 |
+
To find which parts of two documents are similar — rather than scoring them as a whole — use
|
| 116 |
+
granular features and `granular_similarity()`. See [granular features](granular-features.md).
|
| 117 |
+
|
| 118 |
+
## Related pages
|
| 119 |
+
|
| 120 |
+
- **[Granular features](granular-features.md)** — Chunk-level matching with simprints.
|
| 121 |
+
- **[How it works](../explanation/how-it-works.md)** — Why distance reflects meaning.
|
| 122 |
+
- **[API reference](../reference/api.md)** — Signatures for every similarity function.
|
docs/howto/configuration.md
ADDED
|
@@ -0,0 +1,126 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
---
|
| 2 |
+
icon: lucide/settings
|
| 3 |
+
description: Configure iscc-sct with per-call options, environment variables, or a global settings override.
|
| 4 |
+
---
|
| 5 |
+
|
| 6 |
+
# Configuration
|
| 7 |
+
|
| 8 |
+
This guide shows how to control code length, chunking, and output detail through options — per call,
|
| 9 |
+
through environment variables, or globally.
|
| 10 |
+
|
| 11 |
+
Every option has the same name everywhere: as a keyword argument, as an `ISCC_SCT_*` environment
|
| 12 |
+
variable, and as a field on the `SctOptions` model.
|
| 13 |
+
|
| 14 |
+
## Options reference
|
| 15 |
+
|
| 16 |
+
| Option | Env variable | Default | Notes |
|
| 17 |
+
| ------------------ | --------------------------- | ------- | --------------------------------------------------------------- |
|
| 18 |
+
| `bits` | `ISCC_SCT_BITS` | `64` | Document code length. 32–256, multiple of 32. |
|
| 19 |
+
| `bits_granular` | `ISCC_SCT_BITS_GRANULAR` | `64` | Granular simprint length. 32–256, multiple of 32. |
|
| 20 |
+
| `characters` | `ISCC_SCT_CHARACTERS` | `True` | Include the document character count. |
|
| 21 |
+
| `embedding` | `ISCC_SCT_EMBEDDING` | `False` | Include the global document embedding vector. |
|
| 22 |
+
| `precision` | `ISCC_SCT_PRECISION` | `8` | Max fractional digits when storing the embedding. |
|
| 23 |
+
| `simprints` | `ISCC_SCT_SIMPRINTS` | `False` | Include granular per-chunk simprints. |
|
| 24 |
+
| `offsets` | `ISCC_SCT_OFFSETS` | `False` | Include per-chunk offsets. |
|
| 25 |
+
| `byte_offsets` | `ISCC_SCT_BYTE_OFFSETS` | `False` | Report UTF-8 byte offsets instead of character offsets. |
|
| 26 |
+
| `sizes` | `ISCC_SCT_SIZES` | `False` | Include per-chunk sizes. |
|
| 27 |
+
| `contents` | `ISCC_SCT_CONTENTS` | `False` | Include the per-chunk text. |
|
| 28 |
+
| `max_tokens` | `ISCC_SCT_MAX_TOKENS` | `127` | Max tokens per chunk. Cannot exceed 127. |
|
| 29 |
+
| `overlap` | `ISCC_SCT_OVERLAP` | `48` | Max tokens shared between adjacent chunks. |
|
| 30 |
+
| `trim` | `ISCC_SCT_TRIM` | `False` | Trim whitespace from chunks. |
|
| 31 |
+
| `batch_size` | `ISCC_SCT_BATCH_SIZE` | `0` | Chunks per inference batch. `0` = auto (1 on CPU, 100 on CUDA). |
|
| 32 |
+
| `intra_op_threads` | `ISCC_SCT_INTRA_OP_THREADS` | `0` | ONNX Runtime threads per operator. `0` = runtime default. |
|
| 33 |
+
|
| 34 |
+
The `granular=True` shortcut on `create()` is equivalent to setting `simprints`, `offsets`, `sizes`,
|
| 35 |
+
and `contents` to `True` at once.
|
| 36 |
+
|
| 37 |
+
The two inference options only change resource usage, never the generated codes. `intra_op_threads`
|
| 38 |
+
configures the process-wide inference session, so it only takes effect through the global options or
|
| 39 |
+
an environment variable before the first embedding call — worker pools with one process per core
|
| 40 |
+
should set it to `1` to avoid thread oversubscription.
|
| 41 |
+
|
| 42 |
+
## Override per call
|
| 43 |
+
|
| 44 |
+
Pass options as keyword arguments to `create()`. They apply to that call only:
|
| 45 |
+
|
| 46 |
+
```python
|
| 47 |
+
import iscc_sct as sct
|
| 48 |
+
|
| 49 |
+
text = "This is some sample text. It can be a longer document or even an entire book."
|
| 50 |
+
meta = sct.create(text, bits=128, simprints=True, contents=True)
|
| 51 |
+
print(meta.iscc)
|
| 52 |
+
# ISCC:CABV3GG6JH3XEVRNSVYGCLJ7AAV3A
|
| 53 |
+
```
|
| 54 |
+
|
| 55 |
+
## Set defaults with environment variables
|
| 56 |
+
|
| 57 |
+
Set any option through its `ISCC_SCT_*` variable. This changes the default for every call in the
|
| 58 |
+
process:
|
| 59 |
+
|
| 60 |
+
```bash
|
| 61 |
+
export ISCC_SCT_BITS=128
|
| 62 |
+
export ISCC_SCT_MAX_TOKENS=100
|
| 63 |
+
```
|
| 64 |
+
|
| 65 |
+
You can also place these in a `.env` file in the working directory — `iscc-sct` loads it
|
| 66 |
+
automatically on import:
|
| 67 |
+
|
| 68 |
+
```ini
|
| 69 |
+
# .env
|
| 70 |
+
ISCC_SCT_BITS=128
|
| 71 |
+
ISCC_SCT_OVERLAP=24
|
| 72 |
+
```
|
| 73 |
+
|
| 74 |
+
## Override the global settings
|
| 75 |
+
|
| 76 |
+
`sct_opts` is the global settings instance. Use `override()` to get a validated copy with some
|
| 77 |
+
fields changed, without mutating the global:
|
| 78 |
+
|
| 79 |
+
```python
|
| 80 |
+
import iscc_sct as sct
|
| 81 |
+
|
| 82 |
+
opts = sct.sct_opts.override({"bits": 128})
|
| 83 |
+
print(opts.bits) # 128
|
| 84 |
+
print(sct.sct_opts.bits) # 64 — the global is unchanged
|
| 85 |
+
```
|
| 86 |
+
|
| 87 |
+
Prefer `override()` or per-call keyword arguments over assigning to `sct_opts` fields directly. Both
|
| 88 |
+
keep the global default predictable for other code in the same process.
|
| 89 |
+
|
| 90 |
+
## Model storage directory
|
| 91 |
+
|
| 92 |
+
The ~450 MB embedding model is downloaded on first use into the platform-specific user data
|
| 93 |
+
directory. The `ISCC_SCT_MODEL_DIR` environment variable overrides that location — useful for
|
| 94 |
+
container images, CI caches, or a model directory shared between users:
|
| 95 |
+
|
| 96 |
+
```bash
|
| 97 |
+
export ISCC_SCT_MODEL_DIR=/opt/shared/iscc-sct
|
| 98 |
+
```
|
| 99 |
+
|
| 100 |
+
Unlike the options above, `ISCC_SCT_MODEL_DIR` is not an `SctOptions` field — the path is resolved
|
| 101 |
+
when `iscc_sct` is imported, so set it (or place it in `.env`) before importing.
|
| 102 |
+
|
| 103 |
+
## Validation
|
| 104 |
+
|
| 105 |
+
Options are validated whenever they are set. An out-of-range or wrong-shaped value raises a pydantic
|
| 106 |
+
`ValidationError`:
|
| 107 |
+
|
| 108 |
+
```python
|
| 109 |
+
import iscc_sct as sct
|
| 110 |
+
|
| 111 |
+
sct.sct_opts.override({"bits": 100}) # not a multiple of 32 -> ValidationError
|
| 112 |
+
sct.sct_opts.override({"max_tokens": 200}) # exceeds 127 -> ValidationError
|
| 113 |
+
```
|
| 114 |
+
|
| 115 |
+
!!! warning "Library and CLI defaults differ"
|
| 116 |
+
|
| 117 |
+
`create()` and `SctOptions` default `bits` to **64**. The `iscc-sct` command-line tool defaults
|
| 118 |
+
`--bits` to **256**. The same text therefore yields a different code length depending on which entry
|
| 119 |
+
point you use. Set `bits` explicitly when you need a specific length.
|
| 120 |
+
|
| 121 |
+
## Related pages
|
| 122 |
+
|
| 123 |
+
- **[Compare texts](compare-texts.md)** — How `bits` affects matching.
|
| 124 |
+
- **[Granular features](granular-features.md)** — `bits_granular`, offsets, and chunk contents.
|
| 125 |
+
- **[Command line](command-line.md)** — Setting options from the CLI.
|
| 126 |
+
- **[API reference](../reference/api.md)** — The `SctOptions` model in full.
|
docs/howto/granular-features.md
ADDED
|
@@ -0,0 +1,156 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
---
|
| 2 |
+
icon: lucide/layers
|
| 3 |
+
description: Generate per-chunk simprints, choose offset and feature formats, reconstruct text, and match passages across documents.
|
| 4 |
+
---
|
| 5 |
+
|
| 6 |
+
# Granular features
|
| 7 |
+
|
| 8 |
+
This guide shows how to work with granular features: the per-chunk fingerprints that let you match
|
| 9 |
+
individual passages, locate them in the source text, and reconstruct content.
|
| 10 |
+
|
| 11 |
+
A document code summarizes a whole text in one code. Granular features instead describe each chunk
|
| 12 |
+
the text was split into, so you can compare documents passage by passage.
|
| 13 |
+
|
| 14 |
+
## Generate granular features
|
| 15 |
+
|
| 16 |
+
Pass `granular=True` to `create()`. Each chunk becomes a `Feature` with a simprint (its
|
| 17 |
+
similarity-preserving fingerprint), a character `offset`, a `size`, and the chunk `content`:
|
| 18 |
+
|
| 19 |
+
```python
|
| 20 |
+
import iscc_sct as sct
|
| 21 |
+
|
| 22 |
+
text = "This is some sample text. It can be a longer document or even an entire book."
|
| 23 |
+
meta = sct.create(text, bits=256, granular=True)
|
| 24 |
+
|
| 25 |
+
feature = meta.features[0].simprints[0]
|
| 26 |
+
print(feature.simprint, feature.offset, feature.size)
|
| 27 |
+
# XZjeSfdyVi0 0 77
|
| 28 |
+
print(feature.content)
|
| 29 |
+
# This is some sample text. It can be a longer document or even an entire book.
|
| 30 |
+
```
|
| 31 |
+
|
| 32 |
+
Short texts produce a single chunk. Longer texts produce many — see
|
| 33 |
+
[matching passages](#match-passages-across-documents) below.
|
| 34 |
+
|
| 35 |
+
## Choose a feature format
|
| 36 |
+
|
| 37 |
+
Granular features come in two interchangeable shapes:
|
| 38 |
+
|
| 39 |
+
| Format | Shape | Produced by |
|
| 40 |
+
| ------------- | ------------------------------------------------------------ | -------------------------- |
|
| 41 |
+
| Object-Format | a list of self-contained `Feature` objects | `create(granular=True)` |
|
| 42 |
+
| Index-Format | parallel arrays: `simprints`, `offsets`, `sizes`, `contents` | `gen_text_code_semantic()` |
|
| 43 |
+
|
| 44 |
+
`create()` returns Object-Format, which is easy to read and iterate. Convert between the two with
|
| 45 |
+
`to_index_format()` and `to_object_format()`:
|
| 46 |
+
|
| 47 |
+
```python
|
| 48 |
+
import iscc_sct as sct
|
| 49 |
+
|
| 50 |
+
meta = sct.create(text, bits=256, granular=True) # Object-Format
|
| 51 |
+
|
| 52 |
+
index = meta.to_index_format()
|
| 53 |
+
print(index.features[0].simprints) # ['XZjeSfdyVi0']
|
| 54 |
+
print(index.features[0].offsets) # [0]
|
| 55 |
+
```
|
| 56 |
+
|
| 57 |
+
Index-Format stores related attributes in parallel arrays, which is compact for storage and bulk
|
| 58 |
+
indexing. The low-level `gen_text_code_semantic()` returns a plain dict already in Index-Format.
|
| 59 |
+
|
| 60 |
+
## Control the simprint length
|
| 61 |
+
|
| 62 |
+
Granular simprints have their own bit-length, set by `bits_granular` (default **64**). It is
|
| 63 |
+
independent of the document `bits`, so `bits=256, granular=True` still yields 64-bit simprints:
|
| 64 |
+
|
| 65 |
+
```python
|
| 66 |
+
import iscc_sct as sct
|
| 67 |
+
|
| 68 |
+
meta = sct.create(text, bits=256, granular=True, bits_granular=256)
|
| 69 |
+
digest = sct.decode_base64(meta.features[0].simprints[0].simprint)
|
| 70 |
+
print(len(digest) * 8)
|
| 71 |
+
# 256
|
| 72 |
+
```
|
| 73 |
+
|
| 74 |
+
Shorter simprints save space; longer simprints discriminate finer between near-matching passages.
|
| 75 |
+
|
| 76 |
+
## Reconstruct the original text
|
| 77 |
+
|
| 78 |
+
When features include both `offset` and `content`, `get_content()` stitches the chunks back into the
|
| 79 |
+
original text, removing the overlap between adjacent chunks:
|
| 80 |
+
|
| 81 |
+
```python
|
| 82 |
+
import iscc_sct as sct
|
| 83 |
+
|
| 84 |
+
en = (
|
| 85 |
+
"The International Standard Content Code identifies digital content of any media type. "
|
| 86 |
+
"It is generated algorithmically from the content itself, much like a cryptographic hash. "
|
| 87 |
+
"Unlike a cryptographic hash, the ISCC preserves similarity between related items. "
|
| 88 |
+
"Two near-duplicate files therefore receive two codes that are close to each other. "
|
| 89 |
+
"This property supports deduplication, similarity clustering, and content provenance. "
|
| 90 |
+
"The Semantic Text-Code extends these ideas to the meaning of text across languages. "
|
| 91 |
+
"A translation of a document keeps a code that stays close to the original code. "
|
| 92 |
+
"That makes cross-lingual search and matching practical without machine translation."
|
| 93 |
+
)
|
| 94 |
+
meta = sct.create(en, granular=True)
|
| 95 |
+
|
| 96 |
+
print(len(meta.features[0].simprints)) # number of chunks
|
| 97 |
+
# 2
|
| 98 |
+
print(meta.get_content() == en)
|
| 99 |
+
# True
|
| 100 |
+
```
|
| 101 |
+
|
| 102 |
+
`get_overlaps()` returns the overlapping text between consecutive chunks if you need to inspect the
|
| 103 |
+
chunk boundaries directly.
|
| 104 |
+
|
| 105 |
+
## Use byte offsets
|
| 106 |
+
|
| 107 |
+
Offsets and sizes are character positions by default. Enable `byte_offsets` to report UTF-8 byte
|
| 108 |
+
positions instead — useful for fetching a chunk by random access from a remote file:
|
| 109 |
+
|
| 110 |
+
```python
|
| 111 |
+
import iscc_sct as sct
|
| 112 |
+
|
| 113 |
+
meta = sct.create(en, granular=True, byte_offsets=True)
|
| 114 |
+
print(meta.features[0].byte_offsets) # True
|
| 115 |
+
print(meta.features[0].simprints[1].offset) # byte offset of the second chunk
|
| 116 |
+
```
|
| 117 |
+
|
| 118 |
+
## Match passages across documents
|
| 119 |
+
|
| 120 |
+
`granular_similarity()` compares the simprints of two `Metadata` objects and returns the matching
|
| 121 |
+
passages above a similarity threshold. Each result is a `(Feature, score, Feature)` tuple, where the
|
| 122 |
+
score is the `cosine_similarity` between the two simprints:
|
| 123 |
+
|
| 124 |
+
```python
|
| 125 |
+
import iscc_sct as sct
|
| 126 |
+
|
| 127 |
+
de = (
|
| 128 |
+
"Der International Standard Content Code identifiziert digitale Inhalte jedes Medientyps. "
|
| 129 |
+
"Er wird algorithmisch aus dem Inhalt selbst erzeugt, ähnlich einem kryptografischen Hash. "
|
| 130 |
+
"Anders als ein kryptografischer Hash bewahrt der ISCC die Ähnlichkeit verwandter Objekte. "
|
| 131 |
+
"Zwei nahezu identische Dateien erhalten daher zwei Codes, die nahe beieinander liegen. "
|
| 132 |
+
"Diese Eigenschaft unterstützt Deduplizierung, Ähnlichkeits-Clustering und Herkunftsnachweis. "
|
| 133 |
+
"Der Semantic Text-Code überträgt diese Ideen auf die Bedeutung von Text über Sprachen hinweg. "
|
| 134 |
+
"Eine Übersetzung eines Dokuments behält einen Code, der nahe am Originalcode bleibt. "
|
| 135 |
+
"Das macht sprachübergreifende Suche und Zuordnung ohne maschinelle Übersetzung praktikabel."
|
| 136 |
+
)
|
| 137 |
+
|
| 138 |
+
a = sct.create(en, granular=True)
|
| 139 |
+
b = sct.create(de, granular=True)
|
| 140 |
+
|
| 141 |
+
for feat_a, score, feat_b in sct.granular_similarity(a, b, threshold=80):
|
| 142 |
+
print(score, feat_a.offset, feat_b.offset)
|
| 143 |
+
# 81 0 0
|
| 144 |
+
# 90 340 356
|
| 145 |
+
```
|
| 146 |
+
|
| 147 |
+
Each English passage is matched to its closest German counterpart, even though the offsets differ
|
| 148 |
+
because the languages produce text of different lengths. Raise `threshold` to keep only the
|
| 149 |
+
strongest matches; lower it to surface weaker ones. Only the single best match per passage in the
|
| 150 |
+
first document is returned.
|
| 151 |
+
|
| 152 |
+
## Related pages
|
| 153 |
+
|
| 154 |
+
- **[Compare texts](compare-texts.md)** — Whole-document similarity.
|
| 155 |
+
- **[Configuration](configuration.md)** — Defaults for `bits_granular`, chunking, and offsets.
|
| 156 |
+
- **[How it works](../explanation/how-it-works.md)** — How chunks and overlaps are produced.
|
docs/includes/abbreviations.md
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
*[ISCC]: International Standard Content Code (ISO 24138)
|
| 2 |
+
*[SCT]: Semantic Text-Code
|
| 3 |
+
*[ISCC-UNIT]: A single self-describing component of an ISCC
|
| 4 |
+
*[ONNX]: Open Neural Network Exchange
|
| 5 |
+
*[CPU]: Central Processing Unit
|
| 6 |
+
*[GPU]: Graphics Processing Unit
|
| 7 |
+
*[CUDA]: Compute Unified Device Architecture (NVIDIA)
|
| 8 |
+
*[MRL]: Matryoshka Representation Learning
|
| 9 |
+
*[CLI]: Command-Line Interface
|
| 10 |
+
*[API]: Application Programming Interface
|
| 11 |
+
*[PDF]: Portable Document Format
|
| 12 |
+
*[UTF-8]: 8-bit Unicode Transformation Format
|
docs/index.md
ADDED
|
@@ -0,0 +1,157 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
---
|
| 2 |
+
icon: lucide/house
|
| 3 |
+
description: Cross-lingual, similarity-preserving semantic ISCC code for text content. Semantically similar texts, including translations, produce codes with low hamming distance.
|
| 4 |
+
---
|
| 5 |
+
|
| 6 |
+
# iscc-sct
|
| 7 |
+
|
| 8 |
+
[](https://github.com/iscc/iscc-sct/actions/workflows/tests.yml)
|
| 9 |
+
[](https://pypi.python.org/pypi/iscc-sct/)
|
| 10 |
+
[](https://pepy.tech/project/iscc-sct)
|
| 11 |
+
[](https://github.com/iscc/iscc-sct/blob/main/LICENSE)
|
| 12 |
+
[](https://deepwiki.com/iscc/iscc-sct)
|
| 13 |
+
|
| 14 |
+
**A cross-lingual, similarity-preserving binary code for text. Semantically similar texts, including
|
| 15 |
+
translations, produce codes with low hamming distance.**
|
| 16 |
+
|
| 17 |
+
!!! warning "Proof of concept"
|
| 18 |
+
|
| 19 |
+
All releases below v1.0.0 may break backward compatibility and produce incompatible Semantic
|
| 20 |
+
Text-Codes. The algorithms in `iscc-sct` are experimental and **not** part of the official
|
| 21 |
+
[ISO 24138:2024](https://www.iso.org/standard/77899.html) standard.
|
| 22 |
+
|
| 23 |
+
## Introduction
|
| 24 |
+
|
| 25 |
+
The [ISCC](https://iscc.codes) framework already includes a Text-Code based on lexical similarity
|
| 26 |
+
for near-duplicate matching. The Semantic Text-Code (SCT) is a planned additional ISCC-UNIT that
|
| 27 |
+
captures a broader, more abstract similarity. It is engineered to be robust against rephrasing and,
|
| 28 |
+
most notably, translations that lexical matching cannot detect.
|
| 29 |
+
|
| 30 |
+
`iscc-sct` turns any text into a compact binary code built from a binarized, multilingual document
|
| 31 |
+
embedding. The same content expressed in different languages maps to **(near-)identical codes**,
|
| 32 |
+
opening up cross-lingual content identification and similarity detection.
|
| 33 |
+
|
| 34 |
+
| Feature | ISCC Content-Code Text | ISCC Semantic-Code Text |
|
| 35 |
+
| ------------- | ------------------------ | --------------------------------- |
|
| 36 |
+
| Focus | Lexical similarity | Semantic similarity |
|
| 37 |
+
| Cross-lingual | No | Yes |
|
| 38 |
+
| Use case | Near-duplicate detection | Semantic similarity, translations |
|
| 39 |
+
|
| 40 |
+
**Key features:**
|
| 41 |
+
|
| 42 |
+
- **Semantic similarity** - deep multilingual embeddings capture the meaning of the text
|
| 43 |
+
- **Translation matching** - near-identical codes for the same content across languages
|
| 44 |
+
- **Bit-length flexibility** - codes from 32 up to 256 bits for adjustable granularity
|
| 45 |
+
- **ISCC compatible** - codes integrate directly with existing ISCC-based systems
|
| 46 |
+
- **60+ languages** - one model covers a broad range of scripts and language pairs
|
| 47 |
+
|
| 48 |
+
## Quick start
|
| 49 |
+
|
| 50 |
+
=== "pip"
|
| 51 |
+
|
| 52 |
+
```bash
|
| 53 |
+
pip install "iscc-sct[cpu]"
|
| 54 |
+
```
|
| 55 |
+
|
| 56 |
+
=== "uv"
|
| 57 |
+
|
| 58 |
+
```bash
|
| 59 |
+
uv add "iscc-sct[cpu]"
|
| 60 |
+
```
|
| 61 |
+
|
| 62 |
+
=== "uvx (no install)"
|
| 63 |
+
|
| 64 |
+
```bash
|
| 65 |
+
uvx "iscc-sct[cpu]" "path/to/textfile.txt"
|
| 66 |
+
```
|
| 67 |
+
|
| 68 |
+
Generate a Semantic Text-Code from Python:
|
| 69 |
+
|
| 70 |
+
```python
|
| 71 |
+
import iscc_sct as sct
|
| 72 |
+
|
| 73 |
+
text = "This is some sample text. It can be a longer document or even an entire book."
|
| 74 |
+
print(sct.create(text, bits=256).iscc)
|
| 75 |
+
# ISCC:CADV3GG6JH3XEVRNSVYGCLJ7AAV3BOT5J7EHEZKPFXEGRJ2CTWACGZI
|
| 76 |
+
```
|
| 77 |
+
|
| 78 |
+
Or from the command line:
|
| 79 |
+
|
| 80 |
+
```bash
|
| 81 |
+
iscc-sct "path/to/textfile.txt"
|
| 82 |
+
```
|
| 83 |
+
|
| 84 |
+
!!! note "Choose exactly one ONNX runtime"
|
| 85 |
+
|
| 86 |
+
A plain `pip install iscc-sct` installs no ONNX runtime; the first code generation then fails with
|
| 87 |
+
install instructions. Install the `cpu` extra (works everywhere) or the `gpu` extra for NVIDIA CUDA
|
| 88 |
+
acceleration (requires CUDA 12.x and cuDNN 9.x), or run `iscc-sct doctor` to detect and install the
|
| 89 |
+
right one. Never install both: `onnxruntime` and `onnxruntime-gpu` unpack into the same directory
|
| 90 |
+
and silently overwrite each other.
|
| 91 |
+
|
| 92 |
+
## How it works
|
| 93 |
+
|
| 94 |
+
```text
|
| 95 |
+
Text -> split -> embed -> mean-pool -> binarize -> ISCC header + base32
|
| 96 |
+
```
|
| 97 |
+
|
| 98 |
+
1. **Split** the text into overlapping chunks at syntactically sensible boundaries.
|
| 99 |
+
1. **Embed** each chunk with a multilingual sentence-transformer running on ONNX.
|
| 100 |
+
1. **Aggregate** the chunk embeddings into one mean-pooled, normalized document vector.
|
| 101 |
+
1. **Binarize** the vector (positive components become 1-bits), truncate to the requested
|
| 102 |
+
bit-length, prefix the ISCC header, and base32-encode it.
|
| 103 |
+
|
| 104 |
+
This process is robust to variation and translation, enabling cross-lingual matching from a short
|
| 105 |
+
Simprint.
|
| 106 |
+
|
| 107 |
+
## Documentation
|
| 108 |
+
|
| 109 |
+
<div class="grid cards" markdown>
|
| 110 |
+
|
| 111 |
+
- **[Getting started](tutorials/getting-started.md)** - Tutorial
|
| 112 |
+
|
| 113 |
+
Install, generate your first code, and match a translation across languages.
|
| 114 |
+
|
| 115 |
+
- **[How-to guides](howto/compare-texts.md)** - Task recipes
|
| 116 |
+
|
| 117 |
+
Compare texts, work with granular features, configure options, and use the CLI.
|
| 118 |
+
|
| 119 |
+
- **[How it works](explanation/how-it-works.md)** - Understand the design
|
| 120 |
+
|
| 121 |
+
The pipeline, cross-lingual matching, and why binarized vectors still match.
|
| 122 |
+
|
| 123 |
+
- **[API reference](reference/api.md)** - Library details
|
| 124 |
+
|
| 125 |
+
Generated reference for `create()`, the data model, options, and similarity functions.
|
| 126 |
+
|
| 127 |
+
- **[For Coding Agents](reference/for-coding-agents.md)** - Build on iscc-sct
|
| 128 |
+
|
| 129 |
+
A dense architecture map, constraints, and task recipes for AI coding agents.
|
| 130 |
+
|
| 131 |
+
- **[Live Demo :lucide-external-link:](https://huggingface.co/spaces/iscc/iscc-sct)** - Try it in
|
| 132 |
+
the browser
|
| 133 |
+
|
| 134 |
+
Generate and compare Semantic Text-Codes interactively on Hugging Face Spaces.
|
| 135 |
+
|
| 136 |
+
- **[Source Code :lucide-external-link:](https://github.com/iscc/iscc-sct)** - Read the
|
| 137 |
+
implementation
|
| 138 |
+
|
| 139 |
+
The full algorithm, CLI, and Gradio demo on GitHub.
|
| 140 |
+
|
| 141 |
+
- **[Full text for LLMs :lucide-external-link:](llms-full.txt)** - Machine-readable docs
|
| 142 |
+
|
| 143 |
+
Every page concatenated into a single file for language models.
|
| 144 |
+
|
| 145 |
+
</div>
|
| 146 |
+
|
| 147 |
+
??? note "Supported languages (60+)"
|
| 148 |
+
|
| 149 |
+
Arabic, Armenian, Bengali, Bosnian, Bulgarian, Burmese, Catalan, Chinese (China), Chinese (Taiwan),
|
| 150 |
+
Croatian, Czech, Danish, Dutch, English, Estonian, Farsi, Finnish, French, French (Canada),
|
| 151 |
+
Galician, German, Greek, Gujarati, Hebrew, Hindi, Hungarian, Icelandic, Indonesian, Italian,
|
| 152 |
+
Japanese, Kannada, Korean, Kurdish, Latvian, Lithuanian, Macedonian, Malay, Malayalam, Marathi,
|
| 153 |
+
Mongolian, Norwegian Bokmål, Persian, Polish, Portuguese, Portuguese (Brazil), Romanian, Russian,
|
| 154 |
+
Serbian, Sinhala, Slovak, Slovenian, Spanish, Swedish, Tamil, Telugu, Thai, Turkish, Ukrainian,
|
| 155 |
+
Urdu, Vietnamese.
|
| 156 |
+
|
| 157 |
+
[Source code on GitHub :lucide-external-link:](https://github.com/iscc/iscc-sct){ .md-button }
|
docs/javascripts/copilot.js
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
/**
|
| 2 |
+
* Mount the ISCC-AI copilot chat widget with anonymous authentication.
|
| 3 |
+
*
|
| 4 |
+
* Shared across all ISCC documentation sites. Do not modify per project.
|
| 5 |
+
*
|
| 6 |
+
* Fetches a JWT token from the copilot-token endpoint, then mounts the
|
| 7 |
+
* Chainlit copilot widget with the token for cross-origin authentication.
|
| 8 |
+
*/
|
| 9 |
+
window.addEventListener("load", async function () {
|
| 10 |
+
if (typeof window.mountChainlitWidget !== "function") return;
|
| 11 |
+
|
| 12 |
+
var server = "https://iscc.ai";
|
| 13 |
+
var tokenUrl = server + "/api/copilot-token";
|
| 14 |
+
|
| 15 |
+
try {
|
| 16 |
+
var response = await fetch(tokenUrl);
|
| 17 |
+
var data = await response.json();
|
| 18 |
+
window.mountChainlitWidget({
|
| 19 |
+
chainlitServer: server,
|
| 20 |
+
theme: "light",
|
| 21 |
+
accessToken: data.accessToken,
|
| 22 |
+
customCssUrl: window.location.origin + "/stylesheets/copilot.css?v=7",
|
| 23 |
+
});
|
| 24 |
+
} catch (e) {
|
| 25 |
+
console.warn("ISCC-AI copilot: failed to fetch token", e);
|
| 26 |
+
}
|
| 27 |
+
});
|
docs/javascripts/copypage.js
ADDED
|
@@ -0,0 +1,200 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
/**
|
| 2 |
+
* "Copy page" split-button dropdown for documentation pages.
|
| 3 |
+
*
|
| 4 |
+
* Clicking the button copies the page as Markdown. Clicking the chevron
|
| 5 |
+
* opens a dropdown with additional actions: view as Markdown and edit on GitHub.
|
| 6 |
+
*
|
| 7 |
+
* Relies on per-page .md files generated by scripts/gen_llms_full.py.
|
| 8 |
+
*/
|
| 9 |
+
(function () {
|
| 10 |
+
"use strict";
|
| 11 |
+
|
| 12 |
+
var REPO_URL = "https://github.com/iscc/iscc-sct";
|
| 13 |
+
var EDIT_BRANCH = "main";
|
| 14 |
+
|
| 15 |
+
// Lucide icons (inline SVG)
|
| 16 |
+
var ICONS = {
|
| 17 |
+
copy:
|
| 18 |
+
'<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect width="14" height="14" x="8" y="8" rx="2" ry="2"/><path d="M4 16c-1.1 0-2-.9-2-2V4c0-1.1.9-2 2-2h10c1.1 0 2 .9 2 2"/></svg>',
|
| 19 |
+
fileText:
|
| 20 |
+
'<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M15 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V7Z"/><path d="M14 2v4a2 2 0 0 0 2 2h4"/><path d="M10 9H8"/><path d="M16 13H8"/><path d="M16 17H8"/></svg>',
|
| 21 |
+
pencil:
|
| 22 |
+
'<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M21.174 6.812a1 1 0 0 0-3.986-3.987L3.842 16.174a2 2 0 0 0-.5.83l-1.321 4.352a.5.5 0 0 0 .623.622l4.353-1.32a2 2 0 0 0 .83-.497z"/><path d="m15 5 4 4"/></svg>',
|
| 23 |
+
chevronDown:
|
| 24 |
+
'<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="m6 9 6 6 6-6"/></svg>',
|
| 25 |
+
check:
|
| 26 |
+
'<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M20 6 9 17l-5-5"/></svg>',
|
| 27 |
+
};
|
| 28 |
+
|
| 29 |
+
/** Derive the docs-relative path from the current page URL. */
|
| 30 |
+
function getDocPath() {
|
| 31 |
+
var path = window.location.pathname;
|
| 32 |
+
if (path.endsWith("/")) path = path.slice(0, -1);
|
| 33 |
+
return path ? path.slice(1) + ".md" : "index.md";
|
| 34 |
+
}
|
| 35 |
+
|
| 36 |
+
/** URL to the per-page .md file (generated by gen_llms_full.py). */
|
| 37 |
+
function getMarkdownUrl() {
|
| 38 |
+
return "/" + getDocPath();
|
| 39 |
+
}
|
| 40 |
+
|
| 41 |
+
/** GitHub edit URL for the current page source. */
|
| 42 |
+
function getEditUrl() {
|
| 43 |
+
return REPO_URL + "/edit/" + EDIT_BRANCH + "/docs/" + getDocPath();
|
| 44 |
+
}
|
| 45 |
+
|
| 46 |
+
/** Create a dropdown menu item button. */
|
| 47 |
+
function createItem(icon, title, desc, onClick) {
|
| 48 |
+
var btn = document.createElement("button");
|
| 49 |
+
btn.className = "copy-page__item";
|
| 50 |
+
btn.type = "button";
|
| 51 |
+
btn.innerHTML =
|
| 52 |
+
'<span class="copy-page__item-icon">' +
|
| 53 |
+
icon +
|
| 54 |
+
"</span>" +
|
| 55 |
+
'<span class="copy-page__item-text">' +
|
| 56 |
+
'<span class="copy-page__item-title">' +
|
| 57 |
+
title +
|
| 58 |
+
"</span>" +
|
| 59 |
+
'<span class="copy-page__item-desc">' +
|
| 60 |
+
desc +
|
| 61 |
+
"</span>" +
|
| 62 |
+
"</span>";
|
| 63 |
+
btn.addEventListener("click", function () {
|
| 64 |
+
onClick(btn);
|
| 65 |
+
});
|
| 66 |
+
return btn;
|
| 67 |
+
}
|
| 68 |
+
|
| 69 |
+
/** Copy the page Markdown to clipboard with visual feedback on the action button. */
|
| 70 |
+
function copyPage(el, actionBtn) {
|
| 71 |
+
el.classList.remove("copy-page--open");
|
| 72 |
+
var origHtml = actionBtn.innerHTML;
|
| 73 |
+
fetch(getMarkdownUrl())
|
| 74 |
+
.then(function (r) {
|
| 75 |
+
if (!r.ok) throw new Error(r.status);
|
| 76 |
+
return r.text();
|
| 77 |
+
})
|
| 78 |
+
.then(function (text) {
|
| 79 |
+
return navigator.clipboard.writeText(text);
|
| 80 |
+
})
|
| 81 |
+
.then(function () {
|
| 82 |
+
actionBtn.innerHTML = ICONS.check + "<span>Copied!</span>";
|
| 83 |
+
setTimeout(function () {
|
| 84 |
+
actionBtn.innerHTML = origHtml;
|
| 85 |
+
}, 2000);
|
| 86 |
+
})
|
| 87 |
+
.catch(function (err) {
|
| 88 |
+
console.error("Copy page failed:", err);
|
| 89 |
+
});
|
| 90 |
+
}
|
| 91 |
+
|
| 92 |
+
function init() {
|
| 93 |
+
var article = document.querySelector(".md-content__inner");
|
| 94 |
+
if (!article) return;
|
| 95 |
+
var h1 = article.querySelector("h1");
|
| 96 |
+
if (!h1) return;
|
| 97 |
+
|
| 98 |
+
// Container
|
| 99 |
+
var el = document.createElement("div");
|
| 100 |
+
el.className = "copy-page";
|
| 101 |
+
|
| 102 |
+
// Split button wrapper
|
| 103 |
+
var split = document.createElement("div");
|
| 104 |
+
split.className = "copy-page__split";
|
| 105 |
+
|
| 106 |
+
// Left: direct copy action
|
| 107 |
+
var actionBtn = document.createElement("button");
|
| 108 |
+
actionBtn.className = "copy-page__action";
|
| 109 |
+
actionBtn.type = "button";
|
| 110 |
+
actionBtn.title = "Copy page as Markdown for LLMs";
|
| 111 |
+
actionBtn.innerHTML = ICONS.copy + "<span>Copy page</span>";
|
| 112 |
+
actionBtn.addEventListener("click", function () {
|
| 113 |
+
copyPage(el, actionBtn);
|
| 114 |
+
});
|
| 115 |
+
|
| 116 |
+
// Right: chevron toggle for dropdown
|
| 117 |
+
var toggleBtn = document.createElement("button");
|
| 118 |
+
toggleBtn.className = "copy-page__toggle";
|
| 119 |
+
toggleBtn.type = "button";
|
| 120 |
+
toggleBtn.title = "More actions";
|
| 121 |
+
toggleBtn.innerHTML = ICONS.chevronDown;
|
| 122 |
+
toggleBtn.addEventListener("click", function () {
|
| 123 |
+
el.classList.toggle("copy-page--open");
|
| 124 |
+
});
|
| 125 |
+
|
| 126 |
+
split.appendChild(actionBtn);
|
| 127 |
+
split.appendChild(toggleBtn);
|
| 128 |
+
|
| 129 |
+
// Dropdown menu
|
| 130 |
+
var menu = document.createElement("div");
|
| 131 |
+
menu.className = "copy-page__menu";
|
| 132 |
+
|
| 133 |
+
// Item 1: Copy page as Markdown
|
| 134 |
+
menu.appendChild(
|
| 135 |
+
createItem(
|
| 136 |
+
ICONS.copy,
|
| 137 |
+
"Copy page",
|
| 138 |
+
"Copy page as Markdown for LLMs",
|
| 139 |
+
function () {
|
| 140 |
+
copyPage(el, actionBtn);
|
| 141 |
+
},
|
| 142 |
+
),
|
| 143 |
+
);
|
| 144 |
+
|
| 145 |
+
// Item 2: View as Markdown
|
| 146 |
+
menu.appendChild(
|
| 147 |
+
createItem(
|
| 148 |
+
ICONS.fileText,
|
| 149 |
+
"View as Markdown",
|
| 150 |
+
"View this page as plain text",
|
| 151 |
+
function () {
|
| 152 |
+
el.classList.remove("copy-page--open");
|
| 153 |
+
window.open(getMarkdownUrl(), "_blank");
|
| 154 |
+
},
|
| 155 |
+
),
|
| 156 |
+
);
|
| 157 |
+
|
| 158 |
+
// Item 2: Edit on GitHub
|
| 159 |
+
menu.appendChild(
|
| 160 |
+
createItem(
|
| 161 |
+
ICONS.pencil,
|
| 162 |
+
"Edit on GitHub",
|
| 163 |
+
"Edit this page on GitHub",
|
| 164 |
+
function () {
|
| 165 |
+
el.classList.remove("copy-page--open");
|
| 166 |
+
window.open(getEditUrl(), "_blank");
|
| 167 |
+
},
|
| 168 |
+
),
|
| 169 |
+
);
|
| 170 |
+
|
| 171 |
+
el.appendChild(split);
|
| 172 |
+
el.appendChild(menu);
|
| 173 |
+
|
| 174 |
+
// Clicks inside the widget stay contained
|
| 175 |
+
el.addEventListener("click", function (e) {
|
| 176 |
+
e.stopPropagation();
|
| 177 |
+
});
|
| 178 |
+
|
| 179 |
+
// Wrap h1 and button in a flex container for alignment
|
| 180 |
+
var wrapper = document.createElement("div");
|
| 181 |
+
wrapper.className = "copy-page-heading";
|
| 182 |
+
h1.parentNode.insertBefore(wrapper, h1);
|
| 183 |
+
wrapper.appendChild(h1);
|
| 184 |
+
wrapper.appendChild(el);
|
| 185 |
+
|
| 186 |
+
// Close dropdown on outside click or Escape
|
| 187 |
+
document.addEventListener("click", function () {
|
| 188 |
+
el.classList.remove("copy-page--open");
|
| 189 |
+
});
|
| 190 |
+
document.addEventListener("keydown", function (e) {
|
| 191 |
+
if (e.key === "Escape") el.classList.remove("copy-page--open");
|
| 192 |
+
});
|
| 193 |
+
}
|
| 194 |
+
|
| 195 |
+
if (document.readyState === "loading") {
|
| 196 |
+
document.addEventListener("DOMContentLoaded", init);
|
| 197 |
+
} else {
|
| 198 |
+
init();
|
| 199 |
+
}
|
| 200 |
+
})();
|
docs/llms.txt
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# iscc-sct
|
| 2 |
+
|
| 3 |
+
> Cross-lingual, similarity-preserving semantic ISCC code for text content.
|
| 4 |
+
|
| 5 |
+
iscc-sct generates the ISCC Semantic Text-Code: a compact binary code derived from multilingual
|
| 6 |
+
text embeddings. Semantically similar texts - including translations - produce codes with low
|
| 7 |
+
hamming distance, enabling cross-lingual similarity detection and near-duplicate matching.
|
| 8 |
+
|
| 9 |
+
This is an experimental proof of concept and not part of the ISO 24138:2024 standard.
|
| 10 |
+
|
| 11 |
+
Install: `pip install "iscc-sct[cpu]"`
|
| 12 |
+
|
| 13 |
+
- [Source code](https://github.com/iscc/iscc-sct)
|
| 14 |
+
- [PyPI](https://pypi.org/project/iscc-sct/)
|
| 15 |
+
- [Full documentation](https://sct.iscc.codes/llms-full.txt)
|
| 16 |
+
|
| 17 |
+
## Reference
|
| 18 |
+
|
| 19 |
+
- [For Coding Agents](https://sct.iscc.codes/reference/for-coding-agents.md): Dense architecture
|
| 20 |
+
map, constraints, and task recipes for AI coding agents working on this codebase.
|
docs/overrides/main.html
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{% extends "base.html" %}
|
| 2 |
+
|
| 3 |
+
{% block htmltitle %}
|
| 4 |
+
{% if page.title == config.site_name %}
|
| 5 |
+
<title>{{ config.site_name }} - Cross-Lingual Semantic Text-Code</title>
|
| 6 |
+
{% else %}
|
| 7 |
+
<title>{{ page.title }} - {{ config.site_name }}</title>
|
| 8 |
+
{% endif %}
|
| 9 |
+
{% endblock %}
|
| 10 |
+
|
| 11 |
+
{% block extrahead %}
|
| 12 |
+
<meta property="og:type" content="website">
|
| 13 |
+
<meta property="og:site_name" content="{{ config.site_name }}">
|
| 14 |
+
{% if page.title == config.site_name %}
|
| 15 |
+
<meta property="og:title" content="{{ config.site_name }} - Cross-Lingual Semantic Text-Code">
|
| 16 |
+
{% else %}
|
| 17 |
+
<meta property="og:title" content="{{ page.title }} - {{ config.site_name }}">
|
| 18 |
+
{% endif %}
|
| 19 |
+
<meta property="og:description" content="{% if page.meta and page.meta.description %}{{ page.meta.description }}{% else %}{{ config.site_description }}{% endif %}">
|
| 20 |
+
<meta property="og:url" content="{{ page.canonical_url }}">
|
| 21 |
+
<meta name="twitter:card" content="summary">
|
| 22 |
+
{% if page.title == config.site_name %}
|
| 23 |
+
<meta name="twitter:title" content="{{ config.site_name }} - Cross-Lingual Semantic Text-Code">
|
| 24 |
+
{% else %}
|
| 25 |
+
<meta name="twitter:title" content="{{ page.title }} - {{ config.site_name }}">
|
| 26 |
+
{% endif %}
|
| 27 |
+
<meta name="twitter:description" content="{% if page.meta and page.meta.description %}{{ page.meta.description }}{% else %}{{ config.site_description }}{% endif %}">
|
| 28 |
+
{% endblock %}
|
docs/reference/api.md
ADDED
|
@@ -0,0 +1,112 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
---
|
| 2 |
+
icon: lucide/code
|
| 3 |
+
description: Auto-generated reference for the public iscc-sct API - code generation, data model, options, and similarity functions.
|
| 4 |
+
---
|
| 5 |
+
|
| 6 |
+
# API reference
|
| 7 |
+
|
| 8 |
+
This page documents the public API re-exported from the `iscc_sct` package. Import it with
|
| 9 |
+
`import iscc_sct as sct`. The reference below is generated from the source docstrings.
|
| 10 |
+
|
| 11 |
+
## High-level API
|
| 12 |
+
|
| 13 |
+
The primary entry point. Returns a `Metadata` object with the code and, optionally, granular
|
| 14 |
+
features.
|
| 15 |
+
|
| 16 |
+
::: iscc_sct.create
|
| 17 |
+
options:
|
| 18 |
+
heading_level: 3
|
| 19 |
+
|
| 20 |
+
## Core functions
|
| 21 |
+
|
| 22 |
+
Lower-level building blocks of the processing pipeline.
|
| 23 |
+
|
| 24 |
+
::: iscc_sct.gen_text_code_semantic
|
| 25 |
+
options:
|
| 26 |
+
heading_level: 3
|
| 27 |
+
|
| 28 |
+
::: iscc_sct.code_text_semantic
|
| 29 |
+
options:
|
| 30 |
+
heading_level: 3
|
| 31 |
+
|
| 32 |
+
::: iscc_sct.soft_hash_text_semantic
|
| 33 |
+
options:
|
| 34 |
+
heading_level: 3
|
| 35 |
+
|
| 36 |
+
::: iscc_sct.embed_chunks
|
| 37 |
+
options:
|
| 38 |
+
heading_level: 3
|
| 39 |
+
|
| 40 |
+
## Data model
|
| 41 |
+
|
| 42 |
+
The result schema. `Metadata` holds the code and feature sets; `FeatureSet` and `Feature` carry the
|
| 43 |
+
granular data in either Index-Format or Object-Format.
|
| 44 |
+
|
| 45 |
+
::: iscc_sct.Metadata
|
| 46 |
+
options:
|
| 47 |
+
heading_level: 3
|
| 48 |
+
|
| 49 |
+
::: iscc_sct.FeatureSet
|
| 50 |
+
options:
|
| 51 |
+
heading_level: 3
|
| 52 |
+
|
| 53 |
+
::: iscc_sct.Feature
|
| 54 |
+
options:
|
| 55 |
+
heading_level: 3
|
| 56 |
+
|
| 57 |
+
## Options
|
| 58 |
+
|
| 59 |
+
The settings model. Configure it per call, with `ISCC_SCT_*` environment variables, or via
|
| 60 |
+
`override()`.
|
| 61 |
+
|
| 62 |
+
::: iscc_sct.SctOptions
|
| 63 |
+
options:
|
| 64 |
+
heading_level: 3
|
| 65 |
+
|
| 66 |
+
## Similarity and distance
|
| 67 |
+
|
| 68 |
+
Functions for comparing codes, digests, and granular features.
|
| 69 |
+
|
| 70 |
+
::: iscc_sct.iscc_distance
|
| 71 |
+
options:
|
| 72 |
+
heading_level: 3
|
| 73 |
+
|
| 74 |
+
::: iscc_sct.hamming_distance
|
| 75 |
+
options:
|
| 76 |
+
heading_level: 3
|
| 77 |
+
|
| 78 |
+
::: iscc_sct.cosine_similarity
|
| 79 |
+
options:
|
| 80 |
+
heading_level: 3
|
| 81 |
+
|
| 82 |
+
::: iscc_sct.granular_similarity
|
| 83 |
+
options:
|
| 84 |
+
heading_level: 3
|
| 85 |
+
|
| 86 |
+
## Codecs and helpers
|
| 87 |
+
|
| 88 |
+
Encoders, decoders, and utilities.
|
| 89 |
+
|
| 90 |
+
::: iscc_sct.encode_base32
|
| 91 |
+
options:
|
| 92 |
+
heading_level: 3
|
| 93 |
+
|
| 94 |
+
::: iscc_sct.decode_base32
|
| 95 |
+
options:
|
| 96 |
+
heading_level: 3
|
| 97 |
+
|
| 98 |
+
::: iscc_sct.encode_base64
|
| 99 |
+
options:
|
| 100 |
+
heading_level: 3
|
| 101 |
+
|
| 102 |
+
::: iscc_sct.decode_base64
|
| 103 |
+
options:
|
| 104 |
+
heading_level: 3
|
| 105 |
+
|
| 106 |
+
::: iscc_sct.char_to_byte_offsets
|
| 107 |
+
options:
|
| 108 |
+
heading_level: 3
|
| 109 |
+
|
| 110 |
+
::: iscc_sct.get_model
|
| 111 |
+
options:
|
| 112 |
+
heading_level: 3
|
docs/reference/for-coding-agents.md
ADDED
|
@@ -0,0 +1,283 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
---
|
| 2 |
+
icon: lucide/bot
|
| 3 |
+
description: Dense, prescriptive reference for AI coding agents working on or integrating iscc-sct - architecture map, constraints, side effects, task recipes, change playbook, and common mistakes.
|
| 4 |
+
---
|
| 5 |
+
|
| 6 |
+
# For Coding Agents
|
| 7 |
+
|
| 8 |
+
A compressed reference for AI agents working on **iscc-sct** (ISCC Semantic Text-Code). Read this
|
| 9 |
+
before editing the codebase or integrating the library. Terminology matches the source exactly.
|
| 10 |
+
|
| 11 |
+
`iscc-sct` turns text into a cross-lingual, similarity-preserving binary ISCC-UNIT (MainType
|
| 12 |
+
SEMANTIC, SubType TEXT). It is an experimental proof of concept, **not** part of ISO 24138:2024.
|
| 13 |
+
|
| 14 |
+
## Architecture map
|
| 15 |
+
|
| 16 |
+
### File layout
|
| 17 |
+
|
| 18 |
+
| Path | Contains |
|
| 19 |
+
| ------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
| 20 |
+
| `iscc_sct/main.py` | `create()` - high-level API; returns a `Metadata` object in Object-Format. |
|
| 21 |
+
| `iscc_sct/code_semantic_text.py` | Core algorithm. `gen_text_code_semantic()` (returns a plain dict, Index-Format), splitting, embedding, pooling, binarization. Module-level `@cache` singletons: `tokenizer()`, `chunking_tokenizer()`, `splitter()`, `splitter_guarded()`, `model()`. |
|
| 22 |
+
| `iscc_sct/models.py` | Pydantic schema: `Feature`, `FeatureSet`, `Metadata` + format converters. |
|
| 23 |
+
| `iscc_sct/options.py` | `SctOptions` (pydantic-settings), `sct_opts` singleton, `.override()`. |
|
| 24 |
+
| `iscc_sct/utils.py` | Codecs (base32/base64url), distances, model download + blake3 integrity, `char_to_byte_offsets`, `MODEL_DIR`/`MODEL_PATH` (override dir via `ISCC_SCT_MODEL_DIR`), `timer`. |
|
| 25 |
+
| `iscc_sct/cli.py` | `iscc-sct` console entry point (`main()`): glob, charset detection, `gui` subcommand. |
|
| 26 |
+
| `iscc_sct/demo.py`, `iscc_sct/app.py` | Gradio demo (Hugging Face Space). Omitted from coverage. |
|
| 27 |
+
| `iscc_sct/dev.py` | Dev-only poe task helpers. Omitted from coverage. |
|
| 28 |
+
| `iscc_sct/tokenizer.json` | Vendored tokenizer. Byte-exact; excluded from whitespace/EOL hooks. |
|
| 29 |
+
| `tests/chunking_vectors.json` | Frozen chunk-boundary test vectors. Never hand-edit (see Change playbook). |
|
| 30 |
+
|
| 31 |
+
### Pipeline
|
| 32 |
+
|
| 33 |
+
```text
|
| 34 |
+
text -> split_text() -> embed_chunks() -> mean_pooling() -> binarize() -> ISCC header + base32
|
| 35 |
+
```
|
| 36 |
+
|
| 37 |
+
`gen_text_code_semantic(text, **options)` orchestrates this:
|
| 38 |
+
|
| 39 |
+
1. Reject empty text (`ValueError`), then `sct_opts.override(options)`.
|
| 40 |
+
1. `split_text()` -> `[(offset, chunk), ...]` at semantic boundaries (max 127 tokens, 48 overlap).
|
| 41 |
+
1. `embed_chunks()` tokenizes in batches (auto: 1 on CPU, 100 on CUDA), runs the ONNX model,
|
| 42 |
+
`attention_pooling()` per chunk.
|
| 43 |
+
1. `mean_pooling()` averages chunk vectors into one L2-normalized document vector.
|
| 44 |
+
1. `binarize()` (`vec >= 0` -> bits), truncate to `bits // 8`, prefix the 2-byte ISCC header,
|
| 45 |
+
`encode_base32()`, prepend `"ISCC:"`.
|
| 46 |
+
|
| 47 |
+
### Import flow
|
| 48 |
+
|
| 49 |
+
```text
|
| 50 |
+
iscc_sct/__init__.py (star-imports, defines __version__)
|
| 51 |
+
-> options -> utils -> code_semantic_text -> models -> main
|
| 52 |
+
main -> models, code_semantic_text, options
|
| 53 |
+
code_semantic_text -> onnxruntime (lazy import), semantic_text_splitter, tokenizers, numpy,
|
| 54 |
+
iscc_sct as sct (uses sct.* at call time, not import time)
|
| 55 |
+
utils -> models
|
| 56 |
+
cli -> main, charset_normalizer
|
| 57 |
+
```
|
| 58 |
+
|
| 59 |
+
### Public API
|
| 60 |
+
|
| 61 |
+
`iscc_sct/__init__.py` re-exports every module's `__all__`. The public surface
|
| 62 |
+
(`import iscc_sct as sct`):
|
| 63 |
+
|
| 64 |
+
| Symbol | Source | Purpose |
|
| 65 |
+
| ------------------------------------------------------------------------------- | ------------------ | ------------------------------------------------ |
|
| 66 |
+
| `create(text, granular=False, **options)` | main | Primary API. Returns `Metadata` (Object-Format). |
|
| 67 |
+
| `gen_text_code_semantic(text, **options)` | code_semantic_text | Low-level. Returns `dict` (Index-Format). |
|
| 68 |
+
| `code_text_semantic(fp, **options)` | code_semantic_text | Same, reading a UTF-8 file path. |
|
| 69 |
+
| `soft_hash_text_semantic(text)` | code_semantic_text | Raw 384-bit digest (`bytes`), no header. |
|
| 70 |
+
| `embed_chunks(chunks, batch_size=None)` | code_semantic_text | Chunk list -> embedding array. |
|
| 71 |
+
| `Metadata`, `FeatureSet`, `Feature` | models | Result schema + converters. |
|
| 72 |
+
| `SctOptions`, `sct_opts` | options | Settings model + global instance. |
|
| 73 |
+
| `iscc_distance`, `hamming_distance`, `cosine_similarity`, `granular_similarity` | utils | Similarity metrics. |
|
| 74 |
+
| `encode_base32`, `decode_base32`, `encode_base64`, `decode_base64` | utils | Codecs. |
|
| 75 |
+
| `char_to_byte_offsets`, `get_model`, `MODEL_PATH`, `timer` | utils | Helpers. |
|
| 76 |
+
| `__version__` | `__init__` | Package version string. |
|
| 77 |
+
|
| 78 |
+
## Decision dispatch
|
| 79 |
+
|
| 80 |
+
### Which entry point?
|
| 81 |
+
|
| 82 |
+
| Goal | Use |
|
| 83 |
+
| -------------------------------------------------------------- | ------------------------------------------------------- |
|
| 84 |
+
| One ISCC code from a string, ready-to-use object | `create(text)` -> `Metadata` (Object-Format) |
|
| 85 |
+
| Granular per-chunk features (simprints/offsets/sizes/contents) | `create(text, granular=True)` |
|
| 86 |
+
| Compact parallel-array result for storage/indexing | `gen_text_code_semantic(text, ...)` (Index-Format dict) |
|
| 87 |
+
| Process a text file from disk | `code_text_semantic(path)` |
|
| 88 |
+
| Just the raw 384-bit vector digest | `soft_hash_text_semantic(text)` -> `bytes` |
|
| 89 |
+
| Command line / batch over files | `iscc-sct <glob>` |
|
| 90 |
+
|
| 91 |
+
### Which similarity metric?
|
| 92 |
+
|
| 93 |
+
| Inputs | Use | Returns |
|
| 94 |
+
| ------------------------------------- | ----------------------------------------- | --------------------------------------- |
|
| 95 |
+
| Two full ISCC code strings | `iscc_distance(iscc1, iscc2)` | Hamming distance in bits (`int`) |
|
| 96 |
+
| Two raw digests of equal length | `hamming_distance(a, b)` | bit distance |
|
| 97 |
+
| Two raw digests, normalized score | `cosine_similarity(a, b)` | `int` in `[-100, 100]` |
|
| 98 |
+
| Two `Metadata` objects with simprints | `granular_similarity(a, b, threshold=80)` | `[(Feature, similarity, Feature), ...]` |
|
| 99 |
+
|
| 100 |
+
### Which feature format?
|
| 101 |
+
|
| 102 |
+
| Format | Produced by | Shape | Convert with |
|
| 103 |
+
| ------------- | -------------------------- | -------------------------------------------------------- | ----------------------------- |
|
| 104 |
+
| Index-Format | `gen_text_code_semantic()` | parallel arrays `simprints`/`offsets`/`sizes`/`contents` | `Metadata.to_index_format()` |
|
| 105 |
+
| Object-Format | `create()` | list of self-contained `Feature` objects | `Metadata.to_object_format()` |
|
| 106 |
+
|
| 107 |
+
## Constraints and invariants
|
| 108 |
+
|
| 109 |
+
- **No base ONNX runtime.** The base package declares no `onnxruntime`. Install exactly one of the
|
| 110 |
+
mutually exclusive `cpu` / `gpu` extras. `onnxruntime` and `onnxruntime-gpu` unpack into the
|
| 111 |
+
same directory and clobber each other (issue #23); `tool.uv.conflicts` enforces exclusivity for
|
| 112 |
+
uv.
|
| 113 |
+
- **ONNX runtime is imported lazily.** `import iscc_sct`, the `iscc-sct` CLI, and `iscc-sct doctor`
|
| 114 |
+
load without any runtime installed. `load_onnxruntime()` (called from `model()` on first code
|
| 115 |
+
generation) raises a guarded `ImportError` with install instructions - keep that guard intact.
|
| 116 |
+
- **Codes are base32; simprints are base64url.** Never cross the codecs.
|
| 117 |
+
- **ISCC header is fixed:** MainType `SEMANTIC` (`0001`), SubType `TEXT` (`0000`), Version `0000`,
|
| 118 |
+
plus a length nibble from `BIT_LEN_MAP`. 2 bytes, big-endian. `iscc_distance` strips the 5-char
|
| 119 |
+
`ISCC:` prefix, base32-decodes, drops the 2-byte header, then compares bodies of equal length.
|
| 120 |
+
- **`bits` constraints:** `32 <= bits <= 256`, `multiple_of=32`. Same for `bits_granular`.
|
| 121 |
+
- **Default bit-length differs by entry point:** `SctOptions.bits` / `create()` default to **64**;
|
| 122 |
+
the **CLI** `--bits` defaults to **256**.
|
| 123 |
+
- **`SctOptions` validates on assignment.** Mutate only via `.override(dict)`, which deep-copies and
|
| 124 |
+
sets fields individually so validators run. Options flow as `**opts.model_dump()` through all
|
| 125 |
+
layers. Env vars use the `ISCC_SCT_` prefix; `.env` is loaded.
|
| 126 |
+
- **Chunk boundaries are frozen** by `tests/chunking_vectors.json` and identical on both the normal
|
| 127 |
+
(`splitter`) and guarded (`splitter_guarded`) paths. `needs_split_guard()` routes texts whose
|
| 128 |
+
positions sit more than `SPLIT_GUARD_GAP` (8192) chars from the next paragraph separator to the
|
| 129 |
+
guarded sizer (PDF-extracted text, issue #24).
|
| 130 |
+
- **Singletons:** `tokenizer()`, `chunking_tokenizer()`, `splitter(**opts)`,
|
| 131 |
+
`splitter_guarded(**opts)`, `model()` are `@cache`d. The splitter cache keys on the option
|
| 132 |
+
kwargs, so all option values must be hashable.
|
| 133 |
+
- **Two tokenizers:** `tokenizer()` keeps the vendored truncation (128) + padding and embeds chunks;
|
| 134 |
+
`chunking_tokenizer()` disables truncation/padding and sizes chunks. Truncation on the sizer
|
| 135 |
+
makes `tokenizers` >=0.23 emit overflow encodings, the root cause of the issue #24 super-linear
|
| 136 |
+
chunking. Both yield identical boundaries, so don't merge them.
|
| 137 |
+
- **Coverage must stay at 100%** (`--cov-fail-under=100`). GPU branches and model download use
|
| 138 |
+
`# pragma: no cover`. `dev.py`, `demo.py`, and `tests/` are omitted from coverage.
|
| 139 |
+
- **Style:** PEP 484 **type comments** (first line after `def`), PEP 585 generics, PEP 604 unions.
|
| 140 |
+
Short pure functions, max 3 args, no nested functions, sphinx-style docstrings. Ruff line length
|
| 141 |
+
100, LF endings, rule F401 disabled.
|
| 142 |
+
|
| 143 |
+
## Side effects catalog
|
| 144 |
+
|
| 145 |
+
| Function | Effect |
|
| 146 |
+
| ------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
| 147 |
+
| `model()` (first call) | If the model file is absent/corrupt, `get_model()` **downloads ~450 MB** to `MODEL_DIR` (platformdirs user-data dir, or `ISCC_SCT_MODEL_DIR`) and verifies a blake3 checksum. Creates a cached ONNX `InferenceSession`. |
|
| 148 |
+
| `get_model()` | Network download + disk write to `MODEL_PATH`; blake3 integrity check (`# pragma: no cover`). |
|
| 149 |
+
| `tokenizer()`, `chunking_tokenizer()`, `splitter()`, `splitter_guarded()` | Populate process-global `@cache` (loaded once). |
|
| 150 |
+
| `model()` | Calls `rt.preload_dlls()` when CUDA is available; logs a warning via `warn_gpu_shadowed()` if `onnxruntime-gpu` is installed but CUDA is missing. |
|
| 151 |
+
| `import iscc_sct.options` | `load_dotenv()` reads `.env`; instantiates `sct_opts`. |
|
| 152 |
+
| `import iscc_sct.utils` | Resolves `MODEL_DIR` from `ISCC_SCT_MODEL_DIR` (else user data dir); `os.makedirs(MODEL_DIR)` (idempotent). |
|
| 153 |
+
| `create()`, `gen_text_code_semantic()` | Pure given a loaded model: no disk writes, no mutation of inputs. |
|
| 154 |
+
| CLI `main()` | Reads files, prints to stdout, removes the loguru logger unless `--debug`. |
|
| 155 |
+
|
| 156 |
+
## Task recipes
|
| 157 |
+
|
| 158 |
+
### Generate a code (library)
|
| 159 |
+
|
| 160 |
+
```python
|
| 161 |
+
import iscc_sct as sct
|
| 162 |
+
|
| 163 |
+
meta = sct.create("This is some sample text.", bits=256)
|
| 164 |
+
print(meta.iscc) # "ISCC:CAD..."
|
| 165 |
+
print(meta.characters) # input length in characters
|
| 166 |
+
```
|
| 167 |
+
|
| 168 |
+
### Granular per-chunk features
|
| 169 |
+
|
| 170 |
+
```python
|
| 171 |
+
import iscc_sct as sct
|
| 172 |
+
|
| 173 |
+
meta = sct.create(long_text, bits=256, granular=True)
|
| 174 |
+
for feature in meta.features[0].simprints: # Object-Format
|
| 175 |
+
print(feature.offset, feature.size, feature.simprint, feature.content[:40])
|
| 176 |
+
```
|
| 177 |
+
|
| 178 |
+
### Compare two texts (cross-lingual)
|
| 179 |
+
|
| 180 |
+
```python
|
| 181 |
+
import iscc_sct as sct
|
| 182 |
+
|
| 183 |
+
a = sct.create("An ISCC applies to a specific digital asset...")
|
| 184 |
+
b = sct.create("Ein ISCC bezieht sich auf ein bestimmtes digitales Gut...")
|
| 185 |
+
print(sct.iscc_distance(a.iscc, b.iscc)) # low bit distance => similar
|
| 186 |
+
```
|
| 187 |
+
|
| 188 |
+
### Match granular simprints
|
| 189 |
+
|
| 190 |
+
```python
|
| 191 |
+
import iscc_sct as sct
|
| 192 |
+
|
| 193 |
+
a = sct.create(doc_a, granular=True)
|
| 194 |
+
b = sct.create(doc_b, granular=True)
|
| 195 |
+
for feat_a, similarity, feat_b in sct.granular_similarity(a, b, threshold=80):
|
| 196 |
+
print(similarity, feat_a.offset, feat_b.offset)
|
| 197 |
+
```
|
| 198 |
+
|
| 199 |
+
### Configure via options
|
| 200 |
+
|
| 201 |
+
```python
|
| 202 |
+
import iscc_sct as sct
|
| 203 |
+
|
| 204 |
+
# Per-call override (validated copy of the global settings):
|
| 205 |
+
meta = sct.create(text, bits=128, contents=True)
|
| 206 |
+
|
| 207 |
+
# Or globally via environment: ISCC_SCT_BITS=128, ISCC_SCT_MAX_TOKENS=127, ...
|
| 208 |
+
```
|
| 209 |
+
|
| 210 |
+
## Change playbook
|
| 211 |
+
|
| 212 |
+
| If you change... | Also update... |
|
| 213 |
+
| ----------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
| 214 |
+
| The embedding model file | `MODEL_FILENAME`, `MODEL_URL`, `MODEL_CHECKSUM` in `utils.py`; verify codes are bit-stable or bump version + mark BREAKING in CHANGELOG. |
|
| 215 |
+
| Chunking (`max_tokens`, `overlap`, `split_text`, `needs_split_guard`, token sizing) | Regenerate `tests/chunking_vectors.json` via `uv run python tests/test_chunking_vectors.py` (only for an intentional algorithm change); multi-chunk codes shift. |
|
| 216 |
+
| A public function name/signature | The module's `__all__` (it is star-exported through `__init__.py`); README/docs examples. |
|
| 217 |
+
| Add a processing option | Add a field to `SctOptions` (env var name, constraints); it flows via `model_dump()`. Wire into `create()` if it is a granular toggle; add to the README config table. |
|
| 218 |
+
| `Metadata` / `FeatureSet` / `Feature` schema | Keep `to_index_format()`, `to_object_format()`, `get_content()`, `get_overlaps()` consistent. |
|
| 219 |
+
| ISCC header constants (`MAINTYPE`/`SUBTYPE`/`SCT_VERSION`/`BIT_LEN_MAP`) | Codes change - this is BREAKING; bump version and document. |
|
| 220 |
+
| onnxruntime import / provider logic | Keep the `cpu`/`gpu` extras, `tool.uv.conflicts`, the `ImportError` guard, and `warn_gpu_shadowed()` in sync. |
|
| 221 |
+
| Any code path | Add tests to keep coverage at 100%; use `# pragma: no cover` only for GPU/download branches. Run `uv run poe all`. |
|
| 222 |
+
| Dependencies | Regenerate `requirements.txt` via `uv run poe export-requirements` (never hand-edit it). |
|
| 223 |
+
|
| 224 |
+
## Common mistakes
|
| 225 |
+
|
| 226 |
+
**NEVER** add `onnxruntime` as a base dependency to "make install easier."
|
| 227 |
+
|
| 228 |
+
```toml
|
| 229 |
+
# WRONG - re-breaks issue #23: [gpu] then installs both wheels and CPU clobbers GPU
|
| 230 |
+
dependencies = ["onnxruntime"]
|
| 231 |
+
```
|
| 232 |
+
|
| 233 |
+
**ALWAYS** keep it behind the mutually exclusive `cpu` / `gpu` extras.
|
| 234 |
+
|
| 235 |
+
---
|
| 236 |
+
|
| 237 |
+
**NEVER** use inline type annotations.
|
| 238 |
+
|
| 239 |
+
```python
|
| 240 |
+
def binarize(vec: NDArray) -> bytes: ... # WRONG for this codebase
|
| 241 |
+
```
|
| 242 |
+
|
| 243 |
+
**ALWAYS** use PEP 484 type comments as the first line after `def`.
|
| 244 |
+
|
| 245 |
+
```python
|
| 246 |
+
def binarize(vec):
|
| 247 |
+
# type: (NDArray) -> bytes
|
| 248 |
+
...
|
| 249 |
+
```
|
| 250 |
+
|
| 251 |
+
---
|
| 252 |
+
|
| 253 |
+
**NEVER** expect Object-Format from the low-level function.
|
| 254 |
+
|
| 255 |
+
```python
|
| 256 |
+
meta = sct.gen_text_code_semantic(text) # returns a dict in INDEX-Format
|
| 257 |
+
meta.features[0].simprints # WRONG - it is a dict, not Metadata
|
| 258 |
+
```
|
| 259 |
+
|
| 260 |
+
**ALWAYS** use `create()` for an Object-Format `Metadata`, or wrap the dict: `Metadata(**data)`.
|
| 261 |
+
|
| 262 |
+
---
|
| 263 |
+
|
| 264 |
+
**NEVER** mutate `SctOptions` fields directly or assume CLI/library defaults match.
|
| 265 |
+
|
| 266 |
+
```python
|
| 267 |
+
sct.sct_opts.bits = 256 # avoid - mutates the global; use .override()
|
| 268 |
+
sct.create(text) # library default bits=64, NOT the CLI's 256
|
| 269 |
+
```
|
| 270 |
+
|
| 271 |
+
**ALWAYS** pass overrides per call (`create(text, bits=256)`) or via `sct_opts.override({...})`.
|
| 272 |
+
|
| 273 |
+
---
|
| 274 |
+
|
| 275 |
+
**NEVER** hand-edit `tests/chunking_vectors.json`, `requirements.txt`, `iscc_sct/tokenizer.json`, or
|
| 276 |
+
the model file. Each is generated or byte-exact.
|
| 277 |
+
|
| 278 |
+
**ALWAYS** regenerate via the documented command and run `uv run poe all` before reporting done.
|
| 279 |
+
|
| 280 |
+
---
|
| 281 |
+
|
| 282 |
+
**NEVER** mix codecs: ISCC codes are RFC4648 base32 (no padding); granular simprints are base64url
|
| 283 |
+
(no padding). Decode with the matching `decode_base32` / `decode_base64`.
|
docs/robots.txt
ADDED
|
@@ -0,0 +1,2 @@
|
|
|
|
|
|
|
|
|
|
| 1 |
+
User-agent: *
|
| 2 |
+
Allow: /
|
docs/stylesheets/copilot.css
ADDED
|
@@ -0,0 +1,119 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
/* ISCC Copilot Widget Theme for Documentation Sites
|
| 2 |
+
*
|
| 3 |
+
* Shared across all ISCC documentation sites. Do not modify per project.
|
| 4 |
+
*
|
| 5 |
+
* Styles the Chainlit copilot chat widget to match the ISCC brand.
|
| 6 |
+
* Injected into the widget Shadow DOM via customCssUrl.
|
| 7 |
+
*
|
| 8 |
+
* Zensical sets html { font-size: 125% } (20px) which inflates all rem-based
|
| 9 |
+
* sizes inside the Chainlit widget by 25%. Instead of blanket zoom (which
|
| 10 |
+
* over-corrects body text), we pin Tailwind text utilities to their intended
|
| 11 |
+
* pixel values and zoom only the self-contained toggle button.
|
| 12 |
+
*/
|
| 13 |
+
|
| 14 |
+
/* Toggle button: self-contained element, zoom works cleanly here */
|
| 15 |
+
#chainlit-copilot-button {
|
| 16 |
+
zoom: 0.8;
|
| 17 |
+
}
|
| 18 |
+
|
| 19 |
+
/* --- Fix Tailwind text utilities inflated by 125% root font-size ---
|
| 20 |
+
* Tailwind rem values resolve against html 20px instead of 16px.
|
| 21 |
+
* Pin each utility to its intended pixel size. */
|
| 22 |
+
.text-xs { font-size: 12px !important; line-height: 16px !important; }
|
| 23 |
+
.text-sm { font-size: 14px !important; line-height: 20px !important; }
|
| 24 |
+
.text-base { font-size: 16px !important; line-height: 24px !important; }
|
| 25 |
+
.text-lg { font-size: 18px !important; line-height: 28px !important; }
|
| 26 |
+
.text-xl { font-size: 20px !important; line-height: 28px !important; }
|
| 27 |
+
.text-2xl { font-size: 24px !important; line-height: 32px !important; }
|
| 28 |
+
|
| 29 |
+
/* --- Widen the chat panel --- */
|
| 30 |
+
.copilot-container-collapsed {
|
| 31 |
+
min-width: 420px !important;
|
| 32 |
+
}
|
| 33 |
+
|
| 34 |
+
/* Fix inflated rem-based spacing inside the chat panel.
|
| 35 |
+
* Scale factor: 0.8rem per intended 1rem (16/20 = 0.8). */
|
| 36 |
+
[data-radix-popper-content-wrapper] {
|
| 37 |
+
font-size: 16px;
|
| 38 |
+
}
|
| 39 |
+
|
| 40 |
+
/* Ensure chat message body text renders at readable size */
|
| 41 |
+
#chainlit-copilot-chat p,
|
| 42 |
+
#chainlit-copilot-chat li,
|
| 43 |
+
#chainlit-copilot-chat span:not([class]),
|
| 44 |
+
#chainlit-copilot-chat div:not([class]) > span {
|
| 45 |
+
font-size: 15px !important;
|
| 46 |
+
line-height: 1.2 !important;
|
| 47 |
+
}
|
| 48 |
+
|
| 49 |
+
/* Force light theme variables - ISCC brand on white background.
|
| 50 |
+
* Target both .light and .dark to override regardless of server theme. */
|
| 51 |
+
:root,
|
| 52 |
+
:host,
|
| 53 |
+
.light,
|
| 54 |
+
.dark {
|
| 55 |
+
--background: 0 0% 100% !important;
|
| 56 |
+
--foreground: 213 69% 23% !important;
|
| 57 |
+
--primary: 356 89% 67% !important;
|
| 58 |
+
--primary-foreground: 0 0% 100% !important;
|
| 59 |
+
--secondary: 210 30% 95% !important;
|
| 60 |
+
--secondary-foreground: 213 69% 23% !important;
|
| 61 |
+
--accent: 210 30% 95% !important;
|
| 62 |
+
--accent-foreground: 213 69% 23% !important;
|
| 63 |
+
--muted: 210 20% 96% !important;
|
| 64 |
+
--muted-foreground: 213 40% 40% !important;
|
| 65 |
+
--card: 0 0% 100% !important;
|
| 66 |
+
--card-foreground: 213 69% 23% !important;
|
| 67 |
+
--popover: 0 0% 100% !important;
|
| 68 |
+
--popover-foreground: 213 69% 23% !important;
|
| 69 |
+
--border: 210 20% 85% !important;
|
| 70 |
+
--input: 0 0% 100% !important;
|
| 71 |
+
--ring: 211 100% 35% !important;
|
| 72 |
+
}
|
| 73 |
+
|
| 74 |
+
/* Floating toggle button - ISCC Coral */
|
| 75 |
+
button.bg-primary.rounded-full,
|
| 76 |
+
button[class*="bg-primary"][class*="rounded-full"] {
|
| 77 |
+
background-color: #f56169 !important;
|
| 78 |
+
background: #f56169 !important;
|
| 79 |
+
}
|
| 80 |
+
|
| 81 |
+
button.bg-primary.rounded-full:hover,
|
| 82 |
+
button[class*="bg-primary"][class*="rounded-full"]:hover {
|
| 83 |
+
background-color: #e04550 !important;
|
| 84 |
+
background: #e04550 !important;
|
| 85 |
+
}
|
| 86 |
+
|
| 87 |
+
/* Send button and primary actions - ISCC Blue */
|
| 88 |
+
button.bg-primary:not(.rounded-full),
|
| 89 |
+
button[class*="bg-primary"]:not([class*="rounded-full"]),
|
| 90 |
+
.bg-primary:not(.rounded-full) {
|
| 91 |
+
background-color: #0054b2 !important;
|
| 92 |
+
background: #0054b2 !important;
|
| 93 |
+
}
|
| 94 |
+
|
| 95 |
+
button.bg-primary:not(.rounded-full):hover,
|
| 96 |
+
button[class*="bg-primary"]:not([class*="rounded-full"]):hover,
|
| 97 |
+
.bg-primary:not(.rounded-full):hover {
|
| 98 |
+
background-color: #123663 !important;
|
| 99 |
+
background: #123663 !important;
|
| 100 |
+
}
|
| 101 |
+
|
| 102 |
+
/* Links - ISCC Blue */
|
| 103 |
+
a {
|
| 104 |
+
color: #0054b2 !important;
|
| 105 |
+
}
|
| 106 |
+
|
| 107 |
+
/* Code blocks - ISCC navy */
|
| 108 |
+
pre,
|
| 109 |
+
pre code,
|
| 110 |
+
.hljs {
|
| 111 |
+
background-color: #0d2847 !important;
|
| 112 |
+
color: #e6edf5 !important;
|
| 113 |
+
}
|
| 114 |
+
|
| 115 |
+
/* Inline code */
|
| 116 |
+
code:not(pre code):not(.hljs) {
|
| 117 |
+
background-color: #e8ebef !important;
|
| 118 |
+
color: inherit !important;
|
| 119 |
+
}
|
docs/stylesheets/extra.css
ADDED
|
@@ -0,0 +1,289 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
/* ISCC Foundation Brand Colors
|
| 2 |
+
*
|
| 3 |
+
* Shared across all ISCC documentation sites. Do not modify per project.
|
| 4 |
+
*
|
| 5 |
+
* Override palette attribute selectors that Zensical sets by default.
|
| 6 |
+
* The [data-md-color-*] selectors match the specificity of the built-in
|
| 7 |
+
* palette CSS so our custom colors take precedence.
|
| 8 |
+
*/
|
| 9 |
+
|
| 10 |
+
/* Light mode: override primary (indigo -> ISCC Blue) */
|
| 11 |
+
[data-md-color-primary="indigo"] {
|
| 12 |
+
--md-primary-fg-color: #0054b2;
|
| 13 |
+
--md-primary-fg-color--light: #4596f5;
|
| 14 |
+
--md-primary-fg-color--dark: #123663;
|
| 15 |
+
--md-primary-bg-color: #ffffff;
|
| 16 |
+
--md-primary-bg-color--light: #ffffffb3;
|
| 17 |
+
}
|
| 18 |
+
|
| 19 |
+
/* Light mode: override accent (indigo -> ISCC Blue) */
|
| 20 |
+
[data-md-color-accent="indigo"] {
|
| 21 |
+
--md-accent-fg-color: #0054b2;
|
| 22 |
+
--md-accent-fg-color--transparent: #0054b21a;
|
| 23 |
+
--md-accent-bg-color: #ffffff;
|
| 24 |
+
--md-accent-bg-color--light: #ffffffb3;
|
| 25 |
+
}
|
| 26 |
+
|
| 27 |
+
/* Link color for light mode */
|
| 28 |
+
[data-md-color-scheme="default"] {
|
| 29 |
+
--md-typeset-a-color: #0054b2;
|
| 30 |
+
}
|
| 31 |
+
|
| 32 |
+
/* Dark mode: override primary */
|
| 33 |
+
[data-md-color-scheme="slate"][data-md-color-primary="indigo"] {
|
| 34 |
+
--md-primary-fg-color: #4596f5;
|
| 35 |
+
--md-primary-fg-color--light: #7ac2f7;
|
| 36 |
+
--md-primary-fg-color--dark: #0054b2;
|
| 37 |
+
--md-primary-bg-color: #123663;
|
| 38 |
+
--md-primary-bg-color--light: #12366380;
|
| 39 |
+
--md-typeset-a-color: #4596f5;
|
| 40 |
+
}
|
| 41 |
+
|
| 42 |
+
/* Dark mode: override accent */
|
| 43 |
+
[data-md-color-scheme="slate"][data-md-color-accent="indigo"] {
|
| 44 |
+
--md-accent-fg-color: #4596f5;
|
| 45 |
+
--md-accent-fg-color--transparent: #4596f51a;
|
| 46 |
+
}
|
| 47 |
+
|
| 48 |
+
/* Dark mode: invert logo so the black logo becomes white */
|
| 49 |
+
[data-md-color-scheme="slate"] .md-header__button.md-logo img,
|
| 50 |
+
[data-md-color-scheme="slate"] .md-nav__button.md-logo img {
|
| 51 |
+
filter: invert(1);
|
| 52 |
+
}
|
| 53 |
+
|
| 54 |
+
/* ---------- Light mode: ISCC Blue header ---------- */
|
| 55 |
+
[data-md-color-scheme="default"] .md-header {
|
| 56 |
+
background-color: #0054b2;
|
| 57 |
+
color: #ffffff;
|
| 58 |
+
}
|
| 59 |
+
|
| 60 |
+
[data-md-color-scheme="default"] .md-header .md-header__button,
|
| 61 |
+
[data-md-color-scheme="default"] .md-header .md-header__topic,
|
| 62 |
+
[data-md-color-scheme="default"] .md-header .md-header__title {
|
| 63 |
+
color: #ffffff;
|
| 64 |
+
}
|
| 65 |
+
|
| 66 |
+
[data-md-color-scheme="default"] .md-header .md-search__input::placeholder {
|
| 67 |
+
color: rgba(255, 255, 255, 0.7);
|
| 68 |
+
}
|
| 69 |
+
|
| 70 |
+
/* Search button: white text/icon on blue header */
|
| 71 |
+
[data-md-color-scheme="default"] .md-header .md-search__button {
|
| 72 |
+
color: rgba(255, 255, 255, 0.8);
|
| 73 |
+
background-color: rgba(255, 255, 255, 0.12);
|
| 74 |
+
border-color: rgba(255, 255, 255, 0.3);
|
| 75 |
+
}
|
| 76 |
+
|
| 77 |
+
/* Search icon (mask uses background-color as fill) */
|
| 78 |
+
[data-md-color-scheme="default"] .md-header .md-search__button::before {
|
| 79 |
+
background-color: rgba(255, 255, 255, 0.8);
|
| 80 |
+
}
|
| 81 |
+
|
| 82 |
+
/* Ctrl+K shortcut badge */
|
| 83 |
+
[data-md-color-scheme="default"] .md-header .md-search__button::after {
|
| 84 |
+
background-color: #123663;
|
| 85 |
+
border-color: rgba(255, 255, 255, 0.3);
|
| 86 |
+
color: rgba(255, 255, 255, 0.8);
|
| 87 |
+
}
|
| 88 |
+
|
| 89 |
+
/* Invert logo in light mode header so it appears white on blue */
|
| 90 |
+
[data-md-color-scheme="default"] .md-header__button.md-logo img {
|
| 91 |
+
filter: invert(1);
|
| 92 |
+
}
|
| 93 |
+
|
| 94 |
+
/* ---------- Dark mode: Deep Navy header ---------- */
|
| 95 |
+
[data-md-color-scheme="slate"] .md-header {
|
| 96 |
+
background-color: #123663;
|
| 97 |
+
color: rgba(255, 255, 255, 0.9);
|
| 98 |
+
}
|
| 99 |
+
|
| 100 |
+
[data-md-color-scheme="slate"] .md-header .md-header__button,
|
| 101 |
+
[data-md-color-scheme="slate"] .md-header .md-header__topic,
|
| 102 |
+
[data-md-color-scheme="slate"] .md-header .md-header__title {
|
| 103 |
+
color: rgba(255, 255, 255, 0.9);
|
| 104 |
+
}
|
| 105 |
+
|
| 106 |
+
/* ---------- Footer: Deep Navy ---------- */
|
| 107 |
+
[data-md-color-scheme="default"] .md-footer {
|
| 108 |
+
background-color: #123663;
|
| 109 |
+
color: rgba(255, 255, 255, 0.9);
|
| 110 |
+
}
|
| 111 |
+
|
| 112 |
+
[data-md-color-scheme="default"] .md-footer a {
|
| 113 |
+
color: rgba(255, 255, 255, 0.9);
|
| 114 |
+
}
|
| 115 |
+
|
| 116 |
+
[data-md-color-scheme="default"] .md-footer-meta {
|
| 117 |
+
background-color: rgba(0, 0, 0, 0.15);
|
| 118 |
+
}
|
| 119 |
+
|
| 120 |
+
[data-md-color-scheme="default"] .md-copyright,
|
| 121 |
+
[data-md-color-scheme="default"] .md-copyright__highlight {
|
| 122 |
+
color: rgba(255, 255, 255, 0.7);
|
| 123 |
+
}
|
| 124 |
+
|
| 125 |
+
/* Match theme specificity: html .md-footer-meta.md-typeset a */
|
| 126 |
+
html [data-md-color-scheme="default"] .md-footer-meta.md-typeset a {
|
| 127 |
+
color: rgba(255, 255, 255, 0.7);
|
| 128 |
+
}
|
| 129 |
+
|
| 130 |
+
/* Mermaid: softer edge color in dark mode */
|
| 131 |
+
[data-md-color-scheme="slate"] .mermaid {
|
| 132 |
+
--md-mermaid-edge-color: #6e7681;
|
| 133 |
+
}
|
| 134 |
+
|
| 135 |
+
/* ---------- Copy page split-button dropdown ---------- */
|
| 136 |
+
|
| 137 |
+
/* Flex wrapper pairs the h1 and the button on one line */
|
| 138 |
+
.copy-page-heading {
|
| 139 |
+
display: flex;
|
| 140 |
+
align-items: flex-start;
|
| 141 |
+
gap: 0.8rem;
|
| 142 |
+
}
|
| 143 |
+
|
| 144 |
+
.copy-page-heading > h1 {
|
| 145 |
+
flex: 1;
|
| 146 |
+
min-width: 0;
|
| 147 |
+
}
|
| 148 |
+
|
| 149 |
+
.copy-page {
|
| 150 |
+
flex-shrink: 0;
|
| 151 |
+
position: relative;
|
| 152 |
+
margin-top: 0.35rem;
|
| 153 |
+
z-index: 2;
|
| 154 |
+
}
|
| 155 |
+
|
| 156 |
+
/* Split button: shared border wraps both halves */
|
| 157 |
+
.copy-page__split {
|
| 158 |
+
display: inline-flex;
|
| 159 |
+
border: 1px solid var(--md-default-fg-color--lighter);
|
| 160 |
+
border-radius: 0.3rem;
|
| 161 |
+
overflow: hidden;
|
| 162 |
+
transition: border-color 0.2s;
|
| 163 |
+
}
|
| 164 |
+
|
| 165 |
+
.copy-page__split:hover {
|
| 166 |
+
border-color: var(--md-default-fg-color--light);
|
| 167 |
+
}
|
| 168 |
+
|
| 169 |
+
/* Left half: direct copy action */
|
| 170 |
+
.copy-page__action {
|
| 171 |
+
display: inline-flex;
|
| 172 |
+
align-items: center;
|
| 173 |
+
gap: 0.3rem;
|
| 174 |
+
padding: 0.3rem 0.5rem;
|
| 175 |
+
border: none;
|
| 176 |
+
background: var(--md-default-bg-color);
|
| 177 |
+
color: var(--md-default-fg-color--light);
|
| 178 |
+
font-family: inherit;
|
| 179 |
+
font-size: 0.7rem;
|
| 180 |
+
line-height: 1.4;
|
| 181 |
+
cursor: pointer;
|
| 182 |
+
white-space: nowrap;
|
| 183 |
+
transition: background-color 0.15s, color 0.15s;
|
| 184 |
+
}
|
| 185 |
+
|
| 186 |
+
.copy-page__action:hover {
|
| 187 |
+
color: var(--md-default-fg-color);
|
| 188 |
+
background: var(--md-default-fg-color--lightest);
|
| 189 |
+
}
|
| 190 |
+
|
| 191 |
+
.copy-page__action svg {
|
| 192 |
+
flex-shrink: 0;
|
| 193 |
+
}
|
| 194 |
+
|
| 195 |
+
/* Right half: chevron dropdown toggle */
|
| 196 |
+
.copy-page__toggle {
|
| 197 |
+
display: inline-flex;
|
| 198 |
+
align-items: center;
|
| 199 |
+
padding: 0.3rem 0.3rem;
|
| 200 |
+
border: none;
|
| 201 |
+
border-left: 1px solid var(--md-default-fg-color--lighter);
|
| 202 |
+
background: var(--md-default-bg-color);
|
| 203 |
+
color: var(--md-default-fg-color--light);
|
| 204 |
+
cursor: pointer;
|
| 205 |
+
transition: background-color 0.15s, color 0.15s;
|
| 206 |
+
}
|
| 207 |
+
|
| 208 |
+
.copy-page__toggle:hover {
|
| 209 |
+
color: var(--md-default-fg-color);
|
| 210 |
+
background: var(--md-default-fg-color--lightest);
|
| 211 |
+
}
|
| 212 |
+
|
| 213 |
+
/* Dropdown menu */
|
| 214 |
+
.copy-page__menu {
|
| 215 |
+
display: none;
|
| 216 |
+
position: absolute;
|
| 217 |
+
top: calc(100% + 0.3rem);
|
| 218 |
+
right: 0;
|
| 219 |
+
min-width: 15rem;
|
| 220 |
+
background: var(--md-default-bg-color);
|
| 221 |
+
border: 1px solid var(--md-default-fg-color--lightest);
|
| 222 |
+
border-radius: 0.5rem;
|
| 223 |
+
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.1);
|
| 224 |
+
overflow: hidden;
|
| 225 |
+
z-index: 10;
|
| 226 |
+
}
|
| 227 |
+
|
| 228 |
+
.copy-page--open .copy-page__menu {
|
| 229 |
+
display: block;
|
| 230 |
+
}
|
| 231 |
+
|
| 232 |
+
/* Menu items */
|
| 233 |
+
.copy-page__item {
|
| 234 |
+
display: flex;
|
| 235 |
+
align-items: flex-start;
|
| 236 |
+
gap: 0.55rem;
|
| 237 |
+
width: 100%;
|
| 238 |
+
padding: 0.55rem 0.7rem;
|
| 239 |
+
border: none;
|
| 240 |
+
background: none;
|
| 241 |
+
color: var(--md-default-fg-color);
|
| 242 |
+
font-family: inherit;
|
| 243 |
+
font-size: 0.7rem;
|
| 244 |
+
line-height: 1.4;
|
| 245 |
+
cursor: pointer;
|
| 246 |
+
text-align: left;
|
| 247 |
+
transition: background-color 0.15s;
|
| 248 |
+
}
|
| 249 |
+
|
| 250 |
+
.copy-page__item:hover {
|
| 251 |
+
background: var(--md-default-fg-color--lightest);
|
| 252 |
+
}
|
| 253 |
+
|
| 254 |
+
.copy-page__item + .copy-page__item {
|
| 255 |
+
border-top: 1px solid var(--md-default-fg-color--lightest);
|
| 256 |
+
}
|
| 257 |
+
|
| 258 |
+
.copy-page__item-icon {
|
| 259 |
+
flex-shrink: 0;
|
| 260 |
+
margin-top: 0.1rem;
|
| 261 |
+
color: var(--md-default-fg-color--light);
|
| 262 |
+
}
|
| 263 |
+
|
| 264 |
+
.copy-page__item-text {
|
| 265 |
+
display: flex;
|
| 266 |
+
flex-direction: column;
|
| 267 |
+
gap: 0.05rem;
|
| 268 |
+
}
|
| 269 |
+
|
| 270 |
+
.copy-page__item-title {
|
| 271 |
+
font-weight: 600;
|
| 272 |
+
}
|
| 273 |
+
|
| 274 |
+
.copy-page__item-desc {
|
| 275 |
+
color: var(--md-default-fg-color--light);
|
| 276 |
+
font-size: 0.6rem;
|
| 277 |
+
}
|
| 278 |
+
|
| 279 |
+
/* Dark mode: stronger shadow */
|
| 280 |
+
[data-md-color-scheme="slate"] .copy-page__menu {
|
| 281 |
+
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.3);
|
| 282 |
+
}
|
| 283 |
+
|
| 284 |
+
/* Hide on small screens */
|
| 285 |
+
@media screen and (max-width: 44.9375em) {
|
| 286 |
+
.copy-page {
|
| 287 |
+
display: none;
|
| 288 |
+
}
|
| 289 |
+
}
|
docs/tutorials/getting-started.md
ADDED
|
@@ -0,0 +1,138 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
---
|
| 2 |
+
icon: lucide/rocket
|
| 3 |
+
description: Install iscc-sct, generate your first Semantic Text-Code, and match a translation across languages.
|
| 4 |
+
---
|
| 5 |
+
|
| 6 |
+
# Getting started
|
| 7 |
+
|
| 8 |
+
Install `iscc-sct`, generate your first Semantic Text-Code, and watch two languages produce
|
| 9 |
+
near-identical codes.
|
| 10 |
+
|
| 11 |
+
## Prerequisites
|
| 12 |
+
|
| 13 |
+
- Python 3.10 or later
|
| 14 |
+
- [`uv`](https://docs.astral.sh/uv/) (recommended) or `pip`
|
| 15 |
+
|
| 16 |
+
## Install
|
| 17 |
+
|
| 18 |
+
`iscc-sct` needs an ONNX runtime, selected through an install extra. The `cpu` extra works
|
| 19 |
+
everywhere:
|
| 20 |
+
|
| 21 |
+
=== "uv"
|
| 22 |
+
|
| 23 |
+
```bash
|
| 24 |
+
uv add "iscc-sct[cpu]"
|
| 25 |
+
```
|
| 26 |
+
|
| 27 |
+
=== "pip"
|
| 28 |
+
|
| 29 |
+
```bash
|
| 30 |
+
pip install "iscc-sct[cpu]"
|
| 31 |
+
```
|
| 32 |
+
|
| 33 |
+
!!! warning "Choose exactly one runtime"
|
| 34 |
+
|
| 35 |
+
Install the `cpu` extra, or the `gpu` extra for NVIDIA CUDA acceleration — never both. The
|
| 36 |
+
`onnxruntime` and `onnxruntime-gpu` packages unpack into the same directory and overwrite each
|
| 37 |
+
other. A plain `pip install iscc-sct` installs no runtime, and the first code generation then fails
|
| 38 |
+
with install instructions. If you are unsure which extra fits your machine, run `iscc-sct doctor`
|
| 39 |
+
(see the [command-line guide](../howto/command-line.md)).
|
| 40 |
+
|
| 41 |
+
### Verify the installation
|
| 42 |
+
|
| 43 |
+
```python
|
| 44 |
+
import iscc_sct
|
| 45 |
+
|
| 46 |
+
print(iscc_sct.__version__)
|
| 47 |
+
```
|
| 48 |
+
|
| 49 |
+
## Generate your first code
|
| 50 |
+
|
| 51 |
+
Pass any text to `create()`. The first call downloads the embedding model (about 450 MB) to your
|
| 52 |
+
user data directory; later calls reuse it.
|
| 53 |
+
|
| 54 |
+
```python
|
| 55 |
+
import iscc_sct as sct
|
| 56 |
+
|
| 57 |
+
text = "This is some sample text. It can be a longer document or even an entire book."
|
| 58 |
+
print(sct.create(text, bits=256).iscc)
|
| 59 |
+
# ISCC:CADV3GG6JH3XEVRNSVYGCLJ7AAV3BOT5J7EHEZKPFXEGRJ2CTWACGZI
|
| 60 |
+
```
|
| 61 |
+
|
| 62 |
+
The `bits` argument sets the code length. Longer codes carry more detail; 64 bits is the library
|
| 63 |
+
default, and 256 bits is the most precise. See [comparing texts](../howto/compare-texts.md) for how
|
| 64 |
+
length affects matching.
|
| 65 |
+
|
| 66 |
+
## Match a translation
|
| 67 |
+
|
| 68 |
+
The point of a Semantic Text-Code is that meaning survives translation. Generate codes for the same
|
| 69 |
+
passage in English and German, then measure their distance:
|
| 70 |
+
|
| 71 |
+
```python
|
| 72 |
+
import iscc_sct as sct
|
| 73 |
+
|
| 74 |
+
english = (
|
| 75 |
+
"An ISCC applies to a specific digital asset and is a data-descriptor deterministically "
|
| 76 |
+
"constructed from multiple hash digests using the algorithms and rules in this document. "
|
| 77 |
+
"This document does not provide information on registration of ISCCs."
|
| 78 |
+
)
|
| 79 |
+
german = (
|
| 80 |
+
"Ein ISCC bezieht sich auf ein bestimmtes digitales Gut und ist ein Daten-Deskriptor, der "
|
| 81 |
+
"deterministisch aus mehreren Hash-Digests unter Verwendung der Algorithmen und Regeln in "
|
| 82 |
+
"diesem Dokument erstellt wird. Dieses Dokument enthält keine Informationen über die "
|
| 83 |
+
"Registrierung von ISCCs."
|
| 84 |
+
)
|
| 85 |
+
|
| 86 |
+
a = sct.create(english)
|
| 87 |
+
b = sct.create(german)
|
| 88 |
+
|
| 89 |
+
print(sct.iscc_distance(a.iscc, b.iscc))
|
| 90 |
+
# 3
|
| 91 |
+
```
|
| 92 |
+
|
| 93 |
+
A distance of `3` bits out of 64 means the translation is a near-match. For contrast, an unrelated
|
| 94 |
+
sentence lands far away:
|
| 95 |
+
|
| 96 |
+
```python
|
| 97 |
+
import iscc_sct as sct
|
| 98 |
+
|
| 99 |
+
unrelated = (
|
| 100 |
+
"The recipe calls for two cups of flour, a pinch of salt, and three ripe bananas mashed "
|
| 101 |
+
"until smooth before folding the mixture into the buttered baking tin."
|
| 102 |
+
)
|
| 103 |
+
|
| 104 |
+
print(sct.iscc_distance(a.iscc, sct.create(unrelated).iscc))
|
| 105 |
+
# 33
|
| 106 |
+
```
|
| 107 |
+
|
| 108 |
+
Low distance means similar meaning; high distance means unrelated content. Translation matching is
|
| 109 |
+
the behavior that lexical (word-based) codes cannot provide.
|
| 110 |
+
|
| 111 |
+
## Look inside the text
|
| 112 |
+
|
| 113 |
+
Set `granular=True` to get per-chunk features alongside the document code. Each chunk carries its
|
| 114 |
+
own offset, size, similarity-preserving fingerprint (simprint), and text:
|
| 115 |
+
|
| 116 |
+
```python
|
| 117 |
+
import iscc_sct as sct
|
| 118 |
+
|
| 119 |
+
text = "This is some sample text. It can be a longer document or even an entire book."
|
| 120 |
+
meta = sct.create(text, bits=256, granular=True)
|
| 121 |
+
|
| 122 |
+
feature = meta.features[0].simprints[0]
|
| 123 |
+
print(feature.offset, feature.size, feature.simprint)
|
| 124 |
+
# 0 77 XZjeSfdyVi0
|
| 125 |
+
```
|
| 126 |
+
|
| 127 |
+
Granular features let you match individual passages across documents, even when the surrounding text
|
| 128 |
+
differs. The [granular features guide](../howto/granular-features.md) covers chunk-level matching
|
| 129 |
+
and content reconstruction.
|
| 130 |
+
|
| 131 |
+
## Next steps
|
| 132 |
+
|
| 133 |
+
- **[Compare texts](../howto/compare-texts.md)** — Measure semantic and cross-lingual similarity.
|
| 134 |
+
- **[Granular features](../howto/granular-features.md)** — Work with per-chunk simprints and
|
| 135 |
+
offsets.
|
| 136 |
+
- **[Configuration](../howto/configuration.md)** — Tune bit-length, chunking, and output options.
|
| 137 |
+
- **[How it works](../explanation/how-it-works.md)** — Understand why the codes match across
|
| 138 |
+
languages.
|
iscc_sct/__init__.py
CHANGED
|
@@ -1,4 +1,4 @@
|
|
| 1 |
-
__version__ = "0.
|
| 2 |
from iscc_sct.options import *
|
| 3 |
from iscc_sct.utils import *
|
| 4 |
from iscc_sct.code_semantic_text import *
|
|
|
|
| 1 |
+
__version__ = "0.2.2"
|
| 2 |
from iscc_sct.options import *
|
| 3 |
from iscc_sct.utils import *
|
| 4 |
from iscc_sct.code_semantic_text import *
|
iscc_sct/cli.py
CHANGED
|
@@ -1,5 +1,6 @@
|
|
| 1 |
import argparse
|
| 2 |
import glob
|
|
|
|
| 3 |
from pathlib import Path
|
| 4 |
from loguru import logger
|
| 5 |
from iscc_sct.main import create
|
|
@@ -7,11 +8,13 @@ from charset_normalizer import from_bytes
|
|
| 7 |
|
| 8 |
|
| 9 |
def main():
|
| 10 |
-
parser = argparse.ArgumentParser(
|
|
|
|
|
|
|
| 11 |
parser.add_argument(
|
| 12 |
"path",
|
| 13 |
type=str,
|
| 14 |
-
help="Path to text files (
|
| 15 |
nargs="?",
|
| 16 |
)
|
| 17 |
parser.add_argument(
|
|
@@ -21,6 +24,9 @@ def main():
|
|
| 21 |
"-g", "--granular", action="store_true", help="Activate granular processing."
|
| 22 |
)
|
| 23 |
parser.add_argument("-d", "--debug", action="store_true", help="Show debugging messages.")
|
|
|
|
|
|
|
|
|
|
| 24 |
args = parser.parse_args()
|
| 25 |
|
| 26 |
if args.path is None:
|
|
@@ -32,15 +38,21 @@ def main():
|
|
| 32 |
|
| 33 |
if args.path == "gui": # pragma: no cover
|
| 34 |
try:
|
| 35 |
-
from iscc_sct.demo import demo
|
| 36 |
|
| 37 |
-
demo.launch(inbrowser=True)
|
| 38 |
except ImportError:
|
| 39 |
print(
|
| 40 |
-
"Error: Gradio is not installed. Please install it with
|
|
|
|
| 41 |
)
|
| 42 |
return
|
| 43 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 44 |
for path in glob.glob(args.path):
|
| 45 |
path = Path(path)
|
| 46 |
if path.is_file():
|
|
@@ -68,4 +80,4 @@ def main():
|
|
| 68 |
|
| 69 |
|
| 70 |
if __name__ == "__main__": # pragma: no cover
|
| 71 |
-
main()
|
|
|
|
| 1 |
import argparse
|
| 2 |
import glob
|
| 3 |
+
import sys
|
| 4 |
from pathlib import Path
|
| 5 |
from loguru import logger
|
| 6 |
from iscc_sct.main import create
|
|
|
|
| 8 |
|
| 9 |
|
| 10 |
def main():
|
| 11 |
+
parser = argparse.ArgumentParser(
|
| 12 |
+
prog="iscc-sct", description="Generate Semantic Text-Codes for text files."
|
| 13 |
+
)
|
| 14 |
parser.add_argument(
|
| 15 |
"path",
|
| 16 |
type=str,
|
| 17 |
+
help="Path to text files (glob patterns), 'doctor' to check the ONNX runtime, or 'gui' for the demo.",
|
| 18 |
nargs="?",
|
| 19 |
)
|
| 20 |
parser.add_argument(
|
|
|
|
| 24 |
"-g", "--granular", action="store_true", help="Activate granular processing."
|
| 25 |
)
|
| 26 |
parser.add_argument("-d", "--debug", action="store_true", help="Show debugging messages.")
|
| 27 |
+
parser.add_argument(
|
| 28 |
+
"-y", "--yes", action="store_true", help="Auto-confirm the 'doctor' runtime install."
|
| 29 |
+
)
|
| 30 |
args = parser.parse_args()
|
| 31 |
|
| 32 |
if args.path is None:
|
|
|
|
| 38 |
|
| 39 |
if args.path == "gui": # pragma: no cover
|
| 40 |
try:
|
| 41 |
+
from iscc_sct.demo import demo, launch_kwargs
|
| 42 |
|
| 43 |
+
demo.launch(inbrowser=True, **launch_kwargs)
|
| 44 |
except ImportError:
|
| 45 |
print(
|
| 46 |
+
"Error: Gradio is not installed. Please install it with "
|
| 47 |
+
"'pip install \"iscc-sct[demo]\"' to use the GUI."
|
| 48 |
)
|
| 49 |
return
|
| 50 |
|
| 51 |
+
if args.path == "doctor":
|
| 52 |
+
from iscc_sct.doctor import run_doctor
|
| 53 |
+
|
| 54 |
+
return run_doctor(assume_yes=args.yes)
|
| 55 |
+
|
| 56 |
for path in glob.glob(args.path):
|
| 57 |
path = Path(path)
|
| 58 |
if path.is_file():
|
|
|
|
| 80 |
|
| 81 |
|
| 82 |
if __name__ == "__main__": # pragma: no cover
|
| 83 |
+
sys.exit(main())
|
iscc_sct/code_semantic_text.py
CHANGED
|
@@ -20,15 +20,16 @@ The ISCC Text-Code Semantic is a content-based compact binary code generated fro
|
|
| 20 |
"""
|
| 21 |
|
| 22 |
from loguru import logger as log
|
| 23 |
-
|
|
|
|
| 24 |
from semantic_text_splitter import TextSplitter
|
| 25 |
from tokenizers import Tokenizer
|
| 26 |
from pathlib import Path
|
| 27 |
from typing import Any
|
| 28 |
import numpy as np
|
| 29 |
-
import onnxruntime as rt
|
| 30 |
from numpy.typing import NDArray
|
| 31 |
-
from functools import cache
|
|
|
|
| 32 |
import iscc_sct as sct
|
| 33 |
|
| 34 |
|
|
@@ -59,6 +60,27 @@ MAINTYPE = "0001" # SEMANTIC
|
|
| 59 |
SUBTYPE = "0000" # TEXT
|
| 60 |
SCT_VERSION = "0000" # V0
|
| 61 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 62 |
|
| 63 |
def code_text_semantic(fp, **options):
|
| 64 |
# type: (Path|str, Any) -> dict[str, Any]
|
|
@@ -66,22 +88,24 @@ def code_text_semantic(fp, **options):
|
|
| 66 |
Generate ISCC Semantic-Code Text from a text file.
|
| 67 |
|
| 68 |
NOTE:
|
| 69 |
-
|
| 70 |
-
|
| 71 |
-
|
| 72 |
-
:param fp: File path of plaintext file to process
|
| 73 |
-
:param options: Custom processing options for overriding global options
|
| 74 |
-
|
| 75 |
-
|
| 76 |
-
|
| 77 |
-
|
| 78 |
-
|
| 79 |
-
|
| 80 |
-
|
| 81 |
-
|
| 82 |
-
|
| 83 |
-
|
| 84 |
-
|
|
|
|
|
|
|
| 85 |
:return: Dict with ISCC processing results
|
| 86 |
"""
|
| 87 |
fp = Path(fp)
|
|
@@ -93,19 +117,26 @@ def gen_text_code_semantic(text, **options):
|
|
| 93 |
"""
|
| 94 |
Create an ISCC Semantic-Code Text from plaintext.
|
| 95 |
|
| 96 |
-
:param str text:
|
| 97 |
-
:param options: Custom processing options for overriding global options
|
| 98 |
-
|
| 99 |
-
|
| 100 |
-
|
| 101 |
-
|
| 102 |
-
|
| 103 |
-
|
| 104 |
-
|
| 105 |
-
|
| 106 |
-
|
| 107 |
-
|
| 108 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 109 |
:return: Dict with ISCC processing results (using Index-Format for granular features)
|
| 110 |
"""
|
| 111 |
|
|
@@ -125,7 +156,7 @@ def gen_text_code_semantic(text, **options):
|
|
| 125 |
|
| 126 |
# Chunk embedding
|
| 127 |
with sct.timer("EMBEDDING time"):
|
| 128 |
-
embeddings = embed_chunks(chunks)
|
| 129 |
|
| 130 |
# Create global document embedding
|
| 131 |
embedding = mean_pooling(embeddings)
|
|
@@ -136,6 +167,8 @@ def gen_text_code_semantic(text, **options):
|
|
| 136 |
"subtype": "text",
|
| 137 |
"version": 0,
|
| 138 |
}
|
|
|
|
|
|
|
| 139 |
if opts.embedding:
|
| 140 |
feature_set["embedding"] = compress(embedding, opts.precision)
|
| 141 |
if opts.simprints:
|
|
@@ -144,7 +177,10 @@ def gen_text_code_semantic(text, **options):
|
|
| 144 |
if opts.offsets:
|
| 145 |
feature_set["offsets"] = offsets
|
| 146 |
if opts.sizes:
|
| 147 |
-
|
|
|
|
|
|
|
|
|
|
| 148 |
if opts.contents:
|
| 149 |
feature_set["contents"] = chunks
|
| 150 |
result["features"] = [feature_set]
|
|
@@ -160,7 +196,7 @@ def gen_text_code_semantic(text, **options):
|
|
| 160 |
|
| 161 |
def soft_hash_text_semantic(text):
|
| 162 |
# type: (str) -> bytes
|
| 163 |
-
"""
|
| 164 |
chunks = [item[1] for item in split_text(text)]
|
| 165 |
embeddings = embed_chunks(chunks)
|
| 166 |
embedding = mean_pooling(embeddings)
|
|
@@ -176,12 +212,26 @@ def split_text(text, **options):
|
|
| 176 |
:param text: Text to split.
|
| 177 |
:param options: Custom processing options for overriding global options
|
| 178 |
:key max_tokens (int): Max tokens per chunk (default 127).
|
| 179 |
-
:key overlap (int): Max tokens allowed
|
| 180 |
:key trim (int): Trim whitespace from chunks (default False).
|
| 181 |
-
:return: A list of offset, chunk tuples [(offset,chunk), ...]
|
| 182 |
"""
|
| 183 |
opts = sct.sct_opts.override(options)
|
| 184 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 185 |
|
| 186 |
|
| 187 |
@cache
|
|
@@ -190,47 +240,281 @@ def tokenizer():
|
|
| 190 |
"""
|
| 191 |
Load and cache the tokenizer model based on the predefined model name.
|
| 192 |
|
|
|
|
|
|
|
|
|
|
| 193 |
:return: An instance of the Tokenizer.
|
| 194 |
"""
|
| 195 |
with sct.timer("TOKENIZER load time"):
|
| 196 |
return Tokenizer.from_file(TOKENIZER_PATH.as_posix())
|
| 197 |
|
| 198 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 199 |
@cache
|
| 200 |
def splitter(**options):
|
| 201 |
# type: (Any) -> TextSplitter
|
| 202 |
"""
|
| 203 |
-
Load and cache the text splitter, initialized with tokenizer.
|
| 204 |
|
| 205 |
:param options: Custom processing options for overriding global options
|
| 206 |
:key max_tokens (int): Max tokens per chunk (default 127).
|
| 207 |
-
:key overlap (int): Max tokens allowed
|
| 208 |
:key trim (int): Trim whitespace from chunks (default False).
|
| 209 |
:return: An instance of TextSplitter.
|
| 210 |
"""
|
| 211 |
opts = sct.sct_opts.override(options)
|
| 212 |
with sct.timer("TEXTSPLITTER load time"):
|
| 213 |
return TextSplitter.from_huggingface_tokenizer(
|
| 214 |
-
|
| 215 |
)
|
| 216 |
|
| 217 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 218 |
@cache
|
| 219 |
def model():
|
| 220 |
-
# type: () ->
|
| 221 |
"""
|
| 222 |
Load and cache the ONNX inference model from a specified path.
|
| 223 |
|
| 224 |
-
:return: An ONNX inference session.
|
| 225 |
"""
|
|
|
|
|
|
|
|
|
|
| 226 |
available_onnx_providers = rt.get_available_providers()
|
| 227 |
log.debug(f"Available ONNX providers {', '.join(available_onnx_providers)}")
|
|
|
|
| 228 |
selected_onnx_providers = ["CPUExecutionProvider"]
|
| 229 |
if "CUDAExecutionProvider" in available_onnx_providers: # pragma: no cover
|
| 230 |
selected_onnx_providers.insert(0, "CUDAExecutionProvider")
|
|
|
|
|
|
|
|
|
|
|
|
|
| 231 |
log.debug(f"Using ONNX providers {', '.join(selected_onnx_providers)}")
|
| 232 |
-
so = rt.
|
| 233 |
-
so.graph_optimization_level = rt.GraphOptimizationLevel.ORT_ENABLE_ALL
|
| 234 |
try:
|
| 235 |
with sct.timer("ONNXMODEL load time"):
|
| 236 |
return rt.InferenceSession(
|
|
@@ -259,14 +543,19 @@ def tokenize_chunks(chunks):
|
|
| 259 |
return {"input_ids": input_ids, "attention_mask": attention_mask, "token_type_ids": type_ids}
|
| 260 |
|
| 261 |
|
| 262 |
-
def embed_chunks(chunks, batch_size=
|
|
|
|
| 263 |
"""
|
| 264 |
Embed text chunks and return vector embeddings.
|
| 265 |
|
| 266 |
:param chunks: Text chunks to embed.
|
| 267 |
-
:param batch_size: Number of chunks to process in each batch
|
|
|
|
| 268 |
:return: An array of embeddings for each chunk.
|
| 269 |
"""
|
|
|
|
|
|
|
|
|
|
| 270 |
embeddings = []
|
| 271 |
for start_idx in range(0, len(chunks), batch_size):
|
| 272 |
batch_chunks = chunks[start_idx : start_idx + batch_size]
|
|
@@ -292,7 +581,7 @@ def embed_tokens(tokens):
|
|
| 292 |
def attention_pooling(token_embeddings, attention_mask):
|
| 293 |
# type: (np.array, np.array) -> np.array
|
| 294 |
"""
|
| 295 |
-
Apply attention mask
|
| 296 |
|
| 297 |
:param token_embeddings: Raw token embeddings from the model.
|
| 298 |
:param attention_mask: Attention masks for the embeddings.
|
|
@@ -325,7 +614,7 @@ def binarize(vec):
|
|
| 325 |
Binarize an embedding vector into a hash digest.
|
| 326 |
|
| 327 |
:param vec: Vector to be binarized.
|
| 328 |
-
:return: A
|
| 329 |
"""
|
| 330 |
return bytes((np.packbits(np.array(vec) >= 0)))
|
| 331 |
|
|
@@ -337,7 +626,7 @@ def compress(vec, precision):
|
|
| 337 |
|
| 338 |
:param vec: Embedding vector.
|
| 339 |
:param precision: Max number of fractional decimal places.
|
| 340 |
-
:return: Vector as native python list of rounded floats.
|
| 341 |
"""
|
| 342 |
rounded_array = np.around(vec, decimals=precision)
|
| 343 |
compress_list = [round(x, precision) for x in rounded_array.tolist()]
|
|
|
|
| 20 |
"""
|
| 21 |
|
| 22 |
from loguru import logger as log
|
| 23 |
+
|
| 24 |
+
from importlib.metadata import PackageNotFoundError, distribution
|
| 25 |
from semantic_text_splitter import TextSplitter
|
| 26 |
from tokenizers import Tokenizer
|
| 27 |
from pathlib import Path
|
| 28 |
from typing import Any
|
| 29 |
import numpy as np
|
|
|
|
| 30 |
from numpy.typing import NDArray
|
| 31 |
+
from functools import cache, partial
|
| 32 |
+
import re
|
| 33 |
import iscc_sct as sct
|
| 34 |
|
| 35 |
|
|
|
|
| 60 |
SUBTYPE = "0000" # TEXT
|
| 61 |
SCT_VERSION = "0000" # V0
|
| 62 |
|
| 63 |
+
# Newline runs that can act as paragraph-level split boundaries (2+ newline characters)
|
| 64 |
+
NEWLINE_RUNS = re.compile(r"[\r\n]{2,}")
|
| 65 |
+
|
| 66 |
+
# A single newline character - used by needs_split_guard to find separator-free spans
|
| 67 |
+
NEWLINE = re.compile(r"[\r\n]")
|
| 68 |
+
|
| 69 |
+
# Any Unicode whitespace - used by token_count_guarded to find a tokenizer word boundary.
|
| 70 |
+
# The tokenizer's WhitespaceSplit pre-tokenizer splits on Unicode whitespace (not just ASCII),
|
| 71 |
+
# so the prefix cut must match that to short-circuit NBSP/em-space/form-feed PDF text.
|
| 72 |
+
WHITESPACE = re.compile(r"\s")
|
| 73 |
+
|
| 74 |
+
# Max distance (chars) from any position to the next paragraph-level separator before
|
| 75 |
+
# chunking switches to the guarded splitter (see needs_split_guard)
|
| 76 |
+
SPLIT_GUARD_GAP = 8192
|
| 77 |
+
|
| 78 |
+
# Chunks per inference batch chosen by resolve_batch_size when batch_size is 0 (auto).
|
| 79 |
+
# On CPU one chunk per batch is both faster and leaner; on GPU large batches keep the device
|
| 80 |
+
# saturated and the memory lives in VRAM (see resolve_batch_size).
|
| 81 |
+
CPU_BATCH_SIZE = 1
|
| 82 |
+
GPU_BATCH_SIZE = 100
|
| 83 |
+
|
| 84 |
|
| 85 |
def code_text_semantic(fp, **options):
|
| 86 |
# type: (Path|str, Any) -> dict[str, Any]
|
|
|
|
| 88 |
Generate ISCC Semantic-Code Text from a text file.
|
| 89 |
|
| 90 |
NOTE:
|
| 91 |
+
Enable granular features with `simprints=True`. Their length is set by `bits_granular`
|
| 92 |
+
(default 64) and is independent of the document `bits`.
|
| 93 |
+
|
| 94 |
+
:param fp: File path of a plaintext file to process
|
| 95 |
+
:param options: Custom processing options for overriding global options. Recognized keys:
|
| 96 |
+
|
| 97 |
+
- ``bits`` (int): Length of generated Semantic Text-Code in bits (default 64).
|
| 98 |
+
- ``characters`` (bool): Return document character count (default True).
|
| 99 |
+
- ``embedding`` (bool): Return global document embedding (default False).
|
| 100 |
+
- ``precision`` (int): Max fractional digits for embeddings (default 8).
|
| 101 |
+
- ``simprints`` (bool): Return granular document features (default False).
|
| 102 |
+
- ``offsets`` (bool): Return character offsets for granular features (default False).
|
| 103 |
+
- ``sizes`` (bool): Include sizes of granular features in chars (default False).
|
| 104 |
+
- ``contents`` (bool): Return text chunks (default False).
|
| 105 |
+
- ``max_tokens`` (int): Max tokens per chunk (default 127).
|
| 106 |
+
- ``overlap`` (int): Max tokens allowed to overlap between chunks (default 48).
|
| 107 |
+
- ``trim`` (bool): Trim whitespace from chunks (default False).
|
| 108 |
+
- ``batch_size`` (int): Chunks per inference batch (default 0 = auto).
|
| 109 |
:return: Dict with ISCC processing results
|
| 110 |
"""
|
| 111 |
fp = Path(fp)
|
|
|
|
| 117 |
"""
|
| 118 |
Create an ISCC Semantic-Code Text from plaintext.
|
| 119 |
|
| 120 |
+
:param str text: Plain text for ISCC processing
|
| 121 |
+
:param options: Custom processing options for overriding global options. Recognized keys:
|
| 122 |
+
|
| 123 |
+
- ``bits`` (int): Length of generated Semantic Text-Code in bits (default 64).
|
| 124 |
+
- ``characters`` (bool): Return document character count (default True).
|
| 125 |
+
- ``embedding`` (bool): Return global document embedding (default False).
|
| 126 |
+
- ``precision`` (int): Max fractional digits for embeddings (default 8).
|
| 127 |
+
- ``simprints`` (bool): Return granular document features (default False).
|
| 128 |
+
- ``offsets`` (bool): Return character offsets for granular features (default False).
|
| 129 |
+
- ``sizes`` (bool): Include sizes of granular features in chars (default False).
|
| 130 |
+
- ``contents`` (bool): Return text chunks (default False).
|
| 131 |
+
- ``max_tokens`` (int): Max tokens per chunk (default 127).
|
| 132 |
+
- ``overlap`` (int): Max tokens allowed overlapping between chunks (default 48).
|
| 133 |
+
- ``trim`` (bool): Trim whitespace from chunks (default False).
|
| 134 |
+
- ``batch_size`` (int): Chunks per inference batch (default 0 = auto).
|
| 135 |
+
|
| 136 |
+
NOTE:
|
| 137 |
+
`intra_op_threads` configures the shared inference session and therefore only takes
|
| 138 |
+
effect via the global options (`ISCC_SCT_INTRA_OP_THREADS`) before the session is
|
| 139 |
+
created. Passing it here has no effect.
|
| 140 |
:return: Dict with ISCC processing results (using Index-Format for granular features)
|
| 141 |
"""
|
| 142 |
|
|
|
|
| 156 |
|
| 157 |
# Chunk embedding
|
| 158 |
with sct.timer("EMBEDDING time"):
|
| 159 |
+
embeddings = embed_chunks(chunks, batch_size=opts.batch_size)
|
| 160 |
|
| 161 |
# Create global document embedding
|
| 162 |
embedding = mean_pooling(embeddings)
|
|
|
|
| 167 |
"subtype": "text",
|
| 168 |
"version": 0,
|
| 169 |
}
|
| 170 |
+
if opts.offsets or opts.sizes:
|
| 171 |
+
feature_set["byte_offsets"] = opts.byte_offsets
|
| 172 |
if opts.embedding:
|
| 173 |
feature_set["embedding"] = compress(embedding, opts.precision)
|
| 174 |
if opts.simprints:
|
|
|
|
| 177 |
if opts.offsets:
|
| 178 |
feature_set["offsets"] = offsets
|
| 179 |
if opts.sizes:
|
| 180 |
+
if opts.byte_offsets:
|
| 181 |
+
feature_set["sizes"] = [len(chunk.encode("utf-8")) for chunk in chunks]
|
| 182 |
+
else:
|
| 183 |
+
feature_set["sizes"] = [len(chunk) for chunk in chunks]
|
| 184 |
if opts.contents:
|
| 185 |
feature_set["contents"] = chunks
|
| 186 |
result["features"] = [feature_set]
|
|
|
|
| 196 |
|
| 197 |
def soft_hash_text_semantic(text):
|
| 198 |
# type: (str) -> bytes
|
| 199 |
+
"""Create a similarity-preserving hash for text as the full binarized document embedding (384 bits)."""
|
| 200 |
chunks = [item[1] for item in split_text(text)]
|
| 201 |
embeddings = embed_chunks(chunks)
|
| 202 |
embedding = mean_pooling(embeddings)
|
|
|
|
| 212 |
:param text: Text to split.
|
| 213 |
:param options: Custom processing options for overriding global options
|
| 214 |
:key max_tokens (int): Max tokens per chunk (default 127).
|
| 215 |
+
:key overlap (int): Max tokens allowed overlapping between chunks (default 48).
|
| 216 |
:key trim (int): Trim whitespace from chunks (default False).
|
| 217 |
+
:return: A list of offset, chunk tuples [(offset, chunk), ...]
|
| 218 |
"""
|
| 219 |
opts = sct.sct_opts.override(options)
|
| 220 |
+
select = splitter_guarded if needs_split_guard(text) else splitter
|
| 221 |
+
# Only the chunking options may reach the cached splitter constructors - unrelated knobs
|
| 222 |
+
# (e.g. batch_size) would fragment the cache with one TextSplitter per distinct value.
|
| 223 |
+
chunks = select(max_tokens=opts.max_tokens, overlap=opts.overlap, trim=opts.trim).chunk_indices(
|
| 224 |
+
text
|
| 225 |
+
)
|
| 226 |
+
|
| 227 |
+
if not opts.byte_offsets:
|
| 228 |
+
return chunks
|
| 229 |
+
|
| 230 |
+
# Convert character offsets to byte offsets
|
| 231 |
+
char_positions = [offset for offset, _ in chunks]
|
| 232 |
+
byte_positions = sct.char_to_byte_offsets(text, char_positions)
|
| 233 |
+
|
| 234 |
+
return [(byte_positions[i], chunk) for i, (_, chunk) in enumerate(chunks)]
|
| 235 |
|
| 236 |
|
| 237 |
@cache
|
|
|
|
| 240 |
"""
|
| 241 |
Load and cache the tokenizer model based on the predefined model name.
|
| 242 |
|
| 243 |
+
This tokenizer keeps the vendored truncation (128 tokens) and padding settings and is used
|
| 244 |
+
to embed chunks. For chunk sizing use chunking_tokenizer() instead.
|
| 245 |
+
|
| 246 |
:return: An instance of the Tokenizer.
|
| 247 |
"""
|
| 248 |
with sct.timer("TOKENIZER load time"):
|
| 249 |
return Tokenizer.from_file(TOKENIZER_PATH.as_posix())
|
| 250 |
|
| 251 |
|
| 252 |
+
@cache
|
| 253 |
+
def chunking_tokenizer():
|
| 254 |
+
# type: () -> Tokenizer
|
| 255 |
+
"""
|
| 256 |
+
Load and cache the tokenizer used for chunk sizing, with truncation and padding disabled.
|
| 257 |
+
|
| 258 |
+
The embedding tokenizer truncates to the model's 128-token window. With tokenizers >=0.23
|
| 259 |
+
the Hugging Face chunk sizer then sees one overflow encoding per 128 tokens, so sizing a
|
| 260 |
+
huge probe string costs O(length) and chunking degrades to super-linear runtime (issue
|
| 261 |
+
#24). Chunk sizing needs the true token count, so truncation is disabled here; padding
|
| 262 |
+
would only add tokens irrelevant to a count and is disabled too. Boundaries stay unchanged
|
| 263 |
+
because the splitter only compares sizes against the token capacity: at or below it the
|
| 264 |
+
full and truncated counts agree, and any larger probe exceeds it under both, so every
|
| 265 |
+
accept/reject decision is identical.
|
| 266 |
+
|
| 267 |
+
:return: A Tokenizer with truncation and padding disabled.
|
| 268 |
+
"""
|
| 269 |
+
tok = Tokenizer.from_file(TOKENIZER_PATH.as_posix())
|
| 270 |
+
tok.no_truncation()
|
| 271 |
+
tok.no_padding()
|
| 272 |
+
return tok
|
| 273 |
+
|
| 274 |
+
|
| 275 |
@cache
|
| 276 |
def splitter(**options):
|
| 277 |
# type: (Any) -> TextSplitter
|
| 278 |
"""
|
| 279 |
+
Load and cache the text splitter, initialized with the chunking tokenizer.
|
| 280 |
|
| 281 |
:param options: Custom processing options for overriding global options
|
| 282 |
:key max_tokens (int): Max tokens per chunk (default 127).
|
| 283 |
+
:key overlap (int): Max tokens allowed overlapping between chunks (default 48).
|
| 284 |
:key trim (int): Trim whitespace from chunks (default False).
|
| 285 |
:return: An instance of TextSplitter.
|
| 286 |
"""
|
| 287 |
opts = sct.sct_opts.override(options)
|
| 288 |
with sct.timer("TEXTSPLITTER load time"):
|
| 289 |
return TextSplitter.from_huggingface_tokenizer(
|
| 290 |
+
chunking_tokenizer(), capacity=opts.max_tokens, overlap=opts.overlap, trim=opts.trim
|
| 291 |
)
|
| 292 |
|
| 293 |
|
| 294 |
+
@cache
|
| 295 |
+
def splitter_guarded(**options):
|
| 296 |
+
# type: (Any) -> TextSplitter
|
| 297 |
+
"""
|
| 298 |
+
Load and cache a text splitter that sizes chunks via a guarded Python callback.
|
| 299 |
+
|
| 300 |
+
Produces chunks identical to splitter() but avoids the super-linear cost of sizing huge
|
| 301 |
+
probe texts on inputs without regular paragraph separators (see needs_split_guard).
|
| 302 |
+
|
| 303 |
+
:param options: Custom processing options for overriding global options
|
| 304 |
+
:key max_tokens (int): Max tokens per chunk (default 127).
|
| 305 |
+
:key overlap (int): Max tokens allowed overlapping between chunks (default 48).
|
| 306 |
+
:key trim (int): Trim whitespace from chunks (default False).
|
| 307 |
+
:return: An instance of TextSplitter.
|
| 308 |
+
"""
|
| 309 |
+
opts = sct.sct_opts.override(options)
|
| 310 |
+
sizer = partial(token_count_guarded, max_tokens=opts.max_tokens)
|
| 311 |
+
with sct.timer("TEXTSPLITTER load time"):
|
| 312 |
+
return TextSplitter.from_callback(
|
| 313 |
+
sizer, capacity=opts.max_tokens, overlap=opts.overlap, trim=opts.trim
|
| 314 |
+
)
|
| 315 |
+
|
| 316 |
+
|
| 317 |
+
def needs_split_guard(text):
|
| 318 |
+
# type: (str) -> bool
|
| 319 |
+
"""
|
| 320 |
+
Detect text where tokenizer-based chunking degrades to super-linear runtime.
|
| 321 |
+
|
| 322 |
+
To size an oversized section, text-splitter (>=0.32.0) probes prefixes up to that
|
| 323 |
+
section's lower-level semantic boundaries instead of tokenizing the whole section, which
|
| 324 |
+
keeps chunking near-linear while such boundaries exist. When a span carries no
|
| 325 |
+
intermediate separator for more than SPLIT_GUARD_GAP characters - a giant single
|
| 326 |
+
paragraph, a trailing separator-free run, or words sitting far from the next
|
| 327 |
+
paragraph-level separator (print-layout PDF extraction, issue #24) - the probe finds no
|
| 328 |
+
boundary and falls back to tokenizing the whole section, so chunking time grows
|
| 329 |
+
quadratically with the gap size.
|
| 330 |
+
|
| 331 |
+
Such texts are routed to the guarded splitter, whose token sizer caps that fallback cost.
|
| 332 |
+
|
| 333 |
+
:param text: Text to analyze.
|
| 334 |
+
:return: True if the guarded splitter should be used for this text.
|
| 335 |
+
"""
|
| 336 |
+
runs = []
|
| 337 |
+
for match in NEWLINE_RUNS.finditer(text):
|
| 338 |
+
run = match.group()
|
| 339 |
+
level = len(run) - run.count("\r\n") # number of newline graphemes in the run
|
| 340 |
+
if level >= 2:
|
| 341 |
+
runs.append((level, match.start(), match.end()))
|
| 342 |
+
for min_level in sorted({level for level, _, _ in runs}):
|
| 343 |
+
pos = 0
|
| 344 |
+
for level, start, end in runs:
|
| 345 |
+
if level < min_level:
|
| 346 |
+
continue
|
| 347 |
+
if start - pos > SPLIT_GUARD_GAP:
|
| 348 |
+
return True
|
| 349 |
+
pos = end
|
| 350 |
+
# A span containing no newline at all blows up the native sizer the same way: with no
|
| 351 |
+
# line- or paragraph-level separator to bound the probes, sizing reaches toward a distant
|
| 352 |
+
# or absent coarse separator. Catches giant single paragraphs and trailing separator-free
|
| 353 |
+
# runs that the paragraph-level scan above does not see.
|
| 354 |
+
pos = 0
|
| 355 |
+
for match in NEWLINE.finditer(text):
|
| 356 |
+
if match.start() - pos > SPLIT_GUARD_GAP:
|
| 357 |
+
return True
|
| 358 |
+
pos = match.end()
|
| 359 |
+
return len(text) - pos > SPLIT_GUARD_GAP
|
| 360 |
+
|
| 361 |
+
|
| 362 |
+
def token_count(text):
|
| 363 |
+
# type: (str) -> int
|
| 364 |
+
"""
|
| 365 |
+
Count tokens exactly like text-splitter's Hugging Face tokenizer chunk sizer.
|
| 366 |
+
|
| 367 |
+
Encodes without special tokens via the chunking tokenizer (truncation and padding
|
| 368 |
+
disabled), so the count reflects the full input text with no overflow encodings to sum.
|
| 369 |
+
|
| 370 |
+
:param text: Text to size.
|
| 371 |
+
:return: Number of tokens.
|
| 372 |
+
"""
|
| 373 |
+
return len(chunking_tokenizer().encode(text, add_special_tokens=False).ids)
|
| 374 |
+
|
| 375 |
+
|
| 376 |
+
def token_count_guarded(text, max_tokens):
|
| 377 |
+
# type: (str, int) -> int
|
| 378 |
+
"""
|
| 379 |
+
Count tokens with a short-circuit for texts far larger than the chunk capacity.
|
| 380 |
+
|
| 381 |
+
For long texts, tokenize only a prefix that ends at a Unicode whitespace boundary. The
|
| 382 |
+
tokenizer pre-splits on whitespace (WhitespaceSplit + Metaspace), so the full text has at
|
| 383 |
+
least as many tokens as that prefix. If the prefix alone exceeds max_tokens, full
|
| 384 |
+
tokenization is skipped - the splitter only needs to know that the text is too big for one
|
| 385 |
+
chunk. The returned overestimate stays above max_tokens and grows with text length, so all
|
| 386 |
+
chunk capacity comparisons behave exactly as with real token counts.
|
| 387 |
+
|
| 388 |
+
:param text: Text to size.
|
| 389 |
+
:param max_tokens: Chunk capacity the splitter validates against.
|
| 390 |
+
:return: Number of tokens (exact, or an overestimate for oversized texts).
|
| 391 |
+
"""
|
| 392 |
+
probe_chars = max_tokens * 10
|
| 393 |
+
if len(text) > probe_chars * 2:
|
| 394 |
+
window = text[:probe_chars]
|
| 395 |
+
cut = -1
|
| 396 |
+
for match in WHITESPACE.finditer(window):
|
| 397 |
+
cut = match.start()
|
| 398 |
+
if cut > 0:
|
| 399 |
+
prefix_count = token_count(text[:cut])
|
| 400 |
+
if prefix_count > max_tokens:
|
| 401 |
+
return prefix_count + len(text) - cut
|
| 402 |
+
return token_count(text)
|
| 403 |
+
|
| 404 |
+
|
| 405 |
+
def warn_gpu_shadowed(available_providers):
|
| 406 |
+
# type: (list[str]) -> None
|
| 407 |
+
"""
|
| 408 |
+
Warn when onnxruntime-gpu is installed but CUDA support is unavailable.
|
| 409 |
+
|
| 410 |
+
Both onnxruntime variant wheels unpack into the same directory, so installing the CPU
|
| 411 |
+
package alongside onnxruntime-gpu silently disables CUDA support.
|
| 412 |
+
|
| 413 |
+
:param available_providers: Providers reported by the installed onnxruntime build.
|
| 414 |
+
"""
|
| 415 |
+
if "CUDAExecutionProvider" in available_providers:
|
| 416 |
+
return
|
| 417 |
+
try:
|
| 418 |
+
distribution("onnxruntime-gpu")
|
| 419 |
+
except PackageNotFoundError:
|
| 420 |
+
return
|
| 421 |
+
log.warning(
|
| 422 |
+
"onnxruntime-gpu is installed but CUDA support is unavailable - the onnxruntime CPU "
|
| 423 |
+
"package likely overwrote the GPU build. To fix run: pip uninstall -y onnxruntime "
|
| 424 |
+
'onnxruntime-gpu && pip install --force-reinstall "iscc-sct[gpu]"'
|
| 425 |
+
)
|
| 426 |
+
|
| 427 |
+
|
| 428 |
+
ONNX_RUNTIME_MISSING = (
|
| 429 |
+
"iscc-sct requires an ONNX runtime. Install exactly one of:\n"
|
| 430 |
+
' pip install "iscc-sct[cpu]" # CPU inference\n'
|
| 431 |
+
' pip install "iscc-sct[gpu]" # NVIDIA CUDA accelerated inference'
|
| 432 |
+
)
|
| 433 |
+
|
| 434 |
+
|
| 435 |
+
def load_onnxruntime():
|
| 436 |
+
# type: () -> Any
|
| 437 |
+
"""
|
| 438 |
+
Import and return the onnxruntime module.
|
| 439 |
+
|
| 440 |
+
The ONNX runtime is an optional dependency selected via the mutually exclusive cpu/gpu
|
| 441 |
+
extras, so the import is deferred until a model is actually needed. This lets the rest of
|
| 442 |
+
the package - and the `iscc-sct doctor` command - load without a runtime installed.
|
| 443 |
+
|
| 444 |
+
:return: The imported onnxruntime module.
|
| 445 |
+
:raises ImportError: If no ONNX runtime is installed.
|
| 446 |
+
"""
|
| 447 |
+
try:
|
| 448 |
+
import onnxruntime as rt
|
| 449 |
+
except ImportError: # pragma: no cover - exercised only without a runtime extra
|
| 450 |
+
raise ImportError(ONNX_RUNTIME_MISSING) from None
|
| 451 |
+
return rt
|
| 452 |
+
|
| 453 |
+
|
| 454 |
+
def resolve_batch_size(batch_size, providers):
|
| 455 |
+
# type: (int, list[str]) -> int
|
| 456 |
+
"""
|
| 457 |
+
Resolve the number of chunks to embed per inference batch.
|
| 458 |
+
|
| 459 |
+
A non-zero batch_size is used as given. Zero means auto, which depends on the execution
|
| 460 |
+
provider: on CPU the vendored tokenizer pads every batch to its longest chunk and attention
|
| 461 |
+
cost grows quadratically with sequence length, so batching mostly buys padding - single-chunk
|
| 462 |
+
batches are both faster and hold a smaller activation peak. A GPU instead needs large batches
|
| 463 |
+
to stay saturated, and its activations live in device memory rather than in the process.
|
| 464 |
+
|
| 465 |
+
:param batch_size: Configured batch size, 0 for auto.
|
| 466 |
+
:param providers: Execution providers the inference session actually runs on.
|
| 467 |
+
:return: Number of chunks per batch.
|
| 468 |
+
"""
|
| 469 |
+
if batch_size:
|
| 470 |
+
return batch_size
|
| 471 |
+
return GPU_BATCH_SIZE if "CUDAExecutionProvider" in providers else CPU_BATCH_SIZE
|
| 472 |
+
|
| 473 |
+
|
| 474 |
+
def session_options(rt, intra_op_threads):
|
| 475 |
+
# type: (Any, int) -> Any
|
| 476 |
+
"""
|
| 477 |
+
Build the ONNX Runtime session options for the inference session.
|
| 478 |
+
|
| 479 |
+
Thread count is a session-level setting and the session is a process-wide singleton, so it
|
| 480 |
+
is read from the global options rather than passed per call. Leaving it at 0 keeps the
|
| 481 |
+
runtime default (one thread per core), which is fastest for a single process. Callers that
|
| 482 |
+
run a pool of one worker per core should set it to 1 to avoid oversubscription.
|
| 483 |
+
|
| 484 |
+
:param rt: The onnxruntime module.
|
| 485 |
+
:param intra_op_threads: Threads per operator, 0 for the runtime default.
|
| 486 |
+
:return: Configured onnxruntime.SessionOptions.
|
| 487 |
+
"""
|
| 488 |
+
so = rt.SessionOptions()
|
| 489 |
+
so.graph_optimization_level = rt.GraphOptimizationLevel.ORT_ENABLE_ALL
|
| 490 |
+
if intra_op_threads:
|
| 491 |
+
so.intra_op_num_threads = intra_op_threads
|
| 492 |
+
return so
|
| 493 |
+
|
| 494 |
+
|
| 495 |
@cache
|
| 496 |
def model():
|
| 497 |
+
# type: () -> Any
|
| 498 |
"""
|
| 499 |
Load and cache the ONNX inference model from a specified path.
|
| 500 |
|
| 501 |
+
:return: An ONNX inference session (onnxruntime.InferenceSession).
|
| 502 |
"""
|
| 503 |
+
rt = load_onnxruntime()
|
| 504 |
+
from onnxruntime.capi.onnxruntime_pybind11_state import NoSuchFile
|
| 505 |
+
|
| 506 |
available_onnx_providers = rt.get_available_providers()
|
| 507 |
log.debug(f"Available ONNX providers {', '.join(available_onnx_providers)}")
|
| 508 |
+
warn_gpu_shadowed(available_onnx_providers)
|
| 509 |
selected_onnx_providers = ["CPUExecutionProvider"]
|
| 510 |
if "CUDAExecutionProvider" in available_onnx_providers: # pragma: no cover
|
| 511 |
selected_onnx_providers.insert(0, "CUDAExecutionProvider")
|
| 512 |
+
if hasattr(rt, "preload_dlls"):
|
| 513 |
+
# Load CUDA/cuDNN libraries from pip-provided nvidia wheels or the system PATH
|
| 514 |
+
# before session creation (available since onnxruntime 1.21).
|
| 515 |
+
rt.preload_dlls()
|
| 516 |
log.debug(f"Using ONNX providers {', '.join(selected_onnx_providers)}")
|
| 517 |
+
so = session_options(rt, sct.sct_opts.intra_op_threads)
|
|
|
|
| 518 |
try:
|
| 519 |
with sct.timer("ONNXMODEL load time"):
|
| 520 |
return rt.InferenceSession(
|
|
|
|
| 543 |
return {"input_ids": input_ids, "attention_mask": attention_mask, "token_type_ids": type_ids}
|
| 544 |
|
| 545 |
|
| 546 |
+
def embed_chunks(chunks, batch_size=None):
|
| 547 |
+
# type: (list[str], int|None) -> NDArray
|
| 548 |
"""
|
| 549 |
Embed text chunks and return vector embeddings.
|
| 550 |
|
| 551 |
:param chunks: Text chunks to embed.
|
| 552 |
+
:param batch_size: Number of chunks to process in each batch (None = use the global
|
| 553 |
+
`batch_size` option, 0 = auto, see resolve_batch_size).
|
| 554 |
:return: An array of embeddings for each chunk.
|
| 555 |
"""
|
| 556 |
+
if batch_size is None:
|
| 557 |
+
batch_size = sct.sct_opts.batch_size
|
| 558 |
+
batch_size = resolve_batch_size(batch_size, model().get_providers())
|
| 559 |
embeddings = []
|
| 560 |
for start_idx in range(0, len(chunks), batch_size):
|
| 561 |
batch_chunks = chunks[start_idx : start_idx + batch_size]
|
|
|
|
| 581 |
def attention_pooling(token_embeddings, attention_mask):
|
| 582 |
# type: (np.array, np.array) -> np.array
|
| 583 |
"""
|
| 584 |
+
Apply attention mask-based mean pooling to the token embeddings.
|
| 585 |
|
| 586 |
:param token_embeddings: Raw token embeddings from the model.
|
| 587 |
:param attention_mask: Attention masks for the embeddings.
|
|
|
|
| 614 |
Binarize an embedding vector into a hash digest.
|
| 615 |
|
| 616 |
:param vec: Vector to be binarized.
|
| 617 |
+
:return: A byte object representing the binary hash.
|
| 618 |
"""
|
| 619 |
return bytes((np.packbits(np.array(vec) >= 0)))
|
| 620 |
|
|
|
|
| 626 |
|
| 627 |
:param vec: Embedding vector.
|
| 628 |
:param precision: Max number of fractional decimal places.
|
| 629 |
+
:return: Vector as a native python list of rounded floats.
|
| 630 |
"""
|
| 631 |
rounded_array = np.around(vec, decimals=precision)
|
| 632 |
compress_list = [round(x, precision) for x in rounded_array.tolist()]
|
iscc_sct/demo.py
CHANGED
|
@@ -3,6 +3,7 @@ Gradio demo showcasing ISCC Semantic Text Code.
|
|
| 3 |
"""
|
| 4 |
|
| 5 |
from loguru import logger as log
|
|
|
|
| 6 |
import gradio as gr
|
| 7 |
import iscc_sct as sct
|
| 8 |
import textwrap
|
|
@@ -68,9 +69,6 @@ def compute_iscc_code(text1, text2, bit_length):
|
|
| 68 |
return code1["iscc"], code2["iscc"], similarity
|
| 69 |
|
| 70 |
|
| 71 |
-
import binascii
|
| 72 |
-
|
| 73 |
-
|
| 74 |
def compare_codes(code_a, code_b, bits):
|
| 75 |
if code_a and code_b:
|
| 76 |
code_a_str = code_a.value if hasattr(code_a, "value") else str(code_a)
|
|
@@ -140,7 +138,10 @@ iscc_theme = gr.themes.Default(
|
|
| 140 |
radius_size=gr.themes.sizes.radius_none,
|
| 141 |
)
|
| 142 |
|
| 143 |
-
|
|
|
|
|
|
|
|
|
|
| 144 |
with gr.Row(variant="panel"):
|
| 145 |
gr.Markdown(
|
| 146 |
"""
|
|
@@ -519,4 +520,4 @@ For more information about the **ISCC** see:
|
|
| 519 |
)
|
| 520 |
|
| 521 |
if __name__ == "__main__": # pragma: no cover
|
| 522 |
-
demo.launch()
|
|
|
|
| 3 |
"""
|
| 4 |
|
| 5 |
from loguru import logger as log
|
| 6 |
+
import binascii
|
| 7 |
import gradio as gr
|
| 8 |
import iscc_sct as sct
|
| 9 |
import textwrap
|
|
|
|
| 69 |
return code1["iscc"], code2["iscc"], similarity
|
| 70 |
|
| 71 |
|
|
|
|
|
|
|
|
|
|
| 72 |
def compare_codes(code_a, code_b, bits):
|
| 73 |
if code_a and code_b:
|
| 74 |
code_a_str = code_a.value if hasattr(code_a, "value") else str(code_a)
|
|
|
|
| 138 |
radius_size=gr.themes.sizes.radius_none,
|
| 139 |
)
|
| 140 |
|
| 141 |
+
# Gradio 6 expects theme and css as launch() parameters - pass these at every launch site
|
| 142 |
+
launch_kwargs = {"theme": iscc_theme, "css": custom_css}
|
| 143 |
+
|
| 144 |
+
with gr.Blocks() as demo:
|
| 145 |
with gr.Row(variant="panel"):
|
| 146 |
gr.Markdown(
|
| 147 |
"""
|
|
|
|
| 520 |
)
|
| 521 |
|
| 522 |
if __name__ == "__main__": # pragma: no cover
|
| 523 |
+
demo.launch(**launch_kwargs)
|
iscc_sct/dev.py
CHANGED
|
@@ -24,7 +24,7 @@ def convert_lf(): # pragma: no cover
|
|
| 24 |
|
| 25 |
|
| 26 |
def format_yml():
|
| 27 |
-
for f in HERE.glob("**
|
| 28 |
with open(f, "rt", encoding="utf-8") as infile:
|
| 29 |
data = yaml.safe_load(infile)
|
| 30 |
with open(f, "wt", encoding="utf-8", newline="\n") as outf:
|
|
|
|
| 24 |
|
| 25 |
|
| 26 |
def format_yml():
|
| 27 |
+
for f in HERE.glob("**/*.yml"):
|
| 28 |
with open(f, "rt", encoding="utf-8") as infile:
|
| 29 |
data = yaml.safe_load(infile)
|
| 30 |
with open(f, "wt", encoding="utf-8", newline="\n") as outf:
|
iscc_sct/doctor.py
ADDED
|
@@ -0,0 +1,217 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""ONNX runtime diagnostics and opt-in repair for the `iscc-sct doctor` command.
|
| 2 |
+
|
| 3 |
+
Inspects the installed ONNX runtime and classifies the environment: no runtime, a CPU
|
| 4 |
+
`onnxruntime` package shadowing `onnxruntime-gpu` (both installed, CUDA disabled),
|
| 5 |
+
`onnxruntime-gpu` installed without usable CUDA (driver/hardware issue, not fixable by
|
| 6 |
+
reinstalling), or healthy. Recommends the right install extra and - only with explicit
|
| 7 |
+
confirmation - runs the matching pip command in a subprocess.
|
| 8 |
+
"""
|
| 9 |
+
|
| 10 |
+
import importlib
|
| 11 |
+
import shutil
|
| 12 |
+
import subprocess
|
| 13 |
+
import sys
|
| 14 |
+
from importlib.metadata import PackageNotFoundError, distribution
|
| 15 |
+
|
| 16 |
+
|
| 17 |
+
__all__ = ["doctor_report", "format_report", "run_doctor"]
|
| 18 |
+
|
| 19 |
+
|
| 20 |
+
def _dist_installed(name):
|
| 21 |
+
# type: (str) -> bool
|
| 22 |
+
"""
|
| 23 |
+
Check whether a distribution is installed.
|
| 24 |
+
|
| 25 |
+
:param name: Distribution name to look up.
|
| 26 |
+
:return: True if the distribution is installed.
|
| 27 |
+
"""
|
| 28 |
+
try:
|
| 29 |
+
distribution(name)
|
| 30 |
+
return True
|
| 31 |
+
except PackageNotFoundError:
|
| 32 |
+
return False
|
| 33 |
+
|
| 34 |
+
|
| 35 |
+
def _onnx_providers():
|
| 36 |
+
# type: () -> list[str] | None
|
| 37 |
+
"""
|
| 38 |
+
Return the available onnxruntime execution providers.
|
| 39 |
+
|
| 40 |
+
:return: Provider names reported by onnxruntime, or None if no runtime is importable.
|
| 41 |
+
"""
|
| 42 |
+
try:
|
| 43 |
+
rt = importlib.import_module("onnxruntime")
|
| 44 |
+
except ImportError:
|
| 45 |
+
return None
|
| 46 |
+
return rt.get_available_providers()
|
| 47 |
+
|
| 48 |
+
|
| 49 |
+
def _gpu_present():
|
| 50 |
+
# type: () -> bool
|
| 51 |
+
"""
|
| 52 |
+
Detect an NVIDIA GPU by probing for the nvidia-smi tool on PATH.
|
| 53 |
+
|
| 54 |
+
:return: True if nvidia-smi is found on PATH.
|
| 55 |
+
"""
|
| 56 |
+
return shutil.which("nvidia-smi") is not None
|
| 57 |
+
|
| 58 |
+
|
| 59 |
+
def _diagnose(providers, gpu_dist, gpu_present, cpu_dist):
|
| 60 |
+
# type: (list[str]|None, bool, bool, bool) -> dict
|
| 61 |
+
"""
|
| 62 |
+
Derive runtime status, recommended extra, and pip fix from environment facts.
|
| 63 |
+
|
| 64 |
+
:param providers: onnxruntime.get_available_providers() result, or None if not importable.
|
| 65 |
+
:param gpu_dist: Whether the onnxruntime-gpu distribution is installed.
|
| 66 |
+
:param gpu_present: Whether an NVIDIA GPU was detected.
|
| 67 |
+
:param cpu_dist: Whether the CPU onnxruntime distribution is installed.
|
| 68 |
+
:return: Report dict with status, runtime, cuda, gpu_present, gpu_dist, cpu_dist, extra,
|
| 69 |
+
fix, hint.
|
| 70 |
+
"""
|
| 71 |
+
runtime = providers is not None
|
| 72 |
+
cuda = runtime and "CUDAExecutionProvider" in providers
|
| 73 |
+
extra = "gpu" if gpu_present else "cpu"
|
| 74 |
+
|
| 75 |
+
if runtime and gpu_dist and cpu_dist and not cuda:
|
| 76 |
+
# Both runtime distributions are installed: their wheels unpacked into the same
|
| 77 |
+
# directory and the CPU build clobbered onnxruntime-gpu, disabling CUDA (issue #23).
|
| 78 |
+
status = "shadowed"
|
| 79 |
+
fix = [
|
| 80 |
+
["uninstall", "-y", "onnxruntime", "onnxruntime-gpu"],
|
| 81 |
+
["install", "--force-reinstall", "iscc-sct[gpu]"],
|
| 82 |
+
]
|
| 83 |
+
elif not runtime:
|
| 84 |
+
status = "missing"
|
| 85 |
+
fix = [["install", f"iscc-sct[{extra}]"]]
|
| 86 |
+
elif gpu_dist and not cuda:
|
| 87 |
+
# onnxruntime-gpu is installed and importable but CUDA is unavailable, and nothing
|
| 88 |
+
# shadowed it - the NVIDIA driver/GPU is missing or the CUDA libraries failed to load.
|
| 89 |
+
# Reinstalling cannot fix an environment problem, so offer no pip fix.
|
| 90 |
+
status = "no_cuda"
|
| 91 |
+
fix = []
|
| 92 |
+
else:
|
| 93 |
+
status = "ok"
|
| 94 |
+
fix = []
|
| 95 |
+
|
| 96 |
+
hint = status == "ok" and gpu_present and not cuda and not gpu_dist
|
| 97 |
+
return {
|
| 98 |
+
"status": status,
|
| 99 |
+
"runtime": runtime,
|
| 100 |
+
"cuda": cuda,
|
| 101 |
+
"gpu_present": gpu_present,
|
| 102 |
+
"gpu_dist": gpu_dist,
|
| 103 |
+
"cpu_dist": cpu_dist,
|
| 104 |
+
"extra": extra,
|
| 105 |
+
"fix": fix,
|
| 106 |
+
"hint": hint,
|
| 107 |
+
}
|
| 108 |
+
|
| 109 |
+
|
| 110 |
+
def doctor_report():
|
| 111 |
+
# type: () -> dict
|
| 112 |
+
"""
|
| 113 |
+
Diagnose the ONNX runtime in the current environment.
|
| 114 |
+
|
| 115 |
+
:return: A report dict (see `_diagnose`).
|
| 116 |
+
"""
|
| 117 |
+
return _diagnose(
|
| 118 |
+
_onnx_providers(),
|
| 119 |
+
_dist_installed("onnxruntime-gpu"),
|
| 120 |
+
_gpu_present(),
|
| 121 |
+
_dist_installed("onnxruntime"),
|
| 122 |
+
)
|
| 123 |
+
|
| 124 |
+
|
| 125 |
+
def _yn(flag):
|
| 126 |
+
# type: (bool) -> str
|
| 127 |
+
"""Render a boolean as yes/no."""
|
| 128 |
+
return "yes" if flag else "no"
|
| 129 |
+
|
| 130 |
+
|
| 131 |
+
def _runtime_label(report):
|
| 132 |
+
# type: (dict) -> str
|
| 133 |
+
"""Describe which ONNX runtime package(s) are installed."""
|
| 134 |
+
if report["gpu_dist"] and report["cpu_dist"]:
|
| 135 |
+
return "onnxruntime + onnxruntime-gpu (conflicting)"
|
| 136 |
+
if report["gpu_dist"]:
|
| 137 |
+
return "onnxruntime-gpu"
|
| 138 |
+
if report["runtime"]:
|
| 139 |
+
return "onnxruntime"
|
| 140 |
+
return "not installed"
|
| 141 |
+
|
| 142 |
+
|
| 143 |
+
def format_report(report):
|
| 144 |
+
# type: (dict) -> str
|
| 145 |
+
"""
|
| 146 |
+
Render a human-readable diagnosis.
|
| 147 |
+
|
| 148 |
+
:param report: Report dict from `doctor_report`.
|
| 149 |
+
:return: Multi-line report string.
|
| 150 |
+
"""
|
| 151 |
+
lines = [
|
| 152 |
+
"iscc-sct ONNX runtime check",
|
| 153 |
+
"",
|
| 154 |
+
f" ONNX runtime: {_runtime_label(report)}",
|
| 155 |
+
f" CUDA provider: {_yn(report['cuda'])}",
|
| 156 |
+
f" NVIDIA GPU: {_yn(report['gpu_present'])}",
|
| 157 |
+
"",
|
| 158 |
+
]
|
| 159 |
+
if report["status"] == "ok":
|
| 160 |
+
lines.append("Status: OK - a working ONNX runtime is installed.")
|
| 161 |
+
if report["hint"]:
|
| 162 |
+
lines.append('GPU detected. For CUDA acceleration: pip install "iscc-sct[gpu]"')
|
| 163 |
+
elif report["status"] == "missing":
|
| 164 |
+
lines.append("Status: no ONNX runtime installed.")
|
| 165 |
+
lines.append(f'Recommended: pip install "iscc-sct[{report["extra"]}]"')
|
| 166 |
+
elif report["status"] == "no_cuda":
|
| 167 |
+
lines.append(
|
| 168 |
+
"Status: onnxruntime-gpu is installed and runs on CPU, but CUDA is unavailable - no "
|
| 169 |
+
"NVIDIA driver/GPU was detected or the CUDA libraries failed to load. This is an "
|
| 170 |
+
"environment issue, not a packaging problem; reinstalling will not help."
|
| 171 |
+
)
|
| 172 |
+
else:
|
| 173 |
+
lines.append(
|
| 174 |
+
"Status: onnxruntime-gpu is installed but CUDA is unavailable - the CPU onnxruntime "
|
| 175 |
+
"package shadowed the GPU build. The fix reinstalls only the GPU build."
|
| 176 |
+
)
|
| 177 |
+
return "\n".join(lines)
|
| 178 |
+
|
| 179 |
+
|
| 180 |
+
def _confirm(assume_yes):
|
| 181 |
+
# type: (bool) -> bool
|
| 182 |
+
"""
|
| 183 |
+
Confirm the fix with the user unless auto-confirmed.
|
| 184 |
+
|
| 185 |
+
:param assume_yes: Skip the prompt and proceed when True.
|
| 186 |
+
:return: True to proceed with the fix.
|
| 187 |
+
"""
|
| 188 |
+
if assume_yes:
|
| 189 |
+
return True
|
| 190 |
+
try:
|
| 191 |
+
answer = input("Install it now? [y/N] ")
|
| 192 |
+
except EOFError:
|
| 193 |
+
return False
|
| 194 |
+
return answer.strip().lower() in ("y", "yes")
|
| 195 |
+
|
| 196 |
+
|
| 197 |
+
def run_doctor(assume_yes=False):
|
| 198 |
+
# type: (bool) -> int
|
| 199 |
+
"""
|
| 200 |
+
Print the ONNX runtime diagnosis and, on confirmation, run the recommended pip fix.
|
| 201 |
+
|
| 202 |
+
:param assume_yes: Auto-confirm the fix without prompting.
|
| 203 |
+
:return: Exit code: 0 when healthy or the fix ran cleanly, 1 otherwise.
|
| 204 |
+
"""
|
| 205 |
+
report = doctor_report()
|
| 206 |
+
print(format_report(report))
|
| 207 |
+
if not report["fix"]:
|
| 208 |
+
return 0
|
| 209 |
+
if not _confirm(assume_yes):
|
| 210 |
+
print("No changes made.")
|
| 211 |
+
return 1
|
| 212 |
+
failed = False
|
| 213 |
+
for args in report["fix"]:
|
| 214 |
+
print(f" running: pip {' '.join(args)}")
|
| 215 |
+
completed = subprocess.run([sys.executable, "-m", "pip", *args])
|
| 216 |
+
failed = failed or completed.returncode != 0
|
| 217 |
+
return 1 if failed else 0
|
iscc_sct/models.py
CHANGED
|
@@ -98,6 +98,7 @@ class FeatureSet(PrettyBaseModel):
|
|
| 98 |
maintype: str = "semantic"
|
| 99 |
subtype: str = "text"
|
| 100 |
version: int = 0
|
|
|
|
| 101 |
embedding: Optional[List[float]] = None
|
| 102 |
simprints: Optional[
|
| 103 |
Union[
|
|
|
|
| 98 |
maintype: str = "semantic"
|
| 99 |
subtype: str = "text"
|
| 100 |
version: int = 0
|
| 101 |
+
byte_offsets: Optional[bool] = False
|
| 102 |
embedding: Optional[List[float]] = None
|
| 103 |
simprints: Optional[
|
| 104 |
Union[
|
iscc_sct/options.py
CHANGED
|
@@ -47,6 +47,11 @@ class SctOptions(BaseSettings):
|
|
| 47 |
False, description="ISCC_SCT_OFFSETS - Include offsets of granular features"
|
| 48 |
)
|
| 49 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 50 |
sizes: bool = Field(
|
| 51 |
False, description="ISCC_SCT_SIZES - Include sizes of granular features (number of chars)"
|
| 52 |
)
|
|
@@ -68,6 +73,19 @@ class SctOptions(BaseSettings):
|
|
| 68 |
False, description="ISCC_SCT_TRIM - Trim whitespace from chunks (Default False)"
|
| 69 |
)
|
| 70 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 71 |
model_config = SettingsConfigDict(
|
| 72 |
env_file=".env",
|
| 73 |
env_file_encoding="utf-8",
|
|
|
|
| 47 |
False, description="ISCC_SCT_OFFSETS - Include offsets of granular features"
|
| 48 |
)
|
| 49 |
|
| 50 |
+
byte_offsets: bool = Field(
|
| 51 |
+
False,
|
| 52 |
+
description="ISCC_SCT_BYTE_OFFSETS - Use UTF-8 byte offsets instead of character offsets",
|
| 53 |
+
)
|
| 54 |
+
|
| 55 |
sizes: bool = Field(
|
| 56 |
False, description="ISCC_SCT_SIZES - Include sizes of granular features (number of chars)"
|
| 57 |
)
|
|
|
|
| 73 |
False, description="ISCC_SCT_TRIM - Trim whitespace from chunks (Default False)"
|
| 74 |
)
|
| 75 |
|
| 76 |
+
batch_size: int = Field(
|
| 77 |
+
0,
|
| 78 |
+
description="ISCC_SCT_BATCH_SIZE - Chunks per inference batch (0 = auto, Default 0)",
|
| 79 |
+
ge=0,
|
| 80 |
+
)
|
| 81 |
+
|
| 82 |
+
intra_op_threads: int = Field(
|
| 83 |
+
0,
|
| 84 |
+
description="ISCC_SCT_INTRA_OP_THREADS - ONNX Runtime threads per operator "
|
| 85 |
+
"(0 = runtime default, Default 0)",
|
| 86 |
+
ge=0,
|
| 87 |
+
)
|
| 88 |
+
|
| 89 |
model_config = SettingsConfigDict(
|
| 90 |
env_file=".env",
|
| 91 |
env_file_encoding="utf-8",
|
iscc_sct/utils.py
CHANGED
|
@@ -15,7 +15,11 @@ from iscc_sct.models import Metadata, Feature
|
|
| 15 |
APP_NAME = "iscc-sct"
|
| 16 |
APP_AUTHOR = "iscc"
|
| 17 |
dirs = PlatformDirs(appname=APP_NAME, appauthor=APP_AUTHOR)
|
| 18 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 19 |
|
| 20 |
|
| 21 |
__all__ = [
|
|
@@ -29,16 +33,17 @@ __all__ = [
|
|
| 29 |
"iscc_distance",
|
| 30 |
"cosine_similarity",
|
| 31 |
"granular_similarity",
|
|
|
|
| 32 |
"MODEL_PATH",
|
| 33 |
]
|
| 34 |
|
| 35 |
|
| 36 |
BASE_VERSION = "1.0.0"
|
| 37 |
BASE_URL = f"https://github.com/iscc/iscc-binaries/releases/download/v{BASE_VERSION}"
|
| 38 |
-
MODEL_FILENAME = "iscc-sct-v0.
|
| 39 |
MODEL_URL = f"{BASE_URL}/{MODEL_FILENAME}"
|
| 40 |
-
MODEL_PATH =
|
| 41 |
-
MODEL_CHECKSUM = "
|
| 42 |
|
| 43 |
|
| 44 |
class timer:
|
|
@@ -237,3 +242,55 @@ def granular_similarity(metadata_a, metadata_b, threshold=80):
|
|
| 237 |
matches.append(best_match)
|
| 238 |
|
| 239 |
return matches
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 15 |
APP_NAME = "iscc-sct"
|
| 16 |
APP_AUTHOR = "iscc"
|
| 17 |
dirs = PlatformDirs(appname=APP_NAME, appauthor=APP_AUTHOR)
|
| 18 |
+
# ISCC_SCT_MODEL_DIR overrides the platform-specific model storage directory (issue #19).
|
| 19 |
+
# Read via os.environ (not SctOptions) because the path must be known at import time; a `.env`
|
| 20 |
+
# file still works because `iscc_sct.options` runs `load_dotenv()` before this module is imported.
|
| 21 |
+
MODEL_DIR = Path(os.environ.get("ISCC_SCT_MODEL_DIR") or dirs.user_data_dir)
|
| 22 |
+
os.makedirs(MODEL_DIR, exist_ok=True)
|
| 23 |
|
| 24 |
|
| 25 |
__all__ = [
|
|
|
|
| 33 |
"iscc_distance",
|
| 34 |
"cosine_similarity",
|
| 35 |
"granular_similarity",
|
| 36 |
+
"char_to_byte_offsets",
|
| 37 |
"MODEL_PATH",
|
| 38 |
]
|
| 39 |
|
| 40 |
|
| 41 |
BASE_VERSION = "1.0.0"
|
| 42 |
BASE_URL = f"https://github.com/iscc/iscc-binaries/releases/download/v{BASE_VERSION}"
|
| 43 |
+
MODEL_FILENAME = "iscc-sct-v0.2.0.onnx"
|
| 44 |
MODEL_URL = f"{BASE_URL}/{MODEL_FILENAME}"
|
| 45 |
+
MODEL_PATH = MODEL_DIR / MODEL_FILENAME
|
| 46 |
+
MODEL_CHECKSUM = "d4f763474f9be35ce5358637c87935ee11ba396b9196f73b95ab97da0cef8749"
|
| 47 |
|
| 48 |
|
| 49 |
class timer:
|
|
|
|
| 242 |
matches.append(best_match)
|
| 243 |
|
| 244 |
return matches
|
| 245 |
+
|
| 246 |
+
|
| 247 |
+
def char_to_byte_offsets(text, char_positions):
|
| 248 |
+
# type: (str, list[int]) -> list[int]
|
| 249 |
+
"""
|
| 250 |
+
Efficiently convert character positions to byte positions in a single pass.
|
| 251 |
+
|
| 252 |
+
:param text: The input text
|
| 253 |
+
:param char_positions: List of character positions to convert
|
| 254 |
+
:return: List of corresponding byte positions
|
| 255 |
+
"""
|
| 256 |
+
if not char_positions:
|
| 257 |
+
return []
|
| 258 |
+
|
| 259 |
+
# Sort positions for efficient single-pass processing
|
| 260 |
+
sorted_positions = sorted(set(char_positions))
|
| 261 |
+
pos_map = {pos: idx for idx, pos in enumerate(sorted_positions)}
|
| 262 |
+
byte_positions = [0] * len(sorted_positions)
|
| 263 |
+
|
| 264 |
+
char_pos = byte_pos = 0
|
| 265 |
+
pos_idx = 0
|
| 266 |
+
|
| 267 |
+
for ch in text:
|
| 268 |
+
if pos_idx < len(sorted_positions) and char_pos == sorted_positions[pos_idx]:
|
| 269 |
+
byte_positions[pos_idx] = byte_pos
|
| 270 |
+
pos_idx += 1
|
| 271 |
+
|
| 272 |
+
# Efficient branch-free UTF-8 byte length calculation
|
| 273 |
+
cp = ord(ch)
|
| 274 |
+
byte_pos += 1 + (cp >= 0x80) + (cp >= 0x800) + (cp >= 0x10000)
|
| 275 |
+
char_pos += 1
|
| 276 |
+
|
| 277 |
+
# After processing all characters, handle any requested position equal to len(text)
|
| 278 |
+
while pos_idx < len(sorted_positions) and sorted_positions[pos_idx] == char_pos:
|
| 279 |
+
byte_positions[pos_idx] = byte_pos
|
| 280 |
+
pos_idx += 1
|
| 281 |
+
|
| 282 |
+
# Map back to original order
|
| 283 |
+
return [byte_positions[pos_map[pos]] for pos in char_positions]
|
| 284 |
+
|
| 285 |
+
|
| 286 |
+
def char_to_byte_offsets_simple(text, char_positions):
|
| 287 |
+
# type: (str, list[int]) -> list[int]
|
| 288 |
+
"""
|
| 289 |
+
Simple implementation to convert character positions to byte offsets in a UTF-8 encoded string.
|
| 290 |
+
This function repeatedly encodes text slices, so its performance is not optimal for large texts.
|
| 291 |
+
|
| 292 |
+
:param text: The input text.
|
| 293 |
+
:param char_positions: List of character positions.
|
| 294 |
+
:return: List of corresponding byte positions.
|
| 295 |
+
"""
|
| 296 |
+
return [len(text[:pos].encode("utf-8")) for pos in char_positions]
|
poetry.lock
DELETED
|
The diff for this file is too large to render.
See raw diff
|
|
|
pyproject.toml
CHANGED
|
@@ -1,25 +1,28 @@
|
|
| 1 |
-
[
|
| 2 |
name = "iscc-sct"
|
| 3 |
-
version = "0.
|
| 4 |
description = "ISCC - Semantic Code Text"
|
| 5 |
-
authors = ["Titusz
|
| 6 |
-
|
| 7 |
readme = "README.md"
|
| 8 |
-
|
| 9 |
-
|
| 10 |
-
|
| 11 |
-
|
| 12 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 13 |
"Development Status :: 4 - Beta",
|
| 14 |
"Intended Audience :: Developers",
|
| 15 |
"Intended Audience :: Science/Research",
|
| 16 |
-
"License :: OSI Approved :: Apache Software License",
|
| 17 |
"Natural Language :: English",
|
| 18 |
"Operating System :: OS Independent",
|
| 19 |
-
"Programming Language :: Python :: 3.9",
|
| 20 |
-
"Programming Language :: Python :: 3.10",
|
| 21 |
"Programming Language :: Python :: 3.11",
|
| 22 |
"Programming Language :: Python :: 3.12",
|
|
|
|
|
|
|
| 23 |
"Topic :: Text Processing",
|
| 24 |
"Topic :: Text Processing :: General",
|
| 25 |
"Topic :: Text Processing :: Indexing",
|
|
@@ -30,75 +33,117 @@ classifiers=[
|
|
| 30 |
"Topic :: Scientific/Engineering :: Information Analysis",
|
| 31 |
"Topic :: Software Development :: Libraries :: Python Modules",
|
| 32 |
"Topic :: Software Development :: Libraries",
|
| 33 |
-
"Topic :: Software Development :: Libraries :: Python Modules",
|
| 34 |
"Topic :: System :: Archiving",
|
| 35 |
"Topic :: System :: Clustering",
|
| 36 |
"Topic :: System :: Distributed Computing",
|
| 37 |
]
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 38 |
|
| 39 |
-
|
| 40 |
-
|
| 41 |
-
|
| 42 |
-
|
| 43 |
-
|
|
|
|
| 44 |
|
| 45 |
-
[
|
| 46 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 47 |
|
| 48 |
-
[
|
| 49 |
-
|
| 50 |
-
semantic-text-splitter = "*"
|
| 51 |
-
onnxruntime = "*"
|
| 52 |
-
onnxruntime-gpu = { version = "*", optional = true }
|
| 53 |
-
loguru = "*"
|
| 54 |
-
blake3 = "*"
|
| 55 |
-
platformdirs = "*"
|
| 56 |
-
tokenizers = "*"
|
| 57 |
-
pydantic-settings = "*"
|
| 58 |
-
charset-normalizer = "*"
|
| 59 |
-
numpy = "<2.0.0"
|
| 60 |
-
pybase64 = "*"
|
| 61 |
-
certifi = ">=2024.07.04"
|
| 62 |
-
gradio = { version = "*", optional = true }
|
| 63 |
-
pyyaml = "*"
|
| 64 |
-
pydantic = "*"
|
| 65 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 66 |
|
| 67 |
-
[tool.
|
| 68 |
-
|
| 69 |
-
|
|
|
|
|
|
|
|
|
|
| 70 |
|
| 71 |
-
[tool.
|
| 72 |
-
|
| 73 |
-
coverage = "*"
|
| 74 |
-
pytest-cov = "*"
|
| 75 |
|
| 76 |
-
[
|
| 77 |
-
|
| 78 |
-
|
| 79 |
-
mdformat-gfm = "*"
|
| 80 |
-
mdformat-gfm-alerts = "*"
|
| 81 |
-
mdformat-frontmatter = "*"
|
| 82 |
|
| 83 |
[tool.ruff]
|
| 84 |
line-length = 100
|
| 85 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 86 |
[tool.ruff.format]
|
| 87 |
line-ending = "lf"
|
| 88 |
|
| 89 |
[tool.coverage.run]
|
| 90 |
omit = ["iscc_sct/dev.py", "tests/", "iscc_sct/demo.py"]
|
|
|
|
| 91 |
|
| 92 |
[tool.poe.tasks]
|
| 93 |
format-code = { cmd = "ruff format", help = "Code style formating with ruff" }
|
| 94 |
-
format-markdown = { cmd = "mdformat --wrap 100 --end-of-line lf README.md", help = "Markdown formating with mdformat" }
|
| 95 |
format-yml = { script = "iscc_sct.dev:format_yml", help = "Format YML files"}
|
| 96 |
convert-lf = { script = "iscc_sct.dev:convert_lf", help = "Convert line endings to LF"}
|
| 97 |
-
|
| 98 |
-
|
| 99 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 100 |
update = ["update-dependencies", "all"]
|
| 101 |
-
|
| 102 |
-
[build-system]
|
| 103 |
-
requires = ["poetry-core>=1.0.0"]
|
| 104 |
-
build-backend = "poetry.core.masonry.api"
|
|
|
|
| 1 |
+
[project]
|
| 2 |
name = "iscc-sct"
|
| 3 |
+
version = "0.2.2"
|
| 4 |
description = "ISCC - Semantic Code Text"
|
| 5 |
+
authors = [{ name = "Titusz", email = "tp@piscc.io" }]
|
| 6 |
+
requires-python = ">=3.11"
|
| 7 |
readme = "README.md"
|
| 8 |
+
license = "Apache-2.0"
|
| 9 |
+
license-files = ["LICENSE"]
|
| 10 |
+
keywords = [
|
| 11 |
+
"iscc",
|
| 12 |
+
"text similarity",
|
| 13 |
+
"cross lingual",
|
| 14 |
+
"semantic similarity",
|
| 15 |
+
]
|
| 16 |
+
classifiers = [
|
| 17 |
"Development Status :: 4 - Beta",
|
| 18 |
"Intended Audience :: Developers",
|
| 19 |
"Intended Audience :: Science/Research",
|
|
|
|
| 20 |
"Natural Language :: English",
|
| 21 |
"Operating System :: OS Independent",
|
|
|
|
|
|
|
| 22 |
"Programming Language :: Python :: 3.11",
|
| 23 |
"Programming Language :: Python :: 3.12",
|
| 24 |
+
"Programming Language :: Python :: 3.13",
|
| 25 |
+
"Programming Language :: Python :: 3.14",
|
| 26 |
"Topic :: Text Processing",
|
| 27 |
"Topic :: Text Processing :: General",
|
| 28 |
"Topic :: Text Processing :: Indexing",
|
|
|
|
| 33 |
"Topic :: Scientific/Engineering :: Information Analysis",
|
| 34 |
"Topic :: Software Development :: Libraries :: Python Modules",
|
| 35 |
"Topic :: Software Development :: Libraries",
|
|
|
|
| 36 |
"Topic :: System :: Archiving",
|
| 37 |
"Topic :: System :: Clustering",
|
| 38 |
"Topic :: System :: Distributed Computing",
|
| 39 |
]
|
| 40 |
+
dependencies = [
|
| 41 |
+
# >=0.32.0 ships the upstream "avoid sizing whole distant split sections" fix and releases
|
| 42 |
+
# the GIL during native chunking. It mitigates - but does not eliminate - the issue #24
|
| 43 |
+
# super-linear cost on separator-free spans, so the chunking guard below is retained.
|
| 44 |
+
"semantic-text-splitter>=0.32.0",
|
| 45 |
+
"loguru",
|
| 46 |
+
"blake3",
|
| 47 |
+
"platformdirs",
|
| 48 |
+
"tokenizers",
|
| 49 |
+
"pydantic-settings",
|
| 50 |
+
"charset-normalizer",
|
| 51 |
+
"numpy",
|
| 52 |
+
"pybase64",
|
| 53 |
+
"certifi",
|
| 54 |
+
"pyyaml",
|
| 55 |
+
"pydantic",
|
| 56 |
+
]
|
| 57 |
|
| 58 |
+
# The onnxruntime variant wheels unpack into the same site-packages directory and clobber each
|
| 59 |
+
# other, so exactly one of the cpu/gpu extras must be installed (see issue #23).
|
| 60 |
+
[project.optional-dependencies]
|
| 61 |
+
cpu = ["onnxruntime"]
|
| 62 |
+
gpu = ["onnxruntime-gpu"]
|
| 63 |
+
demo = ["gradio"]
|
| 64 |
|
| 65 |
+
[project.urls]
|
| 66 |
+
Homepage = "https://iscc.codes"
|
| 67 |
+
Repository = "https://github.com/iscc/iscc-sct"
|
| 68 |
+
Documentation = "https://github.com/iscc/iscc-sct"
|
| 69 |
+
Changelog = "https://github.com/iscc/iscc-sct/blob/main/CHANGELOG.md"
|
| 70 |
+
"Bug Tracker" = "https://github.com/iscc/iscc-sct/issues"
|
| 71 |
+
Twitter = "https://twitter.com/iscc_foundation"
|
| 72 |
+
Donate = "https://iscc.foundation/support"
|
| 73 |
|
| 74 |
+
[project.scripts]
|
| 75 |
+
iscc-sct = "iscc_sct.cli:main"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 76 |
|
| 77 |
+
[dependency-groups]
|
| 78 |
+
test = [
|
| 79 |
+
"pytest",
|
| 80 |
+
"coverage",
|
| 81 |
+
"pytest-cov",
|
| 82 |
+
"onnxruntime", # dev/test environments use CPU inference
|
| 83 |
+
]
|
| 84 |
+
dev = [
|
| 85 |
+
"poethepoet",
|
| 86 |
+
"ruff",
|
| 87 |
+
"mdformat-mkdocs[recommended]",
|
| 88 |
+
"mdformat-gfm-alerts",
|
| 89 |
+
"prek>=0.4.4",
|
| 90 |
+
"zensical>=0.0.21",
|
| 91 |
+
"mkdocstrings-python>=1.16.0",
|
| 92 |
+
]
|
| 93 |
|
| 94 |
+
[tool.uv]
|
| 95 |
+
default-groups = "all"
|
| 96 |
+
conflicts = [
|
| 97 |
+
[{ extra = "cpu" }, { extra = "gpu" }],
|
| 98 |
+
[{ group = "test" }, { extra = "gpu" }],
|
| 99 |
+
]
|
| 100 |
|
| 101 |
+
[tool.uv.build-backend]
|
| 102 |
+
module-root = ""
|
|
|
|
|
|
|
| 103 |
|
| 104 |
+
[build-system]
|
| 105 |
+
requires = ["uv_build>=0.11.20,<0.12.0"]
|
| 106 |
+
build-backend = "uv_build"
|
|
|
|
|
|
|
|
|
|
| 107 |
|
| 108 |
[tool.ruff]
|
| 109 |
line-length = 100
|
| 110 |
|
| 111 |
+
[tool.ruff.lint]
|
| 112 |
+
# F401: imports referenced only in PEP 484 type comments are invisible to ruff
|
| 113 |
+
ignore = ["F401"]
|
| 114 |
+
|
| 115 |
+
[tool.ruff.lint.per-file-ignores]
|
| 116 |
+
# F403: star imports intentionally re-export the public API
|
| 117 |
+
"iscc_sct/__init__.py" = ["F403"]
|
| 118 |
+
|
| 119 |
[tool.ruff.format]
|
| 120 |
line-ending = "lf"
|
| 121 |
|
| 122 |
[tool.coverage.run]
|
| 123 |
omit = ["iscc_sct/dev.py", "tests/", "iscc_sct/demo.py"]
|
| 124 |
+
patch = ["subprocess"]
|
| 125 |
|
| 126 |
[tool.poe.tasks]
|
| 127 |
format-code = { cmd = "ruff format", help = "Code style formating with ruff" }
|
| 128 |
+
format-markdown = { cmd = "mdformat --wrap 100 --end-of-line lf --exclude docs/reference/api.md README.md CHANGELOG.md docs", help = "Markdown formating with mdformat" }
|
| 129 |
format-yml = { script = "iscc_sct.dev:format_yml", help = "Format YML files"}
|
| 130 |
convert-lf = { script = "iscc_sct.dev:convert_lf", help = "Convert line endings to LF"}
|
| 131 |
+
# Erase any stale .coverage.* parallel data files left by an aborted run before measuring, so the
|
| 132 |
+
# 100% gate never inherits orphaned subprocess data (patch=["subprocess"]) and report a spurious
|
| 133 |
+
# <100% (issue: leftover .coverage.GOLEM.pid* files + failing coverage gate).
|
| 134 |
+
test.sequence = [
|
| 135 |
+
{ cmd = "coverage erase" },
|
| 136 |
+
{ cmd = "pytest --cov=iscc_sct --cov-fail-under=100" },
|
| 137 |
+
]
|
| 138 |
+
test.help = "Run tests with coverage"
|
| 139 |
+
docs-serve = { cmd = "zensical serve", help = "Serve the documentation site locally" }
|
| 140 |
+
docs-build.sequence = [
|
| 141 |
+
{ cmd = "zensical build" },
|
| 142 |
+
{ cmd = "python scripts/gen_llms_full.py" },
|
| 143 |
+
]
|
| 144 |
+
docs-build.help = "Build the docs site and generate llms-full.txt"
|
| 145 |
+
precommit = { cmd = "uv run prek run", help = "Run prek hooks on changed files" }
|
| 146 |
+
export-requirements = { cmd = "uv export --no-default-groups --extra cpu --no-emit-project --no-hashes --no-annotate --no-header --output-file requirements.txt", help = "Export requirements.txt from uv.lock for HF Space" }
|
| 147 |
+
update-dependencies = { cmd = "uv sync --upgrade --extra demo", help = "Update dependencies" }
|
| 148 |
+
all = ["format-code", "format-markdown", "format-yml", "convert-lf", "export-requirements", "test"]
|
| 149 |
update = ["update-dependencies", "all"]
|
|
|
|
|
|
|
|
|
|
|
|
requirements.txt
CHANGED
|
@@ -1,11 +1,35 @@
|
|
| 1 |
-
|
| 2 |
-
|
| 3 |
-
|
| 4 |
-
|
| 5 |
-
|
| 6 |
-
|
| 7 |
-
|
| 8 |
-
|
| 9 |
-
|
| 10 |
-
|
| 11 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
annotated-types==0.8.0
|
| 2 |
+
anyio==4.14.2
|
| 3 |
+
blake3==1.0.9
|
| 4 |
+
certifi==2026.7.22
|
| 5 |
+
charset-normalizer==3.4.9
|
| 6 |
+
click==8.4.2
|
| 7 |
+
colorama==0.4.6 ; sys_platform == 'win32'
|
| 8 |
+
filelock==3.32.2
|
| 9 |
+
flatbuffers==25.12.19
|
| 10 |
+
fsspec==2026.7.0
|
| 11 |
+
h11==0.16.0
|
| 12 |
+
hf-xet==1.5.2 ; platform_machine == 'AMD64' or platform_machine == 'aarch64' or platform_machine == 'amd64' or platform_machine == 'arm64' or platform_machine == 'x86_64'
|
| 13 |
+
httpcore==1.0.9
|
| 14 |
+
httpx==0.28.1
|
| 15 |
+
huggingface-hub==1.26.0
|
| 16 |
+
idna==3.18
|
| 17 |
+
loguru==0.7.3
|
| 18 |
+
numpy==2.4.6 ; python_full_version < '3.12'
|
| 19 |
+
numpy==2.5.1 ; python_full_version >= '3.12'
|
| 20 |
+
onnxruntime==1.28.0
|
| 21 |
+
packaging==26.2
|
| 22 |
+
platformdirs==4.11.0
|
| 23 |
+
protobuf==7.35.1
|
| 24 |
+
pybase64==1.4.3
|
| 25 |
+
pydantic==2.13.4
|
| 26 |
+
pydantic-core==2.46.4
|
| 27 |
+
pydantic-settings==2.14.2
|
| 28 |
+
python-dotenv==1.2.2
|
| 29 |
+
pyyaml==6.0.3
|
| 30 |
+
semantic-text-splitter==0.32.0
|
| 31 |
+
tokenizers==0.23.1
|
| 32 |
+
tqdm==4.70.0
|
| 33 |
+
typing-extensions==4.16.0
|
| 34 |
+
typing-inspection==0.4.2
|
| 35 |
+
win32-setctime==1.2.0 ; sys_platform == 'win32'
|
scripts/gen_llms_full.py
ADDED
|
@@ -0,0 +1,106 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Generate llms-full.txt and per-page .md files for LLM consumption.
|
| 2 |
+
|
| 3 |
+
Runs after `zensical build` (see the `docs-build` poe task). Copies cleaned Markdown
|
| 4 |
+
(frontmatter and abbreviation snippets stripped) into site/ alongside the rendered HTML,
|
| 5 |
+
and concatenates every page into site/llms-full.txt.
|
| 6 |
+
|
| 7 |
+
The API reference page (`reference/api.md`) is all mkdocstrings `:::` directives that only
|
| 8 |
+
render via the plugin, so its plain-text export is a pointer to the rendered page and the
|
| 9 |
+
For Coding Agents API map rather than the raw directives.
|
| 10 |
+
"""
|
| 11 |
+
|
| 12 |
+
import re
|
| 13 |
+
from pathlib import Path
|
| 14 |
+
|
| 15 |
+
DOCS_DIR = Path(__file__).parent.parent / "docs"
|
| 16 |
+
SITE_DIR = Path(__file__).parent.parent / "site"
|
| 17 |
+
|
| 18 |
+
# Ordered list of doc pages to include (relative to docs/), matching the zensical.toml nav
|
| 19 |
+
PAGES = [
|
| 20 |
+
"index.md",
|
| 21 |
+
"tutorials/getting-started.md",
|
| 22 |
+
"howto/compare-texts.md",
|
| 23 |
+
"howto/granular-features.md",
|
| 24 |
+
"howto/configuration.md",
|
| 25 |
+
"howto/command-line.md",
|
| 26 |
+
"explanation/how-it-works.md",
|
| 27 |
+
"reference/api.md",
|
| 28 |
+
"reference/for-coding-agents.md",
|
| 29 |
+
]
|
| 30 |
+
|
| 31 |
+
# Page whose source is mkdocstrings directives; exported as a pointer instead of raw markdown.
|
| 32 |
+
API_PAGE = "reference/api.md"
|
| 33 |
+
API_POINTER = """# API reference
|
| 34 |
+
|
| 35 |
+
The API reference is generated from source docstrings and rendered with full signatures at
|
| 36 |
+
https://sct.iscc.codes/reference/api/.
|
| 37 |
+
|
| 38 |
+
For a plain-text API map of every public symbol - its source module and purpose - see the
|
| 39 |
+
For Coding Agents page (in this file and at https://sct.iscc.codes/reference/for-coding-agents/).
|
| 40 |
+
"""
|
| 41 |
+
|
| 42 |
+
# Regex to strip YAML frontmatter
|
| 43 |
+
FRONTMATTER_RE = re.compile(r"\A---\n.*?\n---\n", re.DOTALL)
|
| 44 |
+
|
| 45 |
+
# Regex to strip snippet auto-append directives
|
| 46 |
+
SNIPPET_RE = re.compile(r"^\*\[.*?\]:.*$", re.MULTILINE)
|
| 47 |
+
|
| 48 |
+
|
| 49 |
+
def strip_frontmatter(content):
|
| 50 |
+
# type: (str) -> str
|
| 51 |
+
"""Remove YAML frontmatter from markdown content."""
|
| 52 |
+
return FRONTMATTER_RE.sub("", content)
|
| 53 |
+
|
| 54 |
+
|
| 55 |
+
def strip_snippets(content):
|
| 56 |
+
# type: (str) -> str
|
| 57 |
+
"""Remove abbreviation snippet definitions appended by pymdownx.snippets."""
|
| 58 |
+
return SNIPPET_RE.sub("", content)
|
| 59 |
+
|
| 60 |
+
|
| 61 |
+
def clean_content(content):
|
| 62 |
+
# type: (str) -> str
|
| 63 |
+
"""Strip frontmatter, snippets, and normalize whitespace."""
|
| 64 |
+
content = strip_frontmatter(content)
|
| 65 |
+
content = strip_snippets(content)
|
| 66 |
+
return content.strip()
|
| 67 |
+
|
| 68 |
+
|
| 69 |
+
def page_content(page):
|
| 70 |
+
# type: (str) -> str
|
| 71 |
+
"""Return the cleaned export markdown for a page, or the pointer for the API page."""
|
| 72 |
+
if page == API_PAGE:
|
| 73 |
+
return API_POINTER.strip()
|
| 74 |
+
return clean_content((DOCS_DIR / page).read_text(encoding="utf-8"))
|
| 75 |
+
|
| 76 |
+
|
| 77 |
+
def main():
|
| 78 |
+
# type: () -> None
|
| 79 |
+
"""Generate llms-full.txt and individual .md files from doc sources."""
|
| 80 |
+
SITE_DIR.mkdir(parents=True, exist_ok=True)
|
| 81 |
+
parts = []
|
| 82 |
+
|
| 83 |
+
for page in PAGES:
|
| 84 |
+
if not (DOCS_DIR / page).exists():
|
| 85 |
+
print(f"Warning: {page} not found, skipping")
|
| 86 |
+
continue
|
| 87 |
+
content = page_content(page)
|
| 88 |
+
if not content:
|
| 89 |
+
continue
|
| 90 |
+
parts.append(content)
|
| 91 |
+
|
| 92 |
+
# Write individual .md file to site directory
|
| 93 |
+
md_path = SITE_DIR / page
|
| 94 |
+
md_path.parent.mkdir(parents=True, exist_ok=True)
|
| 95 |
+
md_path.write_text(content + "\n", encoding="utf-8", newline="")
|
| 96 |
+
|
| 97 |
+
# Write concatenated llms-full.txt
|
| 98 |
+
output = "\n\n---\n\n".join(parts) + "\n"
|
| 99 |
+
out_path = SITE_DIR / "llms-full.txt"
|
| 100 |
+
out_path.write_text(output, encoding="utf-8", newline="")
|
| 101 |
+
print(f"Generated {out_path} ({len(parts)} pages, {len(output)} bytes)")
|
| 102 |
+
print(f"Generated {len(parts)} individual .md files in {SITE_DIR}")
|
| 103 |
+
|
| 104 |
+
|
| 105 |
+
if __name__ == "__main__":
|
| 106 |
+
main()
|
space.yml
CHANGED
|
@@ -3,7 +3,7 @@ emoji: 🔮
|
|
| 3 |
colorFrom: red
|
| 4 |
colorTo: blue
|
| 5 |
sdk: gradio
|
| 6 |
-
sdk_version:
|
| 7 |
pinned: true
|
| 8 |
license: CC-BY-NC-SA-4.0
|
| 9 |
short_description: Cross Lingual Similarity Preserving Text Simprints
|
|
|
|
| 3 |
colorFrom: red
|
| 4 |
colorTo: blue
|
| 5 |
sdk: gradio
|
| 6 |
+
sdk_version: 5.26.0
|
| 7 |
pinned: true
|
| 8 |
license: CC-BY-NC-SA-4.0
|
| 9 |
short_description: Cross Lingual Similarity Preserving Text Simprints
|
tests/chunking_vectors.json
ADDED
|
@@ -0,0 +1,153 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"en-default": {
|
| 3 |
+
"options": {},
|
| 4 |
+
"text_sha256": "e10a7003e5625c3ff9ec7b400b5d244b6a66a88c1d123f2a255fed5eb8aca648",
|
| 5 |
+
"offsets": "0 277 612 1006 1224 1491 1743 2043 2334 2648 3183 3462 3845 4146 4411 4653 4939 5305 5629 5875 6313 6627 6851 7197 7504 7907 8157 8354 8745 8963 9331 9668 10033 10229 10425 10783 11199 11378 11663",
|
| 6 |
+
"sizes": "440 396 431 385 440 380 406 477 415 536 280 449 446 442 443 444 451 485 477 439 517 430 468 394 531 448 421 503 376 403 513 477 393 375 555 533 312 455 413",
|
| 7 |
+
"chunks_sha256": "363f9a4173919b37d74842c9b4ddaefcd966db3ccb360bd7ca06af1ce9ab272e"
|
| 8 |
+
},
|
| 9 |
+
"de-default": {
|
| 10 |
+
"options": {},
|
| 11 |
+
"text_sha256": "1fcf71ab8716ba3fd44eb0a8d46c9a58833a1cc2839dfa16244fab7e9dd676e6",
|
| 12 |
+
"offsets": "0 393 889 1162 1441 1807 2162 2427 2828 3205 3530 3941 4314 4696 5048 5415 5804 6041 6351 6704 7065 7475 7786 8086 8485 8860 9221 9500 10033 10417 10748 11244 11669 12033",
|
| 13 |
+
"sizes": "546 497 474 476 496 437 448 544 494 326 540 567 527 524 518 546 402 453 565 559 554 392 419 577 599 569 516 534 545 552 560 555 563 606",
|
| 14 |
+
"chunks_sha256": "1c55035883d3172edec4af391785d3cdcebd04408160e5287a3af5e45e6169a7"
|
| 15 |
+
},
|
| 16 |
+
"en-trim": {
|
| 17 |
+
"options": {
|
| 18 |
+
"trim": true
|
| 19 |
+
},
|
| 20 |
+
"text_sha256": "e10a7003e5625c3ff9ec7b400b5d244b6a66a88c1d123f2a255fed5eb8aca648",
|
| 21 |
+
"offsets": "2 279 614 1008 1226 1493 1745 2045 2336 2653 3185 3465 3847 4148 4413 4655 4941 5307 5631 5880 6315 6629 6853 7199 7506 7909 8159 8356 8747 8965 9333 9670 10035 10230 10427 10785 11201 11380 11665",
|
| 22 |
+
"sizes": "437 393 428 382 437 377 403 474 410 530 277 445 443 439 440 441 448 482 474 433 514 427 465 391 528 445 418 500 373 400 510 474 390 373 552 530 309 452 410",
|
| 23 |
+
"chunks_sha256": "b5c0939c79f227eebbca578b7cba7d869a743667b8bc05866ccfb856ab84eceb"
|
| 24 |
+
},
|
| 25 |
+
"de-overlap0": {
|
| 26 |
+
"options": {
|
| 27 |
+
"overlap": 0
|
| 28 |
+
},
|
| 29 |
+
"text_sha256": "1fcf71ab8716ba3fd44eb0a8d46c9a58833a1cc2839dfa16244fab7e9dd676e6",
|
| 30 |
+
"offsets": "0 546 1066 1572 1994 2515 3039 3531 4070 4634 5220 5679 6206 6777 7314 7836 8354 8912 9468 10034 10578 11032 11513 12036",
|
| 31 |
+
"sizes": "546 520 506 422 521 524 492 539 564 586 459 527 571 537 522 518 558 556 566 544 454 481 523 603",
|
| 32 |
+
"chunks_sha256": "5c9f00e67136002b0d66bb1dc5e1e2fa5045d58365e3fc92ce79ab677ad52bac"
|
| 33 |
+
},
|
| 34 |
+
"de-small-chunks": {
|
| 35 |
+
"options": {
|
| 36 |
+
"max_tokens": 32,
|
| 37 |
+
"overlap": 16
|
| 38 |
+
},
|
| 39 |
+
"text_sha256": "1fcf71ab8716ba3fd44eb0a8d46c9a58833a1cc2839dfa16244fab7e9dd676e6",
|
| 40 |
+
"offsets": "0 121 266 393 473 545 638 695 758 821 889 984 1063 1104 1152 1209 1286 1362 1441 1519 1638 1713 1807 1881 1994 2059 2162 2241 2309 2427 2514 2607 2690 2828 2970 3039 3105 3205 3322 3392 3458 3531 3603 3668 3755 3829 3941 4069 4201 4230 4314 4401 4482 4507 4633 4696 4805 4908 5048 5163 5219 5317 5415 5497 5565 5678 5805 5875 5960 6041 6080 6206 6275 6351 6494 6548 6674 6736 6776 6915 7013 7103 7262 7314 7369 7475 7619 7680 7786 7867 7954 8024 8086 8205 8272 8353 8485 8633 8699 8784 8912 8986 9083 9221 9323 9428 9500 9576 9737 9820 9903 10034 10103 10247 10331 10417 10502 10652 10748 10860 11032 11126 11244 11386 11513 11590 11669 11753 11881 11941 12036 12113 12206 12231 12384 12440 12584",
|
| 41 |
+
"sizes": "124 146 128 81 73 94 57 118 129 95 96 121 102 106 137 105 77 80 131 119 147 95 75 113 129 104 82 69 121 88 96 84 139 143 69 147 101 117 137 137 73 138 124 87 148 113 129 133 114 171 171 107 80 127 97 145 149 141 116 57 99 99 111 120 114 127 137 86 123 81 126 127 77 143 142 157 103 100 140 151 91 160 52 125 107 144 140 107 81 154 124 63 119 130 82 133 149 66 160 128 170 141 139 132 145 72 162 161 167 156 131 154 145 85 123 132 151 97 115 172 154 119 143 127 152 80 130 131 61 95 144 119 93 154 85 161 55",
|
| 42 |
+
"chunks_sha256": "84e6a9a44d117a455236f42e4e59ea61c978d15155cfc8437e4b8e6389e8c9ca"
|
| 43 |
+
},
|
| 44 |
+
"en-byte-offsets": {
|
| 45 |
+
"options": {
|
| 46 |
+
"byte_offsets": true
|
| 47 |
+
},
|
| 48 |
+
"text_sha256": "e10a7003e5625c3ff9ec7b400b5d244b6a66a88c1d123f2a255fed5eb8aca648",
|
| 49 |
+
"offsets": "0 277 612 1006 1224 1491 1743 2043 2334 2648 3183 3462 3845 4146 4411 4653 4939 5305 5629 5875 6313 6627 6851 7197 7504 7907 8157 8354 8745 8963 9331 9668 10033 10229 10425 10783 11199 11378 11663",
|
| 50 |
+
"sizes": "440 396 431 385 440 380 406 477 415 536 280 449 446 442 443 444 451 485 477 439 517 430 468 394 531 448 421 503 376 403 513 477 393 375 555 533 312 455 413",
|
| 51 |
+
"chunks_sha256": "363f9a4173919b37d74842c9b4ddaefcd966db3ccb360bd7ca06af1ce9ab272e"
|
| 52 |
+
},
|
| 53 |
+
"en-pathological": {
|
| 54 |
+
"options": {},
|
| 55 |
+
"text_sha256": "61a9478a65146e5f1df1aef776a3c5869ace10f1fdb4507a5c85f93e1d311999",
|
| 56 |
+
"offsets": "0 277 612 1006 1224 1491 1743 2043 2334 2648 3183 3462 3845 4146 4411 4653 4939 5305 5629 5875 6313 6627 6851 7197 7504 7907 8157 8354 8745 8963 9331 9668 10033 10229 10425 10783 11199 11378 11663",
|
| 57 |
+
"sizes": "440 396 431 385 440 380 406 477 415 536 280 449 446 442 443 444 451 485 477 439 517 430 468 394 531 448 421 503 376 403 513 477 393 375 555 533 312 455 420",
|
| 58 |
+
"chunks_sha256": "bdf3103cde11b6ef81652e31910e334e461fe6b720028d1960f3ca7ba4d5a596"
|
| 59 |
+
},
|
| 60 |
+
"de-pathological": {
|
| 61 |
+
"options": {},
|
| 62 |
+
"text_sha256": "55a001750306eaeac5aec38ef431db29fafa93aae3aba5af1f787ccf06b4ae73",
|
| 63 |
+
"offsets": "0 393 889 1162 1441 1807 2162 2427 2828 3205 3530 3941 4314 4696 5048 5415 5804 6041 6351 6704 7065 7475 7786 8086 8485 8860 9221 9500 10033 10417 10748 11244 11669 12033 12638",
|
| 64 |
+
"sizes": "546 497 474 476 496 437 448 544 494 326 540 567 527 524 518 546 402 453 565 559 554 392 419 577 599 569 516 534 545 552 560 555 563 608 8",
|
| 65 |
+
"chunks_sha256": "da6b12e7e9b878088e886a1f77ca8cb9eb6a2875d9df0af98b5bdc04021a46ce"
|
| 66 |
+
},
|
| 67 |
+
"de-pathological-trim": {
|
| 68 |
+
"options": {
|
| 69 |
+
"trim": true
|
| 70 |
+
},
|
| 71 |
+
"text_sha256": "55a001750306eaeac5aec38ef431db29fafa93aae3aba5af1f787ccf06b4ae73",
|
| 72 |
+
"offsets": "2 395 891 1167 1443 1809 2164 2431 2831 3208 3533 3943 4317 4698 5050 5417 5806 6043 6353 6706 7067 7477 7788 8088 8487 8862 9223 9501 10035 10422 10750 11246 11671 12037 12641",
|
| 73 |
+
"sizes": "543 494 471 468 493 432 443 539 488 322 536 564 521 521 515 543 397 450 562 556 549 389 416 574 596 566 513 532 542 546 557 552 560 601 5",
|
| 74 |
+
"chunks_sha256": "a5528fe8d172d8818101215117b4b958611fd1a3dda04525a1e2550711e24b81"
|
| 75 |
+
},
|
| 76 |
+
"en-collapsed-no-breaks": {
|
| 77 |
+
"options": {},
|
| 78 |
+
"text_sha256": "e10a7003e5625c3ff9ec7b400b5d244b6a66a88c1d123f2a255fed5eb8aca648",
|
| 79 |
+
"offsets": "0 277 612 1006 1224 1491 1743 2043 2334 2648 3183 3462 3845 4146 4411 4653 4939 5305 5629 5875 6313 6627 6851 7197 7504 7907 8157 8354 8745 8963 9331 9668 10033 10229 10425 10783 11199 11378 11663",
|
| 80 |
+
"sizes": "440 396 431 385 440 380 406 477 415 536 280 449 446 442 443 444 451 485 477 439 517 430 468 394 531 448 421 503 376 403 513 477 393 375 555 533 312 455 413",
|
| 81 |
+
"chunks_sha256": "363f9a4173919b37d74842c9b4ddaefcd966db3ccb360bd7ca06af1ce9ab272e"
|
| 82 |
+
},
|
| 83 |
+
"en-crlf": {
|
| 84 |
+
"options": {},
|
| 85 |
+
"text_sha256": "f7a1aa8aeb6f97fd89cb85aca27dbf778cba13012073111c1f6dcc899971a8e3",
|
| 86 |
+
"offsets": "0 280 620 1018 1241 1512 1766 2069 2362 2679 3221 3503 3889 4194 4461 4708 4998 5368 5698 5946 6391 6713 6944 7293 7604 8011 8264 8464 8858 9080 9453 9792 10160 10361 10558 10918 11339 11521 11808",
|
| 87 |
+
"sizes": "447 403 437 394 446 385 411 482 423 544 284 454 452 449 452 451 458 493 486 447 532 440 475 401 537 455 427 510 385 410 517 483 400 378 561 541 317 461 423",
|
| 88 |
+
"chunks_sha256": "8ddddf9437056739f6856c2b4b14815032fc88b75b32aad94bc9e34b54fa3433"
|
| 89 |
+
},
|
| 90 |
+
"de-multi-blank": {
|
| 91 |
+
"options": {},
|
| 92 |
+
"text_sha256": "1fcf71ab8716ba3fd44eb0a8d46c9a58833a1cc2839dfa16244fab7e9dd676e6",
|
| 93 |
+
"offsets": "0 393 889 1162 1441 1807 2162 2427 2828 3205 3530 3941 4314 4696 5048 5415 5804 6041 6351 6704 7065 7475 7786 8086 8485 8860 9221 9500 10033 10417 10748 11244 11669 12033",
|
| 94 |
+
"sizes": "546 497 474 476 496 437 448 544 494 326 540 567 527 524 518 546 402 453 565 559 554 392 419 577 599 569 516 534 545 552 560 555 563 606",
|
| 95 |
+
"chunks_sha256": "1c55035883d3172edec4af391785d3cdcebd04408160e5287a3af5e45e6169a7"
|
| 96 |
+
},
|
| 97 |
+
"cjk-pathological": {
|
| 98 |
+
"options": {},
|
| 99 |
+
"text_sha256": "188538c93773ba43c5c399dfa60460e74dfe69328fc3ebf15799adea2bb4093e",
|
| 100 |
+
"offsets": "0 132 264 396 528 660 792 924 1056 1188 1320 1452 1584 1716 1848 1980 2112 2244 2376 2508 2640 2772 2904 3036 3168 3300 3432 3564 3696 3828 3960 4092 4224 4356 4488 4620 4752 4884 5016 5148 5280 5412 5544 5676 5808 5940 6072 6204 6336 6468 6600 6732 6864 6996 7128 7260 7392 7524 7656 7788 7920 8052 8184 8316 8448 8580 8712 8844 8976 9108 9240 9372 9504 9636 9768 9900 10032 10164 10296 10428 10560 10692 10824",
|
| 101 |
+
"sizes": "198 198 198 198 198 198 198 198 198 198 198 198 198 198 198 198 198 198 198 198 198 198 198 198 198 198 198 198 198 198 198 198 198 198 198 198 198 198 198 198 198 198 198 198 198 198 198 198 198 198 198 198 198 198 198 198 198 198 198 198 198 198 198 198 198 198 198 198 198 198 198 198 198 198 198 198 198 198 198 198 198 198 179",
|
| 102 |
+
"chunks_sha256": "71502f8844d62063ac8b843e59bc4ff98aa8bf543c0ac16304fb5bfccffb714f"
|
| 103 |
+
},
|
| 104 |
+
"long-word-pathological": {
|
| 105 |
+
"options": {},
|
| 106 |
+
"text_sha256": "e92803ea4acfdda6a33324b409f8425506a12edfc403d42476c44586f8302e9f",
|
| 107 |
+
"offsets": "0 304 608 912 1216 1520 1824 2128 2432 2736 3040 3344 3648 3952 4256 4560 4864 5168 5472 5776 6080 6384 6688 6992 7296 7600 7904 8208 8512 8816 9120 9424 9728 10032 10336 10640 10944",
|
| 108 |
+
"sizes": "483 483 483 483 483 483 483 483 483 483 483 483 483 483 483 483 483 483 483 483 483 483 483 483 483 483 483 483 483 483 483 483 483 483 483 483 468",
|
| 109 |
+
"chunks_sha256": "f69abfad67650c9087ecce78e097c3cab72390eefe1c027860f51e0f05ad97c0"
|
| 110 |
+
},
|
| 111 |
+
"unk-runs-pathological": {
|
| 112 |
+
"options": {},
|
| 113 |
+
"text_sha256": "61ee460db1d82bf3def0e0588b66c20875edecfbf89e4016ba2c429df41698a9",
|
| 114 |
+
"offsets": "0 3939 7878",
|
| 115 |
+
"sizes": "6363 6363 4249",
|
| 116 |
+
"chunks_sha256": "967815a4207392ec55c33d2dda9a79432c79cc75d0111d66a67dd85df8380f16"
|
| 117 |
+
},
|
| 118 |
+
"nbsp-pathological": {
|
| 119 |
+
"options": {},
|
| 120 |
+
"text_sha256": "a0eef59ac4ac26fb0a8c3125739d2727e72dfb56f966a2a83c6df4f44eef5c50",
|
| 121 |
+
"offsets": "0 553 1106 1659 2212 2765 3318 3871 4424 4977 5530 6083 6636 7189 7742 8295 8848 9401 9954 10507 11060 11613 12166 12719 13272 13825 14378 14931 15484 16037 16590 17143 17696 18249 18802 19355 19908 20461 21014 21567 22120 22673 23226 23779 24332 24885 25438 25991 26544 27097 27650",
|
| 122 |
+
"sizes": "889 889 889 889 889 889 889 889 889 889 889 889 889 889 889 889 889 889 889 889 889 889 889 889 889 889 889 889 889 889 889 889 889 889 889 889 889 889 889 889 889 889 889 889 889 889 889 889 889 889 356",
|
| 123 |
+
"chunks_sha256": "a31a1e947114f456e7530f8f107d4fb7e76e36d68f3d268c201311e606fe2a6c"
|
| 124 |
+
},
|
| 125 |
+
"mixed-level-pathological": {
|
| 126 |
+
"options": {},
|
| 127 |
+
"text_sha256": "9ae546991aedeff306af6bb68537de7dd39c60d519497963859130d68d312039",
|
| 128 |
+
"offsets": "0 452 906 1360 1814 2268 2722 3176 3630 4084 4538 4992 5446 5900 6354 6808 7262 7716 8170 8624 9078 9532 9986 10440 10894 11348 11802 12256 12710 13164 13618 14072 14526 14980 15434 15888 16342 16796 17250 17704 18158 18612 19066 19520 19974 20428 20882 21336 21790 22244",
|
| 129 |
+
"sizes": "454 456 456 456 456 456 456 456 456 456 456 456 456 456 456 456 456 456 456 456 456 456 456 456 456 456 456 456 456 456 456 456 456 456 456 456 456 456 456 456 456 456 456 456 456 456 456 456 456 463",
|
| 130 |
+
"chunks_sha256": "122f9cfc24b1c0a43a29bb411f964204fc6bc218d764aa76da9d3ccec7194841"
|
| 131 |
+
},
|
| 132 |
+
"whitespace-only": {
|
| 133 |
+
"options": {},
|
| 134 |
+
"text_sha256": "25ffbca1db7cf0e7f6f5e5146829f328b761a5d4dec01a5de1b522f88f309ad4",
|
| 135 |
+
"offsets": "0",
|
| 136 |
+
"sizes": "11",
|
| 137 |
+
"chunks_sha256": "25ffbca1db7cf0e7f6f5e5146829f328b761a5d4dec01a5de1b522f88f309ad4"
|
| 138 |
+
},
|
| 139 |
+
"tiny": {
|
| 140 |
+
"options": {},
|
| 141 |
+
"text_sha256": "e3b7d16a6eb31eebba3412b159991654121183f95c16de64139f1a762c8e9595",
|
| 142 |
+
"offsets": "0",
|
| 143 |
+
"sizes": "41",
|
| 144 |
+
"chunks_sha256": "e3b7d16a6eb31eebba3412b159991654121183f95c16de64139f1a762c8e9595"
|
| 145 |
+
},
|
| 146 |
+
"unicode-mix": {
|
| 147 |
+
"options": {},
|
| 148 |
+
"text_sha256": "e83cf7d12f7514899f21be066f858d23c094b27c60ef4ff836553604df5fbd6f",
|
| 149 |
+
"offsets": "0",
|
| 150 |
+
"sizes": "34",
|
| 151 |
+
"chunks_sha256": "e83cf7d12f7514899f21be066f858d23c094b27c60ef4ff836553604df5fbd6f"
|
| 152 |
+
}
|
| 153 |
+
}
|
tests/test_chunking_vectors.py
ADDED
|
@@ -0,0 +1,150 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# -*- coding: utf-8 -*-
|
| 2 |
+
"""Chunking test vectors that freeze the exact `split_text` boundaries.
|
| 3 |
+
|
| 4 |
+
Chunk boundaries determine granular simprints and the final Semantic-Code, so any change to
|
| 5 |
+
the chunking pipeline (text-splitter version, tokenizer, chunk sizer implementation) must
|
| 6 |
+
reproduce these vectors bit-exactly to remain backward compatible.
|
| 7 |
+
|
| 8 |
+
The vectors in `chunking_vectors.json` pin chunk offsets and sizes for a variety of text
|
| 9 |
+
shapes (including the pathological PDF-extraction shape from issue #24) and option
|
| 10 |
+
combinations. Texts are built deterministically from the committed fixtures `en.txt` and
|
| 11 |
+
`de.txt` plus small synthetic literals; a sha256 of each input text guards against drift
|
| 12 |
+
between the vector file and the text construction below.
|
| 13 |
+
|
| 14 |
+
Regenerate the vector file (ONLY for an intentional, versioned chunking algorithm change):
|
| 15 |
+
|
| 16 |
+
uv run python tests/test_chunking_vectors.py
|
| 17 |
+
"""
|
| 18 |
+
|
| 19 |
+
import hashlib
|
| 20 |
+
import json
|
| 21 |
+
import re
|
| 22 |
+
from pathlib import Path
|
| 23 |
+
|
| 24 |
+
import pytest
|
| 25 |
+
|
| 26 |
+
from iscc_sct.code_semantic_text import split_text
|
| 27 |
+
|
| 28 |
+
|
| 29 |
+
HERE = Path(__file__).parent.absolute()
|
| 30 |
+
VECTORS_PATH = HERE / "chunking_vectors.json"
|
| 31 |
+
|
| 32 |
+
|
| 33 |
+
def collapse_newlines(text):
|
| 34 |
+
# type: (str) -> str
|
| 35 |
+
"""Collapse all newline runs to single newlines (PDF-extraction-like text shape)."""
|
| 36 |
+
return re.sub(r"[\r\n]+", "\n", text)
|
| 37 |
+
|
| 38 |
+
|
| 39 |
+
def pathological(text):
|
| 40 |
+
# type: (str) -> str
|
| 41 |
+
"""
|
| 42 |
+
Apply the issue #24 trigger shape to text.
|
| 43 |
+
|
| 44 |
+
No blank lines anywhere except a single trailing paragraph break. This makes the
|
| 45 |
+
text-splitter probe from each chunk position all the way to the distant trailing
|
| 46 |
+
separator, which exposed the super-linear chunking behavior.
|
| 47 |
+
"""
|
| 48 |
+
return collapse_newlines(text) + "\n\nEnde."
|
| 49 |
+
|
| 50 |
+
|
| 51 |
+
def vector_cases():
|
| 52 |
+
# type: () -> dict[str, tuple[str, dict]]
|
| 53 |
+
"""Build all vector cases as {name: (text, split_text options)}."""
|
| 54 |
+
en = (HERE / "en.txt").read_text(encoding="utf-8")
|
| 55 |
+
de = (HERE / "de.txt").read_text(encoding="utf-8")
|
| 56 |
+
cjk = "数据是新的石油它推动着现代经济的发展与变革。" * 500
|
| 57 |
+
long_word = "hypermodularization" * 600
|
| 58 |
+
unk_runs = ("𓀀" * 100 + " ") * 120
|
| 59 |
+
level3 = ("Ein kurzer Absatz über die Dinge des Lebens. " * 5 + "\n\n") * 100 + "\n\nEnde."
|
| 60 |
+
# PDF-extraction shape whose words are separated by NBSP (U+00A0) instead of ASCII space;
|
| 61 |
+
# routed to the guarded splitter and exercises its Unicode-whitespace cut search.
|
| 62 |
+
nbsp = (chr(0xA0).join(["Inhalt"] * 4000)) + "\n\nEnde."
|
| 63 |
+
return {
|
| 64 |
+
"en-default": (en, {}),
|
| 65 |
+
"de-default": (de, {}),
|
| 66 |
+
"en-trim": (en, {"trim": True}),
|
| 67 |
+
"de-overlap0": (de, {"overlap": 0}),
|
| 68 |
+
"de-small-chunks": (de, {"max_tokens": 32, "overlap": 16}),
|
| 69 |
+
"en-byte-offsets": (en, {"byte_offsets": True}),
|
| 70 |
+
"en-pathological": (pathological(en), {}),
|
| 71 |
+
"de-pathological": (pathological(de), {}),
|
| 72 |
+
"de-pathological-trim": (pathological(de), {"trim": True}),
|
| 73 |
+
"en-collapsed-no-breaks": (collapse_newlines(en), {}),
|
| 74 |
+
"en-crlf": (en.replace("\n", "\r\n"), {}),
|
| 75 |
+
"de-multi-blank": (de.replace("\n\n", "\n\n\n\n"), {}),
|
| 76 |
+
"cjk-pathological": (cjk + "\n\n完", {}),
|
| 77 |
+
"long-word-pathological": (long_word + " Ende\n\nEnde.", {}),
|
| 78 |
+
"unk-runs-pathological": (unk_runs + "\n\nEnde.", {}),
|
| 79 |
+
"nbsp-pathological": (nbsp, {}),
|
| 80 |
+
"mixed-level-pathological": (level3, {}),
|
| 81 |
+
"whitespace-only": (" \t \n\n ", {}),
|
| 82 |
+
"tiny": ("Hello, World! Schöne Grüße aus München. 😀", {}),
|
| 83 |
+
"unicode-mix": (
|
| 84 |
+
"Café naïve 😀🎉 سلام z̧álgo\n\nEnde.",
|
| 85 |
+
{},
|
| 86 |
+
),
|
| 87 |
+
}
|
| 88 |
+
|
| 89 |
+
|
| 90 |
+
def text_hash(text):
|
| 91 |
+
# type: (str) -> str
|
| 92 |
+
"""Return the sha256 hex digest of text encoded as UTF-8."""
|
| 93 |
+
return hashlib.sha256(text.encode("utf-8")).hexdigest()
|
| 94 |
+
|
| 95 |
+
|
| 96 |
+
def chunks_hash(chunks):
|
| 97 |
+
# type: (list[str]) -> str
|
| 98 |
+
"""Return the sha256 hex digest over all chunk contents."""
|
| 99 |
+
return hashlib.sha256("\x1f".join(chunks).encode("utf-8")).hexdigest()
|
| 100 |
+
|
| 101 |
+
|
| 102 |
+
def load_vectors():
|
| 103 |
+
# type: () -> dict
|
| 104 |
+
"""Load the frozen chunking vectors from chunking_vectors.json."""
|
| 105 |
+
with VECTORS_PATH.open(encoding="utf-8") as f:
|
| 106 |
+
return json.load(f)
|
| 107 |
+
|
| 108 |
+
|
| 109 |
+
def build_vectors():
|
| 110 |
+
# type: () -> dict
|
| 111 |
+
"""Compute chunking vectors for all cases with the current implementation."""
|
| 112 |
+
vectors = {}
|
| 113 |
+
for name, (text, options) in vector_cases().items():
|
| 114 |
+
result = split_text(text, **options)
|
| 115 |
+
offsets = [offset for offset, _ in result]
|
| 116 |
+
sizes = [len(chunk) for _, chunk in result]
|
| 117 |
+
vectors[name] = {
|
| 118 |
+
"options": options,
|
| 119 |
+
"text_sha256": text_hash(text),
|
| 120 |
+
"offsets": " ".join(str(o) for o in offsets),
|
| 121 |
+
"sizes": " ".join(str(s) for s in sizes),
|
| 122 |
+
"chunks_sha256": chunks_hash([chunk for _, chunk in result]),
|
| 123 |
+
}
|
| 124 |
+
return vectors
|
| 125 |
+
|
| 126 |
+
|
| 127 |
+
def test_vector_file_covers_all_cases():
|
| 128 |
+
assert set(load_vectors()) == set(vector_cases())
|
| 129 |
+
|
| 130 |
+
|
| 131 |
+
@pytest.mark.parametrize("name", vector_cases())
|
| 132 |
+
def test_chunking_vector(name):
|
| 133 |
+
text, options = vector_cases()[name]
|
| 134 |
+
expected = load_vectors()[name]
|
| 135 |
+
assert text_hash(text) == expected["text_sha256"], "input text construction drifted"
|
| 136 |
+
result = split_text(text, **options)
|
| 137 |
+
offsets = " ".join(str(offset) for offset, _ in result)
|
| 138 |
+
sizes = " ".join(str(len(chunk)) for _, chunk in result)
|
| 139 |
+
assert offsets == expected["offsets"]
|
| 140 |
+
assert sizes == expected["sizes"]
|
| 141 |
+
assert chunks_hash([chunk for _, chunk in result]) == expected["chunks_sha256"]
|
| 142 |
+
|
| 143 |
+
|
| 144 |
+
if __name__ == "__main__": # pragma: no cover
|
| 145 |
+
VECTORS_PATH.write_text(
|
| 146 |
+
json.dumps(build_vectors(), indent=2, ensure_ascii=False) + "\n",
|
| 147 |
+
encoding="utf-8",
|
| 148 |
+
newline="\n",
|
| 149 |
+
)
|
| 150 |
+
print(f"Wrote {len(build_vectors())} vectors to {VECTORS_PATH}")
|
tests/test_cli.py
CHANGED
|
@@ -1,8 +1,11 @@
|
|
| 1 |
import subprocess
|
|
|
|
| 2 |
import pytest
|
| 3 |
-
import shutil
|
| 4 |
|
| 5 |
-
|
|
|
|
|
|
|
|
|
|
| 6 |
|
| 7 |
|
| 8 |
@pytest.fixture
|
|
@@ -27,39 +30,55 @@ def non_utf8_text_file(tmp_path):
|
|
| 27 |
|
| 28 |
|
| 29 |
def test_cli_no_args():
|
| 30 |
-
result = subprocess.run(
|
| 31 |
assert result.returncode == 0
|
| 32 |
assert "Generate Semantic" in result.stdout
|
| 33 |
|
| 34 |
|
| 35 |
def test_cli_empty_file(empty_text_file):
|
| 36 |
-
result = subprocess.run(
|
|
|
|
|
|
|
| 37 |
assert result.returncode == 0
|
| 38 |
assert "SKIPPED" in result.stderr
|
| 39 |
|
| 40 |
|
| 41 |
def test_cli_non_utf8_file(non_utf8_text_file):
|
| 42 |
-
result = subprocess.run(
|
|
|
|
|
|
|
| 43 |
assert result.returncode == 0
|
| 44 |
assert "Could not decode" in result.stderr
|
| 45 |
assert "ISCC:" in result.stdout
|
| 46 |
|
| 47 |
|
| 48 |
def test_cli_generate_sct(sample_text_file):
|
| 49 |
-
result = subprocess.run(
|
|
|
|
|
|
|
| 50 |
assert result.returncode == 0
|
| 51 |
assert "ISCC:" in result.stdout
|
| 52 |
|
| 53 |
|
| 54 |
def test_cli_generate_sct_granular(sample_text_file):
|
| 55 |
result = subprocess.run(
|
| 56 |
-
[
|
| 57 |
)
|
| 58 |
assert result.returncode == 0
|
| 59 |
assert "features" in result.stdout
|
| 60 |
|
| 61 |
|
| 62 |
def test_cli_debug_mode(sample_text_file):
|
| 63 |
-
result = subprocess.run(
|
|
|
|
|
|
|
| 64 |
assert result.returncode == 0
|
| 65 |
assert "DEBUG" in result.stderr
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
import subprocess
|
| 2 |
+
import sys
|
| 3 |
import pytest
|
|
|
|
| 4 |
|
| 5 |
+
|
| 6 |
+
def get_sct_command():
|
| 7 |
+
"""Get the command to run the SCT CLI"""
|
| 8 |
+
return [sys.executable, "-m", "iscc_sct.cli"]
|
| 9 |
|
| 10 |
|
| 11 |
@pytest.fixture
|
|
|
|
| 30 |
|
| 31 |
|
| 32 |
def test_cli_no_args():
|
| 33 |
+
result = subprocess.run(get_sct_command(), capture_output=True, text=True)
|
| 34 |
assert result.returncode == 0
|
| 35 |
assert "Generate Semantic" in result.stdout
|
| 36 |
|
| 37 |
|
| 38 |
def test_cli_empty_file(empty_text_file):
|
| 39 |
+
result = subprocess.run(
|
| 40 |
+
[*get_sct_command(), str(empty_text_file), "-d"], capture_output=True, text=True
|
| 41 |
+
)
|
| 42 |
assert result.returncode == 0
|
| 43 |
assert "SKIPPED" in result.stderr
|
| 44 |
|
| 45 |
|
| 46 |
def test_cli_non_utf8_file(non_utf8_text_file):
|
| 47 |
+
result = subprocess.run(
|
| 48 |
+
[*get_sct_command(), str(non_utf8_text_file), "-d"], capture_output=True, text=True
|
| 49 |
+
)
|
| 50 |
assert result.returncode == 0
|
| 51 |
assert "Could not decode" in result.stderr
|
| 52 |
assert "ISCC:" in result.stdout
|
| 53 |
|
| 54 |
|
| 55 |
def test_cli_generate_sct(sample_text_file):
|
| 56 |
+
result = subprocess.run(
|
| 57 |
+
[*get_sct_command(), str(sample_text_file)], capture_output=True, text=True
|
| 58 |
+
)
|
| 59 |
assert result.returncode == 0
|
| 60 |
assert "ISCC:" in result.stdout
|
| 61 |
|
| 62 |
|
| 63 |
def test_cli_generate_sct_granular(sample_text_file):
|
| 64 |
result = subprocess.run(
|
| 65 |
+
[*get_sct_command(), str(sample_text_file), "--granular"], capture_output=True, text=True
|
| 66 |
)
|
| 67 |
assert result.returncode == 0
|
| 68 |
assert "features" in result.stdout
|
| 69 |
|
| 70 |
|
| 71 |
def test_cli_debug_mode(sample_text_file):
|
| 72 |
+
result = subprocess.run(
|
| 73 |
+
[*get_sct_command(), str(sample_text_file), "--debug"], capture_output=True, text=True
|
| 74 |
+
)
|
| 75 |
assert result.returncode == 0
|
| 76 |
assert "DEBUG" in result.stderr
|
| 77 |
+
|
| 78 |
+
|
| 79 |
+
def test_cli_doctor():
|
| 80 |
+
# The test environment has a working CPU runtime, so doctor reports OK and exits cleanly
|
| 81 |
+
# without prompting (no stdin, no pip call).
|
| 82 |
+
result = subprocess.run([*get_sct_command(), "doctor"], capture_output=True, text=True)
|
| 83 |
+
assert result.returncode == 0
|
| 84 |
+
assert "ONNX runtime" in result.stdout
|
tests/test_doctor.py
ADDED
|
@@ -0,0 +1,246 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Tests for the iscc-sct doctor command (ONNX runtime diagnostics and opt-in repair)."""
|
| 2 |
+
|
| 3 |
+
import builtins
|
| 4 |
+
|
| 5 |
+
from iscc_sct import doctor
|
| 6 |
+
|
| 7 |
+
|
| 8 |
+
def _report(providers, gpu_dist=False, gpu_present=False, cpu_dist=False):
|
| 9 |
+
"""Build a diagnosis report from raw facts."""
|
| 10 |
+
return doctor._diagnose(providers, gpu_dist, gpu_present, cpu_dist)
|
| 11 |
+
|
| 12 |
+
|
| 13 |
+
# --- detection helpers ---
|
| 14 |
+
|
| 15 |
+
|
| 16 |
+
def test_dist_installed_true():
|
| 17 |
+
# onnxruntime is present in the test environment via the `test` dependency group
|
| 18 |
+
assert doctor._dist_installed("onnxruntime") is True
|
| 19 |
+
|
| 20 |
+
|
| 21 |
+
def test_dist_installed_false():
|
| 22 |
+
assert doctor._dist_installed("definitely-not-a-real-distribution-xyz") is False
|
| 23 |
+
|
| 24 |
+
|
| 25 |
+
def test_onnx_providers_returns_list():
|
| 26 |
+
providers = doctor._onnx_providers()
|
| 27 |
+
assert isinstance(providers, list)
|
| 28 |
+
assert "CPUExecutionProvider" in providers
|
| 29 |
+
|
| 30 |
+
|
| 31 |
+
def test_onnx_providers_missing(monkeypatch):
|
| 32 |
+
def _raise(name):
|
| 33 |
+
raise ImportError(name)
|
| 34 |
+
|
| 35 |
+
monkeypatch.setattr(doctor.importlib, "import_module", _raise)
|
| 36 |
+
assert doctor._onnx_providers() is None
|
| 37 |
+
|
| 38 |
+
|
| 39 |
+
def test_gpu_present_returns_bool():
|
| 40 |
+
assert isinstance(doctor._gpu_present(), bool)
|
| 41 |
+
|
| 42 |
+
|
| 43 |
+
def test_doctor_report_returns_dict():
|
| 44 |
+
report = doctor.doctor_report()
|
| 45 |
+
assert report["status"] in {"ok", "missing", "shadowed"}
|
| 46 |
+
|
| 47 |
+
|
| 48 |
+
# --- pure diagnosis ---
|
| 49 |
+
|
| 50 |
+
|
| 51 |
+
def test_diagnose_missing_cpu():
|
| 52 |
+
report = _report(None)
|
| 53 |
+
assert report["status"] == "missing"
|
| 54 |
+
assert report["extra"] == "cpu"
|
| 55 |
+
assert report["fix"] == [["install", "iscc-sct[cpu]"]]
|
| 56 |
+
assert report["hint"] is False
|
| 57 |
+
|
| 58 |
+
|
| 59 |
+
def test_diagnose_missing_gpu():
|
| 60 |
+
report = _report(None, gpu_present=True)
|
| 61 |
+
assert report["status"] == "missing"
|
| 62 |
+
assert report["extra"] == "gpu"
|
| 63 |
+
assert report["fix"] == [["install", "iscc-sct[gpu]"]]
|
| 64 |
+
|
| 65 |
+
|
| 66 |
+
def test_diagnose_shadowed():
|
| 67 |
+
# Genuine clobber: BOTH onnxruntime and onnxruntime-gpu installed, CUDA disabled.
|
| 68 |
+
report = _report(["CPUExecutionProvider"], gpu_dist=True, gpu_present=True, cpu_dist=True)
|
| 69 |
+
assert report["status"] == "shadowed"
|
| 70 |
+
assert report["fix"][0][0] == "uninstall"
|
| 71 |
+
assert report["fix"][1] == ["install", "--force-reinstall", "iscc-sct[gpu]"]
|
| 72 |
+
|
| 73 |
+
|
| 74 |
+
def test_diagnose_no_cuda_cpu_only_host():
|
| 75 |
+
# onnxruntime-gpu correctly installed, no CUDA, nothing shadowing it (CPU-only host).
|
| 76 |
+
# Not "shadowed": reinstalling cannot help an environment problem, so no fix is offered.
|
| 77 |
+
report = _report(["CPUExecutionProvider"], gpu_dist=True, gpu_present=False)
|
| 78 |
+
assert report["status"] == "no_cuda"
|
| 79 |
+
assert report["fix"] == []
|
| 80 |
+
|
| 81 |
+
|
| 82 |
+
def test_diagnose_no_cuda_with_gpu_present():
|
| 83 |
+
# NVIDIA GPU detected but CUDA unavailable (driver/library issue), gpu package not shadowed.
|
| 84 |
+
report = _report(["CPUExecutionProvider"], gpu_dist=True, gpu_present=True)
|
| 85 |
+
assert report["status"] == "no_cuda"
|
| 86 |
+
assert report["fix"] == []
|
| 87 |
+
|
| 88 |
+
|
| 89 |
+
def test_diagnose_broken_gpu_import_is_missing():
|
| 90 |
+
# onnxruntime-gpu installed but un-importable (providers=None) is "missing", not "shadowed".
|
| 91 |
+
report = _report(None, gpu_dist=True)
|
| 92 |
+
assert report["status"] == "missing"
|
| 93 |
+
assert report["fix"] == [["install", "iscc-sct[cpu]"]]
|
| 94 |
+
|
| 95 |
+
|
| 96 |
+
def test_diagnose_ok_cuda():
|
| 97 |
+
report = _report(["CUDAExecutionProvider", "CPUExecutionProvider"], gpu_dist=True)
|
| 98 |
+
assert report["status"] == "ok"
|
| 99 |
+
assert report["cuda"] is True
|
| 100 |
+
assert report["hint"] is False
|
| 101 |
+
|
| 102 |
+
|
| 103 |
+
def test_diagnose_ok_cpu_no_gpu():
|
| 104 |
+
report = _report(["CPUExecutionProvider"])
|
| 105 |
+
assert report["status"] == "ok"
|
| 106 |
+
assert report["hint"] is False
|
| 107 |
+
|
| 108 |
+
|
| 109 |
+
def test_diagnose_ok_hint_for_gpu_without_runtime():
|
| 110 |
+
report = _report(["CPUExecutionProvider"], gpu_present=True)
|
| 111 |
+
assert report["status"] == "ok"
|
| 112 |
+
assert report["hint"] is True
|
| 113 |
+
|
| 114 |
+
|
| 115 |
+
# --- formatting ---
|
| 116 |
+
|
| 117 |
+
|
| 118 |
+
def test_format_report_ok():
|
| 119 |
+
text = doctor.format_report(_report(["CPUExecutionProvider"]))
|
| 120 |
+
assert "OK" in text
|
| 121 |
+
assert "onnxruntime" in text
|
| 122 |
+
|
| 123 |
+
|
| 124 |
+
def test_format_report_hint():
|
| 125 |
+
text = doctor.format_report(_report(["CPUExecutionProvider"], gpu_present=True))
|
| 126 |
+
assert "CUDA acceleration" in text
|
| 127 |
+
|
| 128 |
+
|
| 129 |
+
def test_format_report_missing():
|
| 130 |
+
text = doctor.format_report(_report(None))
|
| 131 |
+
assert "no ONNX runtime" in text
|
| 132 |
+
assert "not installed" in text
|
| 133 |
+
assert "iscc-sct[cpu]" in text
|
| 134 |
+
|
| 135 |
+
|
| 136 |
+
def test_format_report_shadowed():
|
| 137 |
+
text = doctor.format_report(
|
| 138 |
+
_report(["CPUExecutionProvider"], gpu_dist=True, gpu_present=True, cpu_dist=True)
|
| 139 |
+
)
|
| 140 |
+
assert "shadowed" in text
|
| 141 |
+
assert "onnxruntime-gpu" in text
|
| 142 |
+
assert "conflicting" in text # _runtime_label names both installed packages
|
| 143 |
+
|
| 144 |
+
|
| 145 |
+
def test_format_report_no_cuda():
|
| 146 |
+
text = doctor.format_report(_report(["CPUExecutionProvider"], gpu_dist=True))
|
| 147 |
+
assert "CUDA is unavailable" in text
|
| 148 |
+
assert "environment issue" in text
|
| 149 |
+
assert "onnxruntime-gpu" in text
|
| 150 |
+
|
| 151 |
+
|
| 152 |
+
# --- confirmation ---
|
| 153 |
+
|
| 154 |
+
|
| 155 |
+
def test_confirm_assume_yes():
|
| 156 |
+
assert doctor._confirm(True) is True
|
| 157 |
+
|
| 158 |
+
|
| 159 |
+
def test_confirm_yes(monkeypatch):
|
| 160 |
+
monkeypatch.setattr(builtins, "input", lambda prompt="": "Y")
|
| 161 |
+
assert doctor._confirm(False) is True
|
| 162 |
+
|
| 163 |
+
|
| 164 |
+
def test_confirm_no(monkeypatch):
|
| 165 |
+
monkeypatch.setattr(builtins, "input", lambda prompt="": "")
|
| 166 |
+
assert doctor._confirm(False) is False
|
| 167 |
+
|
| 168 |
+
|
| 169 |
+
def test_confirm_eof(monkeypatch):
|
| 170 |
+
def _raise(prompt=""):
|
| 171 |
+
raise EOFError
|
| 172 |
+
|
| 173 |
+
monkeypatch.setattr(builtins, "input", _raise)
|
| 174 |
+
assert doctor._confirm(False) is False
|
| 175 |
+
|
| 176 |
+
|
| 177 |
+
# --- run_doctor orchestration ---
|
| 178 |
+
|
| 179 |
+
|
| 180 |
+
class _Completed:
|
| 181 |
+
def __init__(self, returncode):
|
| 182 |
+
self.returncode = returncode
|
| 183 |
+
|
| 184 |
+
|
| 185 |
+
def _fake_runner(calls, returncode=0):
|
| 186 |
+
def _run(cmd, *args, **kwargs):
|
| 187 |
+
calls.append(cmd)
|
| 188 |
+
return _Completed(returncode)
|
| 189 |
+
|
| 190 |
+
return _run
|
| 191 |
+
|
| 192 |
+
|
| 193 |
+
def test_run_doctor_ok(monkeypatch):
|
| 194 |
+
monkeypatch.setattr(doctor, "doctor_report", lambda: _report(["CPUExecutionProvider"]))
|
| 195 |
+
calls = []
|
| 196 |
+
monkeypatch.setattr(doctor.subprocess, "run", _fake_runner(calls))
|
| 197 |
+
assert doctor.run_doctor() == 0
|
| 198 |
+
assert calls == [] # no pip call for a healthy runtime
|
| 199 |
+
|
| 200 |
+
|
| 201 |
+
def test_run_doctor_missing_assume_yes(monkeypatch):
|
| 202 |
+
monkeypatch.setattr(doctor, "doctor_report", lambda: _report(None))
|
| 203 |
+
calls = []
|
| 204 |
+
monkeypatch.setattr(doctor.subprocess, "run", _fake_runner(calls, returncode=0))
|
| 205 |
+
assert doctor.run_doctor(assume_yes=True) == 0
|
| 206 |
+
assert len(calls) == 1
|
| 207 |
+
assert calls[0][-1] == "iscc-sct[cpu]"
|
| 208 |
+
|
| 209 |
+
|
| 210 |
+
def test_run_doctor_decline(monkeypatch):
|
| 211 |
+
monkeypatch.setattr(doctor, "doctor_report", lambda: _report(None))
|
| 212 |
+
monkeypatch.setattr(builtins, "input", lambda prompt="": "n")
|
| 213 |
+
calls = []
|
| 214 |
+
monkeypatch.setattr(doctor.subprocess, "run", _fake_runner(calls))
|
| 215 |
+
assert doctor.run_doctor() == 1
|
| 216 |
+
assert calls == []
|
| 217 |
+
|
| 218 |
+
|
| 219 |
+
def test_run_doctor_shadowed_runs_two_commands(monkeypatch):
|
| 220 |
+
monkeypatch.setattr(
|
| 221 |
+
doctor,
|
| 222 |
+
"doctor_report",
|
| 223 |
+
lambda: _report(["CPUExecutionProvider"], gpu_dist=True, cpu_dist=True),
|
| 224 |
+
)
|
| 225 |
+
calls = []
|
| 226 |
+
monkeypatch.setattr(doctor.subprocess, "run", _fake_runner(calls, returncode=0))
|
| 227 |
+
assert doctor.run_doctor(assume_yes=True) == 0
|
| 228 |
+
assert len(calls) == 2
|
| 229 |
+
|
| 230 |
+
|
| 231 |
+
def test_run_doctor_no_cuda(monkeypatch):
|
| 232 |
+
# no_cuda has no actionable fix: print and exit 0 without prompting or pip calls.
|
| 233 |
+
monkeypatch.setattr(
|
| 234 |
+
doctor, "doctor_report", lambda: _report(["CPUExecutionProvider"], gpu_dist=True)
|
| 235 |
+
)
|
| 236 |
+
calls = []
|
| 237 |
+
monkeypatch.setattr(doctor.subprocess, "run", _fake_runner(calls))
|
| 238 |
+
assert doctor.run_doctor() == 0
|
| 239 |
+
assert calls == []
|
| 240 |
+
|
| 241 |
+
|
| 242 |
+
def test_run_doctor_fix_failure(monkeypatch):
|
| 243 |
+
monkeypatch.setattr(doctor, "doctor_report", lambda: _report(None))
|
| 244 |
+
calls = []
|
| 245 |
+
monkeypatch.setattr(doctor.subprocess, "run", _fake_runner(calls, returncode=1))
|
| 246 |
+
assert doctor.run_doctor(assume_yes=True) == 1
|
tests/test_gen_llms_full.py
ADDED
|
@@ -0,0 +1,49 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Tests for the docs llms-full.txt generation helpers in scripts/gen_llms_full.py."""
|
| 2 |
+
|
| 3 |
+
import importlib.util
|
| 4 |
+
from pathlib import Path
|
| 5 |
+
|
| 6 |
+
SCRIPT = Path(__file__).parent.parent / "scripts" / "gen_llms_full.py"
|
| 7 |
+
|
| 8 |
+
|
| 9 |
+
def _load_module():
|
| 10 |
+
# type: () -> object
|
| 11 |
+
"""Load the gen_llms_full script as a module by file path."""
|
| 12 |
+
spec = importlib.util.spec_from_file_location("gen_llms_full", SCRIPT)
|
| 13 |
+
module = importlib.util.module_from_spec(spec)
|
| 14 |
+
spec.loader.exec_module(module)
|
| 15 |
+
return module
|
| 16 |
+
|
| 17 |
+
|
| 18 |
+
gen = _load_module()
|
| 19 |
+
|
| 20 |
+
|
| 21 |
+
def test_clean_content_strips_frontmatter_and_snippets():
|
| 22 |
+
content = (
|
| 23 |
+
"---\nicon: lucide/code\ndescription: x\n---\n"
|
| 24 |
+
"# Title\n\n"
|
| 25 |
+
"Body paragraph.\n\n"
|
| 26 |
+
"*[ISCC]: International Standard Content Code\n"
|
| 27 |
+
)
|
| 28 |
+
result = gen.clean_content(content)
|
| 29 |
+
assert result.startswith("# Title")
|
| 30 |
+
assert "icon:" not in result
|
| 31 |
+
assert "*[ISCC]:" not in result
|
| 32 |
+
assert "Body paragraph." in result
|
| 33 |
+
|
| 34 |
+
|
| 35 |
+
def test_api_page_exports_pointer_not_directives():
|
| 36 |
+
# The API page is mkdocstrings directives; its export must be the pointer, never raw `:::`.
|
| 37 |
+
assert gen.API_PAGE in gen.PAGES
|
| 38 |
+
pointer = gen.page_content(gen.API_PAGE)
|
| 39 |
+
assert ":::" not in pointer
|
| 40 |
+
assert "reference/api/" in pointer
|
| 41 |
+
assert "reference/for-coding-agents/" in pointer
|
| 42 |
+
|
| 43 |
+
|
| 44 |
+
def test_page_content_cleans_normal_page():
|
| 45 |
+
page = "explanation/how-it-works.md"
|
| 46 |
+
src = gen.DOCS_DIR / page
|
| 47 |
+
result = gen.page_content(page)
|
| 48 |
+
assert result == gen.clean_content(src.read_text(encoding="utf-8"))
|
| 49 |
+
assert not result.startswith("---")
|
tests/test_iscc_sct.py
CHANGED
|
@@ -1,9 +1,11 @@
|
|
|
|
|
| 1 |
from pathlib import Path
|
| 2 |
|
| 3 |
import pytest
|
| 4 |
from blake3 import blake3
|
| 5 |
|
| 6 |
import iscc_sct as sct
|
|
|
|
| 7 |
from iscc_sct.code_semantic_text import (
|
| 8 |
split_text,
|
| 9 |
tokenize_chunks,
|
|
@@ -31,7 +33,8 @@ be matched based on lexical similarity.
|
|
| 31 |
|
| 32 |
|
| 33 |
def test_version():
|
| 34 |
-
|
|
|
|
| 35 |
|
| 36 |
|
| 37 |
def test_code_text_semantic_default():
|
|
@@ -104,44 +107,42 @@ def test_gen_text_code_semantic_granular():
|
|
| 104 |
offsets=True,
|
| 105 |
contents=True,
|
| 106 |
)
|
| 107 |
-
assert
|
| 108 |
-
|
| 109 |
-
|
| 110 |
-
|
| 111 |
-
|
| 112 |
-
|
| 113 |
-
|
| 114 |
-
|
| 115 |
-
|
| 116 |
-
|
| 117 |
-
|
| 118 |
-
|
| 119 |
-
"
|
| 120 |
-
|
| 121 |
-
|
| 122 |
-
|
| 123 |
-
|
| 124 |
-
|
| 125 |
-
|
| 126 |
-
|
| 127 |
-
|
| 128 |
-
|
| 129 |
-
|
| 130 |
-
|
| 131 |
-
|
| 132 |
-
|
| 133 |
-
|
| 134 |
-
|
| 135 |
-
|
| 136 |
-
|
| 137 |
-
|
| 138 |
-
|
| 139 |
-
|
| 140 |
-
|
| 141 |
-
|
| 142 |
-
|
| 143 |
-
}
|
| 144 |
-
)
|
| 145 |
|
| 146 |
|
| 147 |
def test_gen_text_code_semantic_checks_bits():
|
|
@@ -156,15 +157,107 @@ def test_split_text(text_en):
|
|
| 156 |
|
| 157 |
|
| 158 |
def test_split_text_override():
|
| 159 |
-
text = "Try some very small and granular text splitting. Use options override for it."
|
| 160 |
chunks = split_text(text, max_tokens=8, overlap=4)
|
| 161 |
assert chunks == [
|
| 162 |
(0, "Try some very small and granular text "),
|
| 163 |
-
(20, "and granular text splitting
|
| 164 |
-
(
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 165 |
]
|
| 166 |
|
| 167 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 168 |
def test_tokenize_chunks():
|
| 169 |
chunks = ["Hello World", "These are chunks"]
|
| 170 |
result = tokenize_chunks(chunks)
|
|
@@ -190,6 +283,73 @@ def test_embed_chunks():
|
|
| 190 |
assert list(embeddings[0][:3]) == pytest.approx(expected, rel=1e-3)
|
| 191 |
|
| 192 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 193 |
def test_gen_text_code_semantic(text_en):
|
| 194 |
result = sct.gen_text_code_semantic(text_en, embedding=True)
|
| 195 |
assert result["iscc"] == "ISCC:CAA636IXQD736IGJ"
|
|
@@ -242,6 +402,99 @@ def test_compress():
|
|
| 242 |
assert compress(arr2, 1) == expected
|
| 243 |
|
| 244 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 245 |
def test_embedding_precision():
|
| 246 |
d16 = sct.gen_text_code_semantic("Hello World", embedding=True, precision=4)
|
| 247 |
assert d16["features"][0]["embedding"][0] == 0.0087
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from importlib.metadata import version
|
| 2 |
from pathlib import Path
|
| 3 |
|
| 4 |
import pytest
|
| 5 |
from blake3 import blake3
|
| 6 |
|
| 7 |
import iscc_sct as sct
|
| 8 |
+
from iscc_sct import code_semantic_text as cst
|
| 9 |
from iscc_sct.code_semantic_text import (
|
| 10 |
split_text,
|
| 11 |
tokenize_chunks,
|
|
|
|
| 33 |
|
| 34 |
|
| 35 |
def test_version():
|
| 36 |
+
# Compared against the package metadata so the two cannot drift apart silently
|
| 37 |
+
assert sct.__version__ == version("iscc-sct")
|
| 38 |
|
| 39 |
|
| 40 |
def test_code_text_semantic_default():
|
|
|
|
| 107 |
offsets=True,
|
| 108 |
contents=True,
|
| 109 |
)
|
| 110 |
+
assert result == {
|
| 111 |
+
"characters": 726,
|
| 112 |
+
"iscc": "ISCC:CAARISHPJHEXQAYL",
|
| 113 |
+
"features": [
|
| 114 |
+
{
|
| 115 |
+
"maintype": "semantic",
|
| 116 |
+
"subtype": "text",
|
| 117 |
+
"version": 0,
|
| 118 |
+
"byte_offsets": False,
|
| 119 |
+
"simprints": ["FWjtTcl4Aws", "lAjHSc1wAws"],
|
| 120 |
+
"offsets": [0, 297],
|
| 121 |
+
"contents": [
|
| 122 |
+
"\n"
|
| 123 |
+
"`iscc-sct` is a **proof of concept implementation** of a semantic "
|
| 124 |
+
"Text-Code for the\n"
|
| 125 |
+
"[ISCC](https://core.iscc.codes) (*International Standard Content "
|
| 126 |
+
"Code*). Semantic Text-Codes are\n"
|
| 127 |
+
"designed to capture and represent the language agnostic semantic "
|
| 128 |
+
"content of text for improved\n"
|
| 129 |
+
"similarity detection.\n"
|
| 130 |
+
"\n", # NOTE: end of first chunk (see comma :)
|
| 131 |
+
"\n"
|
| 132 |
+
"\n"
|
| 133 |
+
"The ISCC framework already comes with a Text-Code that is based "
|
| 134 |
+
"on lexical similarity and can match\n"
|
| 135 |
+
"near duplicates. The ISCC Semantic Text-Code is planned as a new "
|
| 136 |
+
"additional ISCC-UNIT focused on\n"
|
| 137 |
+
"capturing a more abstract and broad semantic similarity. As such "
|
| 138 |
+
"the Semantic Text-Code is\n"
|
| 139 |
+
"engineered to be robust against a broader range of variations and "
|
| 140 |
+
"translations of text that cannot\n"
|
| 141 |
+
"be matched based on lexical similarity.\n",
|
| 142 |
+
],
|
| 143 |
+
}
|
| 144 |
+
],
|
| 145 |
+
}
|
|
|
|
|
|
|
| 146 |
|
| 147 |
|
| 148 |
def test_gen_text_code_semantic_checks_bits():
|
|
|
|
| 157 |
|
| 158 |
|
| 159 |
def test_split_text_override():
|
| 160 |
+
text = "Try some very small and granular text splitting with Iñtërnâtiônàlizætiøn☃. Use options override for it."
|
| 161 |
chunks = split_text(text, max_tokens=8, overlap=4)
|
| 162 |
assert chunks == [
|
| 163 |
(0, "Try some very small and granular text "),
|
| 164 |
+
(20, "and granular text splitting with "),
|
| 165 |
+
(53, "Iñtërnâtiônà"),
|
| 166 |
+
(59, "âtiônàlizætiøn"),
|
| 167 |
+
(73, "☃. "),
|
| 168 |
+
(76, "Use options override for it."),
|
| 169 |
+
]
|
| 170 |
+
|
| 171 |
+
|
| 172 |
+
def test_split_text_override_byte_offsets():
|
| 173 |
+
text = "Try some very small and granular text splitting with Iñtërnâtiônàlizætiøn☃. Use options override for it."
|
| 174 |
+
chunks = split_text(text, max_tokens=8, overlap=4, byte_offsets=True)
|
| 175 |
+
assert chunks == [
|
| 176 |
+
(0, "Try some very small and granular text "),
|
| 177 |
+
(20, "and granular text splitting with "),
|
| 178 |
+
(53, "Iñtërnâtiônà"),
|
| 179 |
+
(61, "âtiônàlizætiøn"),
|
| 180 |
+
(80, "☃. "),
|
| 181 |
+
(85, "Use options override for it."),
|
| 182 |
]
|
| 183 |
|
| 184 |
|
| 185 |
+
def test_split_text_resource_options_do_not_fragment_splitter_cache():
|
| 186 |
+
# The splitter cache is keyed on its kwargs - resource knobs like batch_size must not
|
| 187 |
+
# reach it, or every distinct value would retain another tokenizer-backed TextSplitter.
|
| 188 |
+
split_text("Hello World", batch_size=11)
|
| 189 |
+
cached = cst.splitter.cache_info().currsize
|
| 190 |
+
split_text("Hello World", batch_size=12, intra_op_threads=3)
|
| 191 |
+
assert cst.splitter.cache_info().currsize == cached
|
| 192 |
+
|
| 193 |
+
|
| 194 |
+
def test_chunking_tokenizer_disables_truncation_and_padding():
|
| 195 |
+
# The sizing tokenizer must not truncate/pad: truncation makes the HF chunk sizer emit
|
| 196 |
+
# overflow encodings and degrades chunking to super-linear runtime (issue #24). The
|
| 197 |
+
# embedding tokenizer must keep truncation so ISCC codes stay unchanged.
|
| 198 |
+
chunking = cst.chunking_tokenizer()
|
| 199 |
+
assert chunking.truncation is None
|
| 200 |
+
assert chunking.padding is None
|
| 201 |
+
assert cst.tokenizer().truncation is not None
|
| 202 |
+
|
| 203 |
+
|
| 204 |
+
def test_token_count_matches_guarded_for_small_text():
|
| 205 |
+
text = "Ein kurzer deutscher Satz mit Umlauten: äöüß."
|
| 206 |
+
assert cst.token_count_guarded(text, 127) == cst.token_count(text)
|
| 207 |
+
|
| 208 |
+
|
| 209 |
+
def test_token_count_guarded_short_circuits_oversized_text():
|
| 210 |
+
text = "Viele kleine Wörter ergeben einen sehr langen Text. " * 100
|
| 211 |
+
guarded = cst.token_count_guarded(text, 127)
|
| 212 |
+
assert guarded > 127
|
| 213 |
+
assert cst.token_count(text) > 127
|
| 214 |
+
|
| 215 |
+
|
| 216 |
+
def test_token_count_guarded_falls_back_on_sparse_prefix():
|
| 217 |
+
# Prefix probe stays below max_tokens, forcing exact full tokenization
|
| 218 |
+
text = "a" + " " * 2000 + "b" * 2000
|
| 219 |
+
assert cst.token_count_guarded(text, 127) == cst.token_count(text)
|
| 220 |
+
|
| 221 |
+
|
| 222 |
+
def test_token_count_guarded_short_circuits_unicode_whitespace():
|
| 223 |
+
# NBSP-separated words are tokenizer word boundaries (WhitespaceSplit), so the guard must
|
| 224 |
+
# short-circuit on them instead of falling back to full tokenization (issue #24 PDF whitespace).
|
| 225 |
+
text = chr(0xA0).join(["wort"] * 4000) # NBSP-separated words
|
| 226 |
+
guarded = cst.token_count_guarded(text, 127)
|
| 227 |
+
# The overestimate path returns prefix_count + tail-chars, strictly above the exact count;
|
| 228 |
+
# a fallback would instead return exactly token_count(text).
|
| 229 |
+
assert guarded > cst.token_count(text)
|
| 230 |
+
|
| 231 |
+
|
| 232 |
+
def test_needs_split_guard_paragraph_gap():
|
| 233 |
+
# Large gap from the start to the next paragraph separator routes to the guarded splitter.
|
| 234 |
+
assert cst.needs_split_guard("x" * 20000 + "\n\nEnde.") is True
|
| 235 |
+
|
| 236 |
+
|
| 237 |
+
def test_needs_split_guard_newline_free_span():
|
| 238 |
+
# A giant span with no newline at all (one long paragraph) also blows up the native sizer.
|
| 239 |
+
assert cst.needs_split_guard("word " * 4000) is True
|
| 240 |
+
|
| 241 |
+
|
| 242 |
+
def test_needs_split_guard_long_line_before_newline():
|
| 243 |
+
# A >8K separator-free span before the first single newline is enough to route to guarded.
|
| 244 |
+
assert cst.needs_split_guard("word " * 4000 + "\nrest") is True
|
| 245 |
+
|
| 246 |
+
|
| 247 |
+
def test_needs_split_guard_trailing_separator_free_span():
|
| 248 |
+
# Early paragraph break then a long separator-free tail (missed by the paragraph-level scan).
|
| 249 |
+
assert cst.needs_split_guard("Intro.\n\n" + "word " * 4000) is True
|
| 250 |
+
|
| 251 |
+
|
| 252 |
+
def test_needs_split_guard_dense_newlines_is_false():
|
| 253 |
+
# Dense single newlines bound the probes; no oversized span -> normal splitter.
|
| 254 |
+
assert cst.needs_split_guard("line\n" * 4000) is False
|
| 255 |
+
|
| 256 |
+
|
| 257 |
+
def test_needs_split_guard_short_text_is_false():
|
| 258 |
+
assert cst.needs_split_guard("A short paragraph.\n\nAnother one.") is False
|
| 259 |
+
|
| 260 |
+
|
| 261 |
def test_tokenize_chunks():
|
| 262 |
chunks = ["Hello World", "These are chunks"]
|
| 263 |
result = tokenize_chunks(chunks)
|
|
|
|
| 283 |
assert list(embeddings[0][:3]) == pytest.approx(expected, rel=1e-3)
|
| 284 |
|
| 285 |
|
| 286 |
+
def test_embed_chunks_batching_is_result_invariant():
|
| 287 |
+
# Batch size is a pure resource knob - it must not change the embeddings. Chunks differ in
|
| 288 |
+
# length so the batched run pads, which is exactly what could perturb the result.
|
| 289 |
+
chunks = ["Hello World", "These are chunks", "A considerably longer chunk of sample text"]
|
| 290 |
+
assert embed_chunks(chunks, batch_size=1) == pytest.approx(
|
| 291 |
+
embed_chunks(chunks, batch_size=3), abs=1e-6
|
| 292 |
+
)
|
| 293 |
+
|
| 294 |
+
|
| 295 |
+
def test_embed_chunks_default_uses_global_batch_size(monkeypatch):
|
| 296 |
+
# soft_hash_text_semantic calls embed_chunks without a batch size - the documented global
|
| 297 |
+
# override (ISCC_SCT_BATCH_SIZE) must still apply on that path.
|
| 298 |
+
calls = []
|
| 299 |
+
real_tokenize = cst.tokenize_chunks
|
| 300 |
+
monkeypatch.setattr(cst, "tokenize_chunks", lambda c: calls.append(len(c)) or real_tokenize(c))
|
| 301 |
+
monkeypatch.setattr(sct.sct_opts, "batch_size", 2)
|
| 302 |
+
embed_chunks(["Hello World", "These are chunks", "A third chunk"])
|
| 303 |
+
assert calls == [2, 1]
|
| 304 |
+
|
| 305 |
+
|
| 306 |
+
def test_resolve_batch_size_explicit_wins():
|
| 307 |
+
assert cst.resolve_batch_size(7, ["CPUExecutionProvider"]) == 7
|
| 308 |
+
assert cst.resolve_batch_size(7, ["CUDAExecutionProvider"]) == 7
|
| 309 |
+
|
| 310 |
+
|
| 311 |
+
def test_resolve_batch_size_auto():
|
| 312 |
+
assert cst.resolve_batch_size(0, ["CPUExecutionProvider"]) == cst.CPU_BATCH_SIZE
|
| 313 |
+
assert (
|
| 314 |
+
cst.resolve_batch_size(0, ["CUDAExecutionProvider", "CPUExecutionProvider"])
|
| 315 |
+
== cst.GPU_BATCH_SIZE
|
| 316 |
+
)
|
| 317 |
+
|
| 318 |
+
|
| 319 |
+
def test_session_options_thread_default():
|
| 320 |
+
rt = cst.load_onnxruntime()
|
| 321 |
+
# 0 leaves the runtime default in place instead of pinning a thread count
|
| 322 |
+
assert cst.session_options(rt, 0).intra_op_num_threads == 0
|
| 323 |
+
assert cst.session_options(rt, 2).intra_op_num_threads == 2
|
| 324 |
+
|
| 325 |
+
|
| 326 |
+
def capture_gpu_shadowed_warnings(providers):
|
| 327 |
+
# type: (list[str]) -> list[str]
|
| 328 |
+
"""Run warn_gpu_shadowed with a capturing log sink and return emitted warnings."""
|
| 329 |
+
messages = []
|
| 330 |
+
handler_id = cst.log.add(messages.append, level="WARNING")
|
| 331 |
+
try:
|
| 332 |
+
cst.warn_gpu_shadowed(providers)
|
| 333 |
+
finally:
|
| 334 |
+
cst.log.remove(handler_id)
|
| 335 |
+
return messages
|
| 336 |
+
|
| 337 |
+
|
| 338 |
+
def test_warn_gpu_shadowed_silent_with_cuda():
|
| 339 |
+
assert capture_gpu_shadowed_warnings(["CUDAExecutionProvider", "CPUExecutionProvider"]) == []
|
| 340 |
+
|
| 341 |
+
|
| 342 |
+
def test_warn_gpu_shadowed_silent_without_gpu_package():
|
| 343 |
+
assert capture_gpu_shadowed_warnings(["CPUExecutionProvider"]) == []
|
| 344 |
+
|
| 345 |
+
|
| 346 |
+
def test_warn_gpu_shadowed_warns_on_shadowed_install(monkeypatch):
|
| 347 |
+
monkeypatch.setattr(cst, "distribution", lambda name: object())
|
| 348 |
+
messages = capture_gpu_shadowed_warnings(["CPUExecutionProvider"])
|
| 349 |
+
assert len(messages) == 1
|
| 350 |
+
assert "CUDA support is unavailable" in messages[0]
|
| 351 |
+
|
| 352 |
+
|
| 353 |
def test_gen_text_code_semantic(text_en):
|
| 354 |
result = sct.gen_text_code_semantic(text_en, embedding=True)
|
| 355 |
assert result["iscc"] == "ISCC:CAA636IXQD736IGJ"
|
|
|
|
| 402 |
assert compress(arr2, 1) == expected
|
| 403 |
|
| 404 |
|
| 405 |
+
def test_utf32be_chunk_retrieval():
|
| 406 |
+
"""Test that we can retrieve text chunks using UTF-32BE encoding with offset/size * 4."""
|
| 407 |
+
# Generate text code with features
|
| 408 |
+
text = (
|
| 409 |
+
"Hello world! 你好世界! こんにちは! 안녕하세요! مرحبا! שלום! Ç 가 Ω ℍ ① ︷ i⁹ ¼ dž ⫝̸ ȴ ȷ ɂ ć "
|
| 410 |
+
"Iñtërnâtiôn\nàlizætiøn☃💩 – is a tric\t ky thing!\r"
|
| 411 |
+
)
|
| 412 |
+
text += TEXT
|
| 413 |
+
result = sct.gen_text_code_semantic(
|
| 414 |
+
text, simprints=True, offsets=True, sizes=True, contents=True
|
| 415 |
+
)
|
| 416 |
+
|
| 417 |
+
# Convert text to UTF-32BE
|
| 418 |
+
text_utf32be = text.encode("utf-32be")
|
| 419 |
+
|
| 420 |
+
# For each feature, retrieve the chunk using offset and size
|
| 421 |
+
features = result["features"][0]
|
| 422 |
+
for i, simprint in enumerate(features["simprints"]):
|
| 423 |
+
offset = features["offsets"][i]
|
| 424 |
+
size = features["sizes"][i]
|
| 425 |
+
original_chunk = features["contents"][i]
|
| 426 |
+
|
| 427 |
+
# Calculate byte offset and size in UTF-32BE
|
| 428 |
+
byte_offset = offset * 4
|
| 429 |
+
byte_size = size * 4
|
| 430 |
+
|
| 431 |
+
# Retrieve chunk from UTF-32BE encoded text
|
| 432 |
+
chunk_bytes = text_utf32be[byte_offset : byte_offset + byte_size]
|
| 433 |
+
retrieved_chunk = chunk_bytes.decode("utf-32be")
|
| 434 |
+
|
| 435 |
+
# Verify retrieved chunk matches the original
|
| 436 |
+
assert retrieved_chunk == original_chunk, f"Chunk mismatch at index {i}"
|
| 437 |
+
|
| 438 |
+
|
| 439 |
def test_embedding_precision():
|
| 440 |
d16 = sct.gen_text_code_semantic("Hello World", embedding=True, precision=4)
|
| 441 |
assert d16["features"][0]["embedding"][0] == 0.0087
|
| 442 |
+
|
| 443 |
+
|
| 444 |
+
def test_create_byte_offsets():
|
| 445 |
+
"""Test generation with byte offsets using text with multibyte characters."""
|
| 446 |
+
# Text with ASCII, CJK, emoji, etc.
|
| 447 |
+
text = (
|
| 448 |
+
"Hello world! 你好世界! こんにちは! 안녕하세요! مرحبا! שלום! Ç 가 Ω ℍ ① ︷ i⁹ ¼ dž ⫝̸ ȴ ȷ ɂ ć "
|
| 449 |
+
"Iñtërnâtiôn\nàlizætiøn☃💩 – is a tric\t ky thing!\r"
|
| 450 |
+
)
|
| 451 |
+
text += TEXT
|
| 452 |
+
|
| 453 |
+
# Generate with character offsets (default)
|
| 454 |
+
result_char = sct.create(text, offsets=True, sizes=True)
|
| 455 |
+
# Generate with byte offsets
|
| 456 |
+
result_bytes = sct.create(text, offsets=True, sizes=True, byte_offsets=True)
|
| 457 |
+
|
| 458 |
+
# ISCC and Character Count should be the same
|
| 459 |
+
assert result_char.iscc == result_bytes.iscc
|
| 460 |
+
assert result_char.characters == result_bytes.characters
|
| 461 |
+
|
| 462 |
+
# Features should be different
|
| 463 |
+
assert result_char.features != result_bytes.features
|
| 464 |
+
|
| 465 |
+
# Character versus UTF-8 chunk retrieval and actual chunks should match
|
| 466 |
+
char_chunk = text[0 : result_char.features[0].sizes[0]]
|
| 467 |
+
byte_chunk = text.encode("utf-8")[0 : result_bytes.features[0].sizes[0]].decode("utf-8")
|
| 468 |
+
assert char_chunk == byte_chunk
|
| 469 |
+
actual_chunk = sct.create(text, contents=True)
|
| 470 |
+
assert actual_chunk.features[0].contents[0] == char_chunk
|
| 471 |
+
|
| 472 |
+
assert result_char.model_dump(exclude_none=True) == {
|
| 473 |
+
"iscc": "ISCC:CAARISGPJHEXQBQL",
|
| 474 |
+
"characters": 851,
|
| 475 |
+
"features": [
|
| 476 |
+
{
|
| 477 |
+
"byte_offsets": False,
|
| 478 |
+
"maintype": "semantic",
|
| 479 |
+
"offsets": [0, 209, 422],
|
| 480 |
+
"sizes": [307, 215, 429],
|
| 481 |
+
"subtype": "text",
|
| 482 |
+
"version": 0,
|
| 483 |
+
}
|
| 484 |
+
],
|
| 485 |
+
}
|
| 486 |
+
|
| 487 |
+
assert result_bytes.model_dump(exclude_none=True) == {
|
| 488 |
+
"iscc": "ISCC:CAARISGPJHEXQBQL",
|
| 489 |
+
"characters": 851,
|
| 490 |
+
"features": [
|
| 491 |
+
{
|
| 492 |
+
"byte_offsets": True,
|
| 493 |
+
"maintype": "semantic",
|
| 494 |
+
"offsets": [0, 280, 493],
|
| 495 |
+
"sizes": [378, 215, 429],
|
| 496 |
+
"subtype": "text",
|
| 497 |
+
"version": 0,
|
| 498 |
+
}
|
| 499 |
+
],
|
| 500 |
+
}
|
tests/test_main.py
CHANGED
|
@@ -21,6 +21,7 @@ def test_create_granular():
|
|
| 21 |
"maintype": "semantic",
|
| 22 |
"subtype": "text",
|
| 23 |
"version": 0,
|
|
|
|
| 24 |
"simprints": [
|
| 25 |
{"content": "Hello World", "offset": 0, "simprint": "82eJ2NG741E", "size": 11}
|
| 26 |
],
|
|
|
|
| 21 |
"maintype": "semantic",
|
| 22 |
"subtype": "text",
|
| 23 |
"version": 0,
|
| 24 |
+
"byte_offsets": False,
|
| 25 |
"simprints": [
|
| 26 |
{"content": "Hello World", "offset": 0, "simprint": "82eJ2NG741E", "size": 11}
|
| 27 |
],
|
tests/test_models.py
CHANGED
|
@@ -26,6 +26,7 @@ def test_feature_set_initialization():
|
|
| 26 |
"maintype": "semantic",
|
| 27 |
"subtype": "text",
|
| 28 |
"version": 0,
|
|
|
|
| 29 |
}
|
| 30 |
|
| 31 |
|
|
|
|
| 26 |
"maintype": "semantic",
|
| 27 |
"subtype": "text",
|
| 28 |
"version": 0,
|
| 29 |
+
"byte_offsets": False,
|
| 30 |
}
|
| 31 |
|
| 32 |
|