titusz commited on
Commit
8ef3a6b
·
verified ·
1 Parent(s): 681cad2

Sync to v0.2.2 (github.com/iscc/iscc-sct @ bf39b3a)

Browse files
This view is limited to 50 files because it contains too many changes.   See raw diff
.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.1.3] - Unrelease
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
- # LICENSE
 
 
2
 
3
- This work is licensed under the Creative Commons Attribution-NonCommercial-ShareAlike 4.0 International License.
4
 
5
- To view a copy of this license, visit https://creativecommons.org/licenses/by-nc-sa/4.0/ or send a letter to
6
- Creative Commons, PO Box 1866, Mountain View, CA 94042, USA.
7
 
8
- ## Attribution
 
9
 
10
- Titusz Pan, ISCC Foundation - 2024
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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: Iscc Sct
3
- emoji: 📚
4
- colorFrom: pink
5
- colorTo: green
6
  sdk: gradio
7
- sdk_version: 4.41.0
 
8
  app_file: app.py
9
- pinned: false
 
 
10
  ---
11
 
12
- Check out the configuration reference at https://huggingface.co/docs/hub/spaces-config-reference
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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

  • SHA256: fe5c771cbdcb1828bd15a7526a294418df77b56340689da268f35a189a932903
  • Pointer size: 131 Bytes
  • Size of remote file: 116 kB
docs/assets/logo_light.png ADDED

