cjk-semantic-split 0.1.4__tar.gz → 0.2.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {cjk_semantic_split-0.1.4 → cjk_semantic_split-0.2.0}/PKG-INFO +1 -1
- {cjk_semantic_split-0.1.4 → cjk_semantic_split-0.2.0}/pyproject.toml +2 -2
- {cjk_semantic_split-0.1.4 → cjk_semantic_split-0.2.0}/scripts/harvest.py +69 -29
- {cjk_semantic_split-0.1.4 → cjk_semantic_split-0.2.0}/src/chinese_decompose/__init__.py +1 -1
- cjk_semantic_split-0.2.0/src/chinese_decompose/data/decomp_l1.txt +74410 -0
- cjk_semantic_split-0.2.0/src/chinese_decompose/data/decomp_l2.txt +6 -0
- cjk_semantic_split-0.2.0/src/chinese_decompose/data/decomp_l3.txt +8 -0
- cjk_semantic_split-0.2.0/src/chinese_decompose/data/decomp_patches.txt +6 -0
- cjk_semantic_split-0.2.0/src/chinese_decompose/data/primitives.txt +1510 -0
- {cjk_semantic_split-0.1.4 → cjk_semantic_split-0.2.0}/src/chinese_decompose/dsl.py +34 -3
- {cjk_semantic_split-0.1.4 → cjk_semantic_split-0.2.0}/src/chinese_decompose/loader.py +64 -5
- {cjk_semantic_split-0.1.4 → cjk_semantic_split-0.2.0}/src/chinese_decompose/primitives.py +25 -4
- {cjk_semantic_split-0.1.4 → cjk_semantic_split-0.2.0}/tests/test_decompose.py +7 -7
- {cjk_semantic_split-0.1.4 → cjk_semantic_split-0.2.0}/tests/test_harvest.py +13 -11
- {cjk_semantic_split-0.1.4 → cjk_semantic_split-0.2.0}/tests/test_primitives.py +13 -10
- {cjk_semantic_split-0.1.4 → cjk_semantic_split-0.2.0}/tests/test_smoke.py +1 -1
- cjk_semantic_split-0.1.4/src/chinese_decompose/data/decomp_l1.json +0 -1
- cjk_semantic_split-0.1.4/src/chinese_decompose/data/decomp_l2.json +0 -1
- cjk_semantic_split-0.1.4/src/chinese_decompose/data/decomp_l3.json +0 -5
- cjk_semantic_split-0.1.4/src/chinese_decompose/data/decomp_patches.json +0 -1
- cjk_semantic_split-0.1.4/src/chinese_decompose/data/primitives.json +0 -1
- {cjk_semantic_split-0.1.4 → cjk_semantic_split-0.2.0}/.github/workflows/ci.yml +0 -0
- {cjk_semantic_split-0.1.4 → cjk_semantic_split-0.2.0}/.gitignore +0 -0
- {cjk_semantic_split-0.1.4 → cjk_semantic_split-0.2.0}/LICENSE +0 -0
- {cjk_semantic_split-0.1.4 → cjk_semantic_split-0.2.0}/README.md +0 -0
- {cjk_semantic_split-0.1.4 → cjk_semantic_split-0.2.0}/docs/component-position-encoding.md +0 -0
- {cjk_semantic_split-0.1.4 → cjk_semantic_split-0.2.0}/docs/decomposition-logic.md +0 -0
- {cjk_semantic_split-0.1.4 → cjk_semantic_split-0.2.0}/docs/superpowers/plans/2026-07-16-cjk-semantic-split-implementation.md +0 -0
- {cjk_semantic_split-0.1.4 → cjk_semantic_split-0.2.0}/docs/superpowers/specs/2026-07-16-cjk-semantic-split-design.md +0 -0
- {cjk_semantic_split-0.1.4 → cjk_semantic_split-0.2.0}/scripts/__init__.py +0 -0
- {cjk_semantic_split-0.1.4 → cjk_semantic_split-0.2.0}/scripts/review_conflicts.py +0 -0
- {cjk_semantic_split-0.1.4 → cjk_semantic_split-0.2.0}/src/chinese_decompose/__main__.py +0 -0
- {cjk_semantic_split-0.1.4 → cjk_semantic_split-0.2.0}/src/chinese_decompose/api.py +0 -0
- {cjk_semantic_split-0.1.4 → cjk_semantic_split-0.2.0}/src/chinese_decompose/errors.py +0 -0
- {cjk_semantic_split-0.1.4 → cjk_semantic_split-0.2.0}/src/chinese_decompose/split.py +0 -0
- {cjk_semantic_split-0.1.4 → cjk_semantic_split-0.2.0}/src/chinese_decompose/structures.py +0 -0
- {cjk_semantic_split-0.1.4 → cjk_semantic_split-0.2.0}/tests/__init__.py +0 -0
- {cjk_semantic_split-0.1.4 → cjk_semantic_split-0.2.0}/tests/conftest.py +0 -0
- {cjk_semantic_split-0.1.4 → cjk_semantic_split-0.2.0}/tests/test_api.py +0 -0
- {cjk_semantic_split-0.1.4 → cjk_semantic_split-0.2.0}/tests/test_cli.py +0 -0
- {cjk_semantic_split-0.1.4 → cjk_semantic_split-0.2.0}/tests/test_compose.py +0 -0
- {cjk_semantic_split-0.1.4 → cjk_semantic_split-0.2.0}/tests/test_dsl.py +0 -0
- {cjk_semantic_split-0.1.4 → cjk_semantic_split-0.2.0}/tests/test_loader.py +0 -0
- {cjk_semantic_split-0.1.4 → cjk_semantic_split-0.2.0}/tests/test_properties.py +0 -0
- {cjk_semantic_split-0.1.4 → cjk_semantic_split-0.2.0}/tests/test_read_structure.py +0 -0
- {cjk_semantic_split-0.1.4 → cjk_semantic_split-0.2.0}/tests/test_split.py +0 -0
- {cjk_semantic_split-0.1.4 → cjk_semantic_split-0.2.0}/tests/test_structures.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: cjk-semantic-split
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.2.0
|
|
4
4
|
Summary: Recursive atomic decomposition of CJK characters with 9-grid 2-digit spatial encoding. 将汉字递归拆解为原子部件,并在 9 宫格 2 位数字空间编码中标记每个组件的位置。
|
|
5
5
|
Project-URL: Source, https://github.com/yueguanyu/Semantic-Character-Splitting-and-Structural-Coding-for-Chinese-Script.git
|
|
6
6
|
Project-URL: Bug Tracker, https://github.com/yueguanyu/Semantic-Character-Splitting-and-Structural-Coding-for-Chinese-Script/issues
|
|
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "cjk-semantic-split"
|
|
7
|
-
version = "0.
|
|
7
|
+
version = "0.2.0"
|
|
8
8
|
description = "Recursive atomic decomposition of CJK characters with 9-grid 2-digit spatial encoding. 将汉字递归拆解为原子部件,并在 9 宫格 2 位数字空间编码中标记每个组件的位置。"
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
requires-python = ">=3.9"
|
|
@@ -40,7 +40,7 @@ dev = [
|
|
|
40
40
|
|
|
41
41
|
[tool.hatch.build.targets.wheel]
|
|
42
42
|
packages = ["src/chinese_decompose"]
|
|
43
|
-
include = ["src/chinese_decompose/data/*.
|
|
43
|
+
include = ["src/chinese_decompose/data/*.txt"]
|
|
44
44
|
|
|
45
45
|
[tool.pytest.ini_options]
|
|
46
46
|
testpaths = ["tests"]
|
|
@@ -7,7 +7,7 @@ notation (⿰⿱⿴⿵⿶⿷⿸⿹⿺, plus 3-cell and overlaid variants w
|
|
|
7
7
|
|
|
8
8
|
Usage:
|
|
9
9
|
python -m scripts.harvest --tier l1 \
|
|
10
|
-
--output ../src/chinese_decompose/data/decomp_l1.
|
|
10
|
+
--output ../src/chinese_decompose/data/decomp_l1.txt
|
|
11
11
|
|
|
12
12
|
Source file format (tab-separated):
|
|
13
13
|
|
|
@@ -153,6 +153,11 @@ def _walk_kids(
|
|
|
153
153
|
return False
|
|
154
154
|
composed = compose_fn(parent_position, local_pos)
|
|
155
155
|
if len(comp_str) == 1:
|
|
156
|
+
# Reject leaves that are themselves IDC codes (e.g. degenerate
|
|
157
|
+
# yi-bai entries where ⿰ appears as a slot — that's a parse
|
|
158
|
+
# artifact, not a real atom).
|
|
159
|
+
if comp_str in IDC_TABLE_2_CELL:
|
|
160
|
+
return False
|
|
156
161
|
out.append((comp_str, composed))
|
|
157
162
|
else:
|
|
158
163
|
# Recurse into subtree
|
|
@@ -221,8 +226,40 @@ def parse_ids_source(text: str, compose_fn) -> tuple[dict[str, list[tuple[str, i
|
|
|
221
226
|
return entries, refs
|
|
222
227
|
|
|
223
228
|
|
|
229
|
+
def _serialize_dsl(char: str, comps: list[tuple[str, int]]) -> str:
|
|
230
|
+
"""Build the staggered DSL string for a character + its components.
|
|
231
|
+
|
|
232
|
+
Matches the format used by ``DecompositionTree.to_dsl()``:
|
|
233
|
+
"踩 [30]:足 [60]:爪 [80]:木" — the bracket carries the *next* leaf's
|
|
234
|
+
position; the last leaf is bare. A primitive char with no children
|
|
235
|
+
yields just the bare char.
|
|
236
|
+
"""
|
|
237
|
+
if not comps:
|
|
238
|
+
return char # primitive — bare char
|
|
239
|
+
parts = [f"{char} [{comps[0][1]}]"]
|
|
240
|
+
leaves = [c for c, _ in comps]
|
|
241
|
+
leaf_positions = [p for _, p in comps]
|
|
242
|
+
for i in range(len(leaves) - 1):
|
|
243
|
+
parts.append(f"{leaves[i]} [{leaf_positions[i + 1]}]")
|
|
244
|
+
parts.append(leaves[-1])
|
|
245
|
+
return ":".join(parts)
|
|
246
|
+
|
|
247
|
+
|
|
248
|
+
def _load_existing_primitives_txt(path: Path) -> set[str]:
|
|
249
|
+
if not path.exists():
|
|
250
|
+
return set()
|
|
251
|
+
chars: set[str] = set()
|
|
252
|
+
for line in path.read_text(encoding="utf-8").splitlines():
|
|
253
|
+
line = line.strip()
|
|
254
|
+
if not line or line.startswith("#"):
|
|
255
|
+
continue
|
|
256
|
+
if len(line) == 1:
|
|
257
|
+
chars.add(line)
|
|
258
|
+
return chars
|
|
259
|
+
|
|
260
|
+
|
|
224
261
|
def emit_tier1(output_path: Path, compose_fn) -> dict:
|
|
225
|
-
"""Download IDS dataset, parse, write decomp_l1.
|
|
262
|
+
"""Download IDS dataset, parse, write decomp_l1.txt and extend primitives.txt."""
|
|
226
263
|
print(f"Downloading {IDS_SOURCES['lv0']}...", file=sys.stderr)
|
|
227
264
|
text = fetch_text(IDS_SOURCES["lv0"])
|
|
228
265
|
print(f" fetched {len(text.splitlines())} lines", file=sys.stderr)
|
|
@@ -232,18 +269,26 @@ def emit_tier1(output_path: Path, compose_fn) -> dict:
|
|
|
232
269
|
print(f" entries: {len(entries)}, primitive refs: {len(refs)}", file=sys.stderr)
|
|
233
270
|
|
|
234
271
|
output_path.parent.mkdir(parents=True, exist_ok=True)
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
272
|
+
|
|
273
|
+
# Emit Tier 1 as TXT (DSL format, one line per char). Format:
|
|
274
|
+
# <char>\t<DSL>
|
|
275
|
+
# Blank lines and lines starting with `#` are comments.
|
|
276
|
+
lines = [
|
|
277
|
+
f"# Tier 1 decomposition data harvested from yi-bai/ids (MIT) via jsDelivr",
|
|
278
|
+
"# Format per line: <char>\\t<DSL>",
|
|
279
|
+
"# DSL is the staggered form: each bracket carries the NEXT atom's",
|
|
280
|
+
"# position; the last atom is bare. Lines starting with # are comments.",
|
|
281
|
+
"# Empty DSL after the tab means the character is primitive at this tier.",
|
|
282
|
+
]
|
|
283
|
+
for char in sorted(entries):
|
|
284
|
+
lines.append(f"{char}\t{_serialize_dsl(char, entries[char])}")
|
|
285
|
+
output_path.write_text("\n".join(lines) + "\n", encoding="utf-8")
|
|
240
286
|
size_kb = output_path.stat().st_size // 1024
|
|
241
287
|
print(f" wrote {output_path} ({size_kb} KB)", file=sys.stderr)
|
|
242
288
|
|
|
243
289
|
# Find all characters that get further decomposed in `entries`
|
|
244
|
-
# (recursively, since chars may appear nested). These MUST be
|
|
245
|
-
#
|
|
246
|
-
# is in both primitives and l1, l1 wins (per last-wins loader order).
|
|
290
|
+
# (recursively, since chars may appear nested). These MUST NOT be
|
|
291
|
+
# promoted into the primitive set.
|
|
247
292
|
def _collect_decomposable(char: str, seen: set[str], into: set[str]) -> None:
|
|
248
293
|
if char in seen:
|
|
249
294
|
return
|
|
@@ -260,33 +305,28 @@ def emit_tier1(output_path: Path, compose_fn) -> dict:
|
|
|
260
305
|
for char in entries:
|
|
261
306
|
_collect_decomposable(char, set(), decomposable)
|
|
262
307
|
|
|
263
|
-
# Primitive leaves: characters in `refs` that NEVER get decomposed (i.e.,
|
|
264
|
-
# not in the `decomposable` set we just built). These stay in the
|
|
265
|
-
# primitive set; the rest get removed.
|
|
266
308
|
leaves_without_decomp = {
|
|
267
309
|
c for c in refs if c not in decomposable and _is_data_char(c)
|
|
268
310
|
}
|
|
269
311
|
|
|
270
|
-
# Append new
|
|
271
|
-
#
|
|
272
|
-
#
|
|
273
|
-
#
|
|
274
|
-
# new leaves — never remove from the curated set.
|
|
312
|
+
# Append new leaves to primitives.txt. Per the wikimedia commons
|
|
313
|
+
# atomicity principle (§6.3 of the spec), primitives.txt is the
|
|
314
|
+
# authoritative atomicity definition — we only ADD new leaves; never
|
|
315
|
+
# remove from the curated set.
|
|
275
316
|
primitives_path = (
|
|
276
|
-
Path(__file__).parent.parent / "src" / "chinese_decompose" / "data" / "primitives.
|
|
317
|
+
Path(__file__).parent.parent / "src" / "chinese_decompose" / "data" / "primitives.txt"
|
|
277
318
|
)
|
|
278
|
-
|
|
279
|
-
existing = set(json.loads(primitives_path.read_text(encoding="utf-8"))["primitives"])
|
|
280
|
-
else:
|
|
281
|
-
existing = set()
|
|
319
|
+
existing = _load_existing_primitives_txt(primitives_path)
|
|
282
320
|
new_primitives = sorted(existing | leaves_without_decomp)
|
|
321
|
+
primitives_lines = [
|
|
322
|
+
"# chinese-decompose primitives (authoritative atomic components per wikimedia commons view)",
|
|
323
|
+
"# Format: one character per line. Lines starting with # are comments.",
|
|
324
|
+
"# Characters listed here are treated as atomic by the walker regardless",
|
|
325
|
+
"# of any deeper structure recorded in decomp_*.txt.",
|
|
326
|
+
]
|
|
327
|
+
primitives_lines += new_primitives
|
|
283
328
|
primitives_path.write_text(
|
|
284
|
-
|
|
285
|
-
{"primitives": new_primitives},
|
|
286
|
-
ensure_ascii=False,
|
|
287
|
-
separators=(",", ":"),
|
|
288
|
-
),
|
|
289
|
-
encoding="utf-8",
|
|
329
|
+
"\n".join(primitives_lines) + "\n", encoding="utf-8"
|
|
290
330
|
)
|
|
291
331
|
added = len(new_primitives) - len(existing)
|
|
292
332
|
print(
|