Git LFS Details

  • SHA256: 74cfb03145731f8873c0f3dbb0478f946c23dc847891def584a2866c3dc988c2
  • Pointer size: 131 Bytes
  • Size of remote file: 113 kB
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
+ [![Tests](https://github.com/iscc/iscc-sct/actions/workflows/tests.yml/badge.svg)](https://github.com/iscc/iscc-sct/actions/workflows/tests.yml)
9
+ [![Version](https://img.shields.io/pypi/v/iscc-sct.svg)](https://pypi.python.org/pypi/iscc-sct/)
10
+ [![Downloads](https://pepy.tech/badge/iscc-sct)](https://pepy.tech/project/iscc-sct)
11
+ [![License](https://img.shields.io/badge/license-Apache%202.0-green.svg)](https://github.com/iscc/iscc-sct/blob/main/LICENSE)
12
+ [![Ask DeepWiki](https://deepwiki.com/badge.svg)](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.1.3"
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(description="Generate Semantic Text-Codes for text files.")
 
 
11
  parser.add_argument(
12
  "path",
13
  type=str,
14
- help="Path to text files (supports glob patterns) or 'gui' to launch Gradio demo.",
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 'pip install gradio' to use the GUI."
 
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
- from onnxruntime.capi.onnxruntime_pybind11_state import NoSuchFile
 
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
- If you enable generating granular features with `features=True` those features will have
70
- the same bit-length as the generated ISCC-UNIT.
71
-
72
- :param fp: File path of plaintext file to process
73
- :param options: Custom processing options for overriding global options
74
- :key bits (int): Length of generated Semantic Text-Code in bits (default 64)
75
- :key characters (bool): Return document character count (default True).
76
- :key embedding (bool): Return global document embedding (default False).
77
- :key precision (int): Max fractional digits for embeddings (default 8).
78
- :key simprints (bool): Return granular document features (default False).
79
- :key offsets (bool): Return character offsets for granular features (default False).
80
- :key sizes (bool): Include sizes of granular features (number of chars, default False).
81
- :key contents (bool): Return text chunks (default False).
82
- :key max_tokens (int): Max tokens per chunk (default 127).
83
- :key overlap (int): Max tokens allowed to overlap between chunks (default 48).
84
- :key trim (int): Trim whitespace from chunks (default False).
 
 
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: Plaint text for ISCC processing
97
- :param options: Custom processing options for overriding global options
98
- :key bits (int): Length of generated Semantic Text-Code in bits (default 64)
99
- :key characters (bool): Return document character count (default True).
100
- :key embedding (bool): Return global document embedding (default False).
101
- :key precision (int): Max fractional digits for embeddings (default 8).
102
- :key simprints (bool): Return granular document features (default False).
103
- :key offsets (bool): Return character offsets for granular features (default False).
104
- :key sizes (bool): Include sizes of granular features (number of chars, default False).
105
- :key contents (bool): Return text chunks (default False).
106
- :key max_tokens (int): Max tokens per chunk (default 127).
107
- :key overlap (int): Max tokens allowed to overlap between chunks (default 48).
108
- :key trim (int): Trim whitespace from chunks (default False).
 
 
 
 
 
 
 
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
- feature_set["sizes"] = [len(chunk) for chunk in chunks]
 
 
 
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
- """Creates a 256-bit semantic similarity preserving hash for text input."""
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 to overlap between chunks (default 48).
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
- return splitter(**opts.model_dump()).chunk_indices(text)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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 to overlap between chunks (default 48).
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
- tokenizer(), capacity=opts.max_tokens, overlap=opts.overlap, trim=opts.trim
215
  )
216
 
217
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
218
  @cache
219
  def model():
220
- # type: () -> rt.InferenceSession
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.SessionOptions()
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=100):
 
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 based mean pooling to the token embeddings.
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 bytes object representing the binary hash.
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
- with gr.Blocks(css=custom_css, theme=iscc_theme) as demo:
 
 
 
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("**\*.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:
 
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
- os.makedirs(dirs.user_data_dir, exist_ok=True)
 
 
 
 
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.1.0.onnx"
39
  MODEL_URL = f"{BASE_URL}/{MODEL_FILENAME}"
40
- MODEL_PATH = Path(dirs.user_data_dir) / MODEL_FILENAME
41
- MODEL_CHECKSUM = "ff254d62db55ed88a1451b323a66416f60838dd2f0338dba21bc3b8822459abc"
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
- [tool.poetry]
2
  name = "iscc-sct"
3
- version = "0.1.3"
4
  description = "ISCC - Semantic Code Text"
5
- authors = ["Titusz <tp@py7.de>"]
6
- license = "CC-BY-NC-SA-4.0"
7
  readme = "README.md"
8
- homepage = "https://iscc.codes"
9
- repository = "https://github.com/iscc/iscc-sct"
10
- documentation = "https://github.com/iscc/iscc-sct"
11
- keywords=["iscc", "text similarity", "cross lingual", "semantic similarity"]
12
- classifiers=[
 
 
 
 
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
- [tool.poetry.urls]
40
- "Changelog" = "https://github.com/iscc/iscc-sct/blob/main/CHANGELOG.md"
41
- "Bug Tracker" = "https://github.com/iscc/iscc-sct/issues"
42
- "Twitter" = "https://twitter.com/iscc_foundation"
43
- "Donate" = "https://iscc.foundation/support"
 
44
 
45
- [tool.poetry.scripts]
46
- sct = 'iscc_sct.cli:main'
 
 
 
 
 
 
47
 
48
- [tool.poetry.dependencies]
49
- python = ">=3.9,<3.13"
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.poetry.extras]
68
- gpu = ["onnxruntime-gpu"]
69
- demo = ["gradio"]
 
 
 
70
 
71
- [tool.poetry.group.test.dependencies]
72
- pytest = "*"
73
- coverage = "*"
74
- pytest-cov = "*"
75
 
76
- [tool.poetry.group.dev.dependencies]
77
- poethepoet = "*"
78
- ruff = "*"
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
- test = { cmd = "pytest --cov=iscc_sct --cov-fail-under=100", help = "Run tests with coverage" }
98
- update-dependencies = { cmd = "poetry update", help = "Update dependencies" }
99
- all = ["format-code", "format-markdown", "format-yml", "convert-lf", "test"]
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
- semantic-text-splitter
2
- onnxruntime
3
- loguru
4
- blake3
5
- platformdirs
6
- tokenizers
7
- pydantic-settings
8
- charset-normalizer
9
- numpy<2.0.0
10
- pybase64
11
- certifi
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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: 4.41.0
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
- sct = shutil.which("sct")
 
 
 
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([sct], capture_output=True, text=True)
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([sct, str(empty_text_file), "-d"], capture_output=True, text=True)
 
 
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([sct, str(non_utf8_text_file), "-d"], capture_output=True, text=True)
 
 
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([sct, str(sample_text_file)], capture_output=True, text=True)
 
 
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
- [sct, str(sample_text_file), "--granular"], capture_output=True, text=True
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([sct, str(sample_text_file), "--debug"], capture_output=True, text=True)
 
 
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
- assert sct.__version__ == "0.1.3"
 
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
- result
109
- == {
110
- "characters": 726,
111
- "iscc": "ISCC:CAARISHPJHEXQAYL",
112
- "features": [
113
- {
114
- "maintype": "semantic",
115
- "subtype": "text",
116
- "version": 0,
117
- "simprints": ["FWjtTcl4Aws", "lAjHSc1wAws"],
118
- "offsets": [0, 297],
119
- "contents": [
120
- "\n"
121
- "`iscc-sct` is a **proof of concept implementation** of a semantic "
122
- "Text-Code for the\n"
123
- "[ISCC](https://core.iscc.codes) (*International Standard Content "
124
- "Code*). Semantic Text-Codes are\n"
125
- "designed to capture and represent the language agnostic semantic "
126
- "content of text for improved\n"
127
- "similarity detection.\n"
128
- "\n", # NOTE: end of first chunk (see comma :)
129
- "\n"
130
- "\n"
131
- "The ISCC framework already comes with a Text-Code that is based "
132
- "on lexical similarity and can match\n"
133
- "near duplicates. The ISCC Semantic Text-Code is planned as a new "
134
- "additional ISCC-UNIT focused on\n"
135
- "capturing a more abstract and broad semantic similarity. As such "
136
- "the Semantic Text-Code is\n"
137
- "engineered to be robust against a broader range of variations and "
138
- "translations of text that cannot\n"
139
- "be matched based on lexical similarity.\n",
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
- (49, "Use options override for it."),
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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