cjk-semantic-split 0.1.2__tar.gz → 0.1.3__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.3/.hypothesis/constants/28b72037d99ba6eb +4 -0
- cjk_semantic_split-0.1.3/.hypothesis/constants/a80ac1b9c26466c7 +4 -0
- {cjk_semantic_split-0.1.2 → cjk_semantic_split-0.1.3}/PKG-INFO +1 -1
- {cjk_semantic_split-0.1.2 → cjk_semantic_split-0.1.3}/pyproject.toml +1 -1
- cjk_semantic_split-0.1.3/scripts/harvest.py +325 -0
- {cjk_semantic_split-0.1.2 → cjk_semantic_split-0.1.3}/src/chinese_decompose/__init__.py +1 -1
- cjk_semantic_split-0.1.3/src/chinese_decompose/data/decomp_l1.json +1 -0
- cjk_semantic_split-0.1.3/src/chinese_decompose/data/primitives.json +1 -0
- {cjk_semantic_split-0.1.2 → cjk_semantic_split-0.1.3}/src/chinese_decompose/structures.py +26 -0
- {cjk_semantic_split-0.1.2 → cjk_semantic_split-0.1.3}/tests/test_harvest.py +9 -3
- {cjk_semantic_split-0.1.2 → cjk_semantic_split-0.1.3}/tests/test_read_structure.py +9 -9
- {cjk_semantic_split-0.1.2 → cjk_semantic_split-0.1.3}/tests/test_smoke.py +1 -1
- cjk_semantic_split-0.1.2/scripts/harvest.py +0 -52
- cjk_semantic_split-0.1.2/src/chinese_decompose/data/decomp_l1.json +0 -12
- cjk_semantic_split-0.1.2/src/chinese_decompose/data/primitives.json +0 -21
- {cjk_semantic_split-0.1.2 → cjk_semantic_split-0.1.3}/.github/workflows/ci.yml +0 -0
- {cjk_semantic_split-0.1.2 → cjk_semantic_split-0.1.3}/.gitignore +0 -0
- {cjk_semantic_split-0.1.2 → cjk_semantic_split-0.1.3}/.hypothesis/.gitignore +0 -0
- {cjk_semantic_split-0.1.2 → cjk_semantic_split-0.1.3}/.hypothesis/constants/2b726390919ba924 +0 -0
- {cjk_semantic_split-0.1.2 → cjk_semantic_split-0.1.3}/.hypothesis/constants/3f14f0ebb32e0fff +0 -0
- {cjk_semantic_split-0.1.2 → cjk_semantic_split-0.1.3}/.hypothesis/constants/447154ffb3443956 +0 -0
- {cjk_semantic_split-0.1.2 → cjk_semantic_split-0.1.3}/.hypothesis/constants/448896e190e1a139 +0 -0
- {cjk_semantic_split-0.1.2 → cjk_semantic_split-0.1.3}/.hypothesis/constants/44fdbf8fc7dec723 +0 -0
- /cjk_semantic_split-0.1.2/.hypothesis/constants/a80ac1b9c26466c7 → /cjk_semantic_split-0.1.3/.hypothesis/constants/487391a4cba1e887 +0 -0
- {cjk_semantic_split-0.1.2 → cjk_semantic_split-0.1.3}/.hypothesis/constants/6dc13c365eb3deeb +0 -0
- {cjk_semantic_split-0.1.2 → cjk_semantic_split-0.1.3}/.hypothesis/constants/7d0a29d738e12257 +0 -0
- {cjk_semantic_split-0.1.2 → cjk_semantic_split-0.1.3}/.hypothesis/constants/a686eee4be3e3873 +0 -0
- {cjk_semantic_split-0.1.2 → cjk_semantic_split-0.1.3}/.hypothesis/constants/cd01bf70c5c5b577 +0 -0
- {cjk_semantic_split-0.1.2 → cjk_semantic_split-0.1.3}/.hypothesis/constants/dc1aa6aba7748109 +0 -0
- {cjk_semantic_split-0.1.2 → cjk_semantic_split-0.1.3}/.hypothesis/constants/e6674edc9b17fd2d +0 -0
- {cjk_semantic_split-0.1.2 → cjk_semantic_split-0.1.3}/.hypothesis/constants/eb3de9a3db1e637c +0 -0
- {cjk_semantic_split-0.1.2 → cjk_semantic_split-0.1.3}/.hypothesis/constants/f2e388b96604c388 +0 -0
- {cjk_semantic_split-0.1.2 → cjk_semantic_split-0.1.3}/.hypothesis/unicode_data/15.1.0/charmap.json.gz +0 -0
- {cjk_semantic_split-0.1.2 → cjk_semantic_split-0.1.3}/.superpowers/sdd/.gitignore +0 -0
- {cjk_semantic_split-0.1.2 → cjk_semantic_split-0.1.3}/.superpowers/sdd/progress.md +0 -0
- {cjk_semantic_split-0.1.2 → cjk_semantic_split-0.1.3}/.superpowers/sdd/review-22d28a8..564c3e6.diff +0 -0
- {cjk_semantic_split-0.1.2 → cjk_semantic_split-0.1.3}/.superpowers/sdd/review-564c3e6..0b4b3f6.diff +0 -0
- {cjk_semantic_split-0.1.2 → cjk_semantic_split-0.1.3}/.superpowers/sdd/task-1-brief.md +0 -0
- {cjk_semantic_split-0.1.2 → cjk_semantic_split-0.1.3}/.superpowers/sdd/task-1-report.md +0 -0
- {cjk_semantic_split-0.1.2 → cjk_semantic_split-0.1.3}/.superpowers/sdd/task-10-report.md +0 -0
- {cjk_semantic_split-0.1.2 → cjk_semantic_split-0.1.3}/.superpowers/sdd/task-11-report.md +0 -0
- {cjk_semantic_split-0.1.2 → cjk_semantic_split-0.1.3}/.superpowers/sdd/task-12-report.md +0 -0
- {cjk_semantic_split-0.1.2 → cjk_semantic_split-0.1.3}/.superpowers/sdd/task-13-report.md +0 -0
- {cjk_semantic_split-0.1.2 → cjk_semantic_split-0.1.3}/.superpowers/sdd/task-14-report.md +0 -0
- {cjk_semantic_split-0.1.2 → cjk_semantic_split-0.1.3}/.superpowers/sdd/task-15-report.md +0 -0
- {cjk_semantic_split-0.1.2 → cjk_semantic_split-0.1.3}/.superpowers/sdd/task-2-brief.md +0 -0
- {cjk_semantic_split-0.1.2 → cjk_semantic_split-0.1.3}/.superpowers/sdd/task-2-report.md +0 -0
- {cjk_semantic_split-0.1.2 → cjk_semantic_split-0.1.3}/.superpowers/sdd/task-3-brief.md +0 -0
- {cjk_semantic_split-0.1.2 → cjk_semantic_split-0.1.3}/.superpowers/sdd/task-3-report.md +0 -0
- {cjk_semantic_split-0.1.2 → cjk_semantic_split-0.1.3}/.superpowers/sdd/task-4-report.md +0 -0
- {cjk_semantic_split-0.1.2 → cjk_semantic_split-0.1.3}/.superpowers/sdd/task-5-report.md +0 -0
- {cjk_semantic_split-0.1.2 → cjk_semantic_split-0.1.3}/.superpowers/sdd/task-6-report.md +0 -0
- {cjk_semantic_split-0.1.2 → cjk_semantic_split-0.1.3}/.superpowers/sdd/task-7-report.md +0 -0
- {cjk_semantic_split-0.1.2 → cjk_semantic_split-0.1.3}/.superpowers/sdd/task-8-report.md +0 -0
- {cjk_semantic_split-0.1.2 → cjk_semantic_split-0.1.3}/.superpowers/sdd/task-9-report.md +0 -0
- {cjk_semantic_split-0.1.2 → cjk_semantic_split-0.1.3}/LICENSE +0 -0
- {cjk_semantic_split-0.1.2 → cjk_semantic_split-0.1.3}/README.md +0 -0
- {cjk_semantic_split-0.1.2 → cjk_semantic_split-0.1.3}/docs/component-position-encoding.md +0 -0
- {cjk_semantic_split-0.1.2 → cjk_semantic_split-0.1.3}/docs/decomposition-logic.md +0 -0
- {cjk_semantic_split-0.1.2 → cjk_semantic_split-0.1.3}/docs/superpowers/plans/2026-07-16-cjk-semantic-split-implementation.md +0 -0
- {cjk_semantic_split-0.1.2 → cjk_semantic_split-0.1.3}/docs/superpowers/specs/2026-07-16-cjk-semantic-split-design.md +0 -0
- {cjk_semantic_split-0.1.2 → cjk_semantic_split-0.1.3}/scripts/__init__.py +0 -0
- {cjk_semantic_split-0.1.2 → cjk_semantic_split-0.1.3}/scripts/review_conflicts.py +0 -0
- {cjk_semantic_split-0.1.2 → cjk_semantic_split-0.1.3}/src/chinese_decompose/__main__.py +0 -0
- {cjk_semantic_split-0.1.2 → cjk_semantic_split-0.1.3}/src/chinese_decompose/api.py +0 -0
- {cjk_semantic_split-0.1.2 → cjk_semantic_split-0.1.3}/src/chinese_decompose/data/decomp_l2.json +0 -0
- {cjk_semantic_split-0.1.2 → cjk_semantic_split-0.1.3}/src/chinese_decompose/data/decomp_l3.json +0 -0
- {cjk_semantic_split-0.1.2 → cjk_semantic_split-0.1.3}/src/chinese_decompose/data/decomp_patches.json +0 -0
- {cjk_semantic_split-0.1.2 → cjk_semantic_split-0.1.3}/src/chinese_decompose/dsl.py +0 -0
- {cjk_semantic_split-0.1.2 → cjk_semantic_split-0.1.3}/src/chinese_decompose/errors.py +0 -0
- {cjk_semantic_split-0.1.2 → cjk_semantic_split-0.1.3}/src/chinese_decompose/loader.py +0 -0
- {cjk_semantic_split-0.1.2 → cjk_semantic_split-0.1.3}/src/chinese_decompose/primitives.py +0 -0
- {cjk_semantic_split-0.1.2 → cjk_semantic_split-0.1.3}/src/chinese_decompose/split.py +0 -0
- {cjk_semantic_split-0.1.2 → cjk_semantic_split-0.1.3}/tests/__init__.py +0 -0
- {cjk_semantic_split-0.1.2 → cjk_semantic_split-0.1.3}/tests/conftest.py +0 -0
- {cjk_semantic_split-0.1.2 → cjk_semantic_split-0.1.3}/tests/test_api.py +0 -0
- {cjk_semantic_split-0.1.2 → cjk_semantic_split-0.1.3}/tests/test_cli.py +0 -0
- {cjk_semantic_split-0.1.2 → cjk_semantic_split-0.1.3}/tests/test_compose.py +0 -0
- {cjk_semantic_split-0.1.2 → cjk_semantic_split-0.1.3}/tests/test_decompose.py +0 -0
- {cjk_semantic_split-0.1.2 → cjk_semantic_split-0.1.3}/tests/test_dsl.py +0 -0
- {cjk_semantic_split-0.1.2 → cjk_semantic_split-0.1.3}/tests/test_loader.py +0 -0
- {cjk_semantic_split-0.1.2 → cjk_semantic_split-0.1.3}/tests/test_primitives.py +0 -0
- {cjk_semantic_split-0.1.2 → cjk_semantic_split-0.1.3}/tests/test_properties.py +0 -0
- {cjk_semantic_split-0.1.2 → cjk_semantic_split-0.1.3}/tests/test_split.py +0 -0
- {cjk_semantic_split-0.1.2 → cjk_semantic_split-0.1.3}/tests/test_structures.py +0 -0
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
# file: /Users/mac/Develop/gitstore/full_brain_intelligence/Semantic-Character-Splitting-and-Structural-Coding-for-Chinese-Script/src/chinese_decompose/__init__.py
|
|
2
|
+
# hypothesis_version: 6.156.6
|
|
3
|
+
|
|
4
|
+
['0.1.3', 'CompositionError', 'DecompositionTree', 'Loader', 'VALID_POSITIONS', '__version__', 'clear_cache', 'compose', 'decompose', 'get_primitives', 'is_primitive', 'make_loader', 'parse_dsl', 'read_structure', 'reset_primitives', 'serialize_tree', 'set_primitives', 'split_one']
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: cjk-semantic-split
|
|
3
|
-
Version: 0.1.
|
|
3
|
+
Version: 0.1.3
|
|
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.1.
|
|
7
|
+
version = "0.1.3"
|
|
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"
|
|
@@ -0,0 +1,325 @@
|
|
|
1
|
+
"""Offline data pipeline (spec §4.3, §9.2).
|
|
2
|
+
|
|
3
|
+
Fetches CJK decomposition data from a maintained IDS (Ideographic Description
|
|
4
|
+
Sequences) dataset on jsDelivr CDN (mirroring github.com/yi-bai/ids). The
|
|
5
|
+
dataset is MIT-licensed and covers ~102k CJK characters using Unicode IDC
|
|
6
|
+
notation (⿰⿱⿴⿵⿶⿷⿸⿹⿺, plus 3-cell and overlaid variants we skip).
|
|
7
|
+
|
|
8
|
+
Usage:
|
|
9
|
+
python -m scripts.harvest --tier l1 \
|
|
10
|
+
--output ../src/chinese_decompose/data/decomp_l1.json
|
|
11
|
+
|
|
12
|
+
Source file format (tab-separated):
|
|
13
|
+
|
|
14
|
+
<char>\t<IDC-1>\t<IDC-2>\t...
|
|
15
|
+
|
|
16
|
+
Multiple alternative decompositions (when present) are joined by ";". We take
|
|
17
|
+
the first alternative. Trailing "t" / "K" / "H" / "d" suffixes on each atom
|
|
18
|
+
indicate shape variants — we strip them.
|
|
19
|
+
|
|
20
|
+
Sub-graphs within a parent IDC (e.g. ⿱⿰日月一) are recursively expanded and
|
|
21
|
+
component positions composed via the parent ⊙ child rule (see
|
|
22
|
+
``docs/component-position-encoding.md``).
|
|
23
|
+
|
|
24
|
+
IDC → position table (2-cell only supported in v0.x):
|
|
25
|
+
|
|
26
|
+
⿰ A B → A at 30, B at 40 (left-right)
|
|
27
|
+
⿱ A B → A at 10, B at 20 (top-bottom)
|
|
28
|
+
⿴ A B → A at 90, B at 0 (full surround)
|
|
29
|
+
⿵ A B → A at 93, B at 0 (surround from above)
|
|
30
|
+
⿶ A B → A at 91, B at 0 (surround from below)
|
|
31
|
+
⿷ A B → A at 92, B at 0 (surround from left)
|
|
32
|
+
⿸ A B → A at 95, B at 0 (surround from upper-left)
|
|
33
|
+
⿹ A B → A at 97, B at 0 (surround from upper-right)
|
|
34
|
+
⿺ A B → A at 99, B at 0 (surround from lower-left)
|
|
35
|
+
A B → A at 94, B at 0 (surround from right)
|
|
36
|
+
A B → A at 96, B at 0 (surround from lower-right)
|
|
37
|
+
|
|
38
|
+
3-cell horizontal (⿲), 3-cell vertical (⿳), and overlaid (⿻//) entries
|
|
39
|
+
are skipped — they require additional position slots outside v0.x schema.
|
|
40
|
+
"""
|
|
41
|
+
from __future__ import annotations
|
|
42
|
+
|
|
43
|
+
import argparse
|
|
44
|
+
import io
|
|
45
|
+
import json
|
|
46
|
+
import re
|
|
47
|
+
import sys
|
|
48
|
+
import urllib.request
|
|
49
|
+
import zipfile
|
|
50
|
+
from pathlib import Path
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def _import_for_compose():
|
|
54
|
+
from chinese_decompose.structures import compose as _compose
|
|
55
|
+
from chinese_decompose.errors import InvalidPositionCodeError
|
|
56
|
+
return _compose, InvalidPositionCodeError
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
# 2-cell IDC codes → [pos_for_left/top/wall, pos_for_right/bottom/content]
|
|
60
|
+
IDC_TABLE_2_CELL: dict[str, list[int]] = {
|
|
61
|
+
"⿰": [30, 40], # ⿰ left-right
|
|
62
|
+
"⿱": [10, 20], # ⿱ top-bottom
|
|
63
|
+
"⿴": [90, 0], # ⿴ full surround
|
|
64
|
+
"⿵": [93, 0], # ⿵ surround above
|
|
65
|
+
"⿶": [91, 0], # ⿶ surround below
|
|
66
|
+
"⿷": [92, 0], # ⿷ surround left
|
|
67
|
+
"⿸": [95, 0], # ⿸ surround upper-left
|
|
68
|
+
"⿹": [97, 0], # ⿹ surround upper-right
|
|
69
|
+
"⿺": [99, 0], # ⿺ surround lower-left
|
|
70
|
+
"": [94, 0], # surround right (extension)
|
|
71
|
+
"": [96, 0], # surround lower-right (extension)
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
# Leaf characters that are recognized as atoms in the IDS source. CJK Unified
|
|
75
|
+
# Ideographs, Extension A, Extensions B–F (supplementary plane), and the CJK
|
|
76
|
+
# Compatibility Ideographs block. Strokes (single code points in 1E000+) and
|
|
77
|
+
# other auxiliary blocks are excluded — they would dilute the primitives set.
|
|
78
|
+
def _is_data_char(c: str) -> bool:
|
|
79
|
+
cp = ord(c)
|
|
80
|
+
return (
|
|
81
|
+
0x3400 <= cp <= 0x4DBF # CJK Extension A
|
|
82
|
+
or 0x4E00 <= cp <= 0x9FFF # CJK Unified Ideographs
|
|
83
|
+
or 0x20000 <= cp <= 0x2FFFF # CJK Extensions B–F + supplementary
|
|
84
|
+
or 0xF900 <= cp <= 0xFAFF # CJK Compatibility Ideographs
|
|
85
|
+
)
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
# Strip trailing shape-variant markers: t, K, H, d, etc.
|
|
89
|
+
# Examples from the source: "一.(T)" → "一"; "一t(t)" → "一"; "乚w一" → "乚, 一" (split)
|
|
90
|
+
# The "w" / "d" / "K" / "t" / "H" markers indicate variant positioning. We strip
|
|
91
|
+
# them and the surrounding parens, leaving just the CJK characters.
|
|
92
|
+
_VARIANT_MARKER = re.compile(r"[\(\)\[\]]")
|
|
93
|
+
_VARIANT_LETTER = re.compile(r"[a-zA-Z]")
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
def _clean_atom_string(s: str) -> str:
|
|
97
|
+
"""Strip parens, letters, dots, and other markers from an IDS atom segment.
|
|
98
|
+
|
|
99
|
+
Returns only the CJK / IDC characters. Variants like '一t(t)' → '一'.
|
|
100
|
+
"""
|
|
101
|
+
out_chars = []
|
|
102
|
+
for c in s:
|
|
103
|
+
if _is_data_char(c) or c in IDC_TABLE_2_CELL:
|
|
104
|
+
out_chars.append(c)
|
|
105
|
+
# else: drop letters, parens, dots, etc.
|
|
106
|
+
return "".join(out_chars)
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
def _consume_one(s: str) -> tuple[str | None, str]:
|
|
110
|
+
"""Consume one component from the start of s.
|
|
111
|
+
|
|
112
|
+
Returns (component_string, remaining). The component is either a single
|
|
113
|
+
CJK character or an entire nested IDC subtree.
|
|
114
|
+
"""
|
|
115
|
+
if not s:
|
|
116
|
+
return None, ""
|
|
117
|
+
c = s[0]
|
|
118
|
+
if c in IDC_TABLE_2_CELL:
|
|
119
|
+
rest = s[1:]
|
|
120
|
+
for _ in IDC_TABLE_2_CELL[c]:
|
|
121
|
+
child, rest = _consume_one(rest)
|
|
122
|
+
if child is None:
|
|
123
|
+
return None, ""
|
|
124
|
+
return c, rest
|
|
125
|
+
if _is_data_char(c):
|
|
126
|
+
return c, s[1:]
|
|
127
|
+
return None, ""
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
def _walk_kids(
|
|
131
|
+
s: str,
|
|
132
|
+
parent_position: int,
|
|
133
|
+
out: list[tuple[str, int]],
|
|
134
|
+
compose_fn,
|
|
135
|
+
) -> bool:
|
|
136
|
+
"""Recursively expand an IDS string into one-level (atom, final_position) pairs.
|
|
137
|
+
|
|
138
|
+
Returns True on success, False if input contains unsupported IDC pattern
|
|
139
|
+
(caller should skip).
|
|
140
|
+
"""
|
|
141
|
+
if not s:
|
|
142
|
+
return True
|
|
143
|
+
if len(s) == 1:
|
|
144
|
+
return False
|
|
145
|
+
idc = s[0]
|
|
146
|
+
if idc not in IDC_TABLE_2_CELL:
|
|
147
|
+
# ⿲ ⿳ ⿻ not supported.
|
|
148
|
+
return False
|
|
149
|
+
rest = s[1:]
|
|
150
|
+
for local_pos in IDC_TABLE_2_CELL[idc]:
|
|
151
|
+
comp_str, rest = _consume_one(rest)
|
|
152
|
+
if comp_str is None:
|
|
153
|
+
return False
|
|
154
|
+
composed = compose_fn(parent_position, local_pos)
|
|
155
|
+
if len(comp_str) == 1:
|
|
156
|
+
out.append((comp_str, composed))
|
|
157
|
+
else:
|
|
158
|
+
# Recurse into subtree
|
|
159
|
+
if not _walk_kids(comp_str, composed, out, compose_fn):
|
|
160
|
+
return False
|
|
161
|
+
if rest:
|
|
162
|
+
return False
|
|
163
|
+
return True
|
|
164
|
+
|
|
165
|
+
|
|
166
|
+
def _parse_kids(s: str, compose_fn) -> list[tuple[str, int]] | None:
|
|
167
|
+
"""Parse IDS value, return one-level decomp list or None if unsupported."""
|
|
168
|
+
out: list[tuple[str, int]] = []
|
|
169
|
+
if _walk_kids(s, parent_position=0, out=out, compose_fn=compose_fn):
|
|
170
|
+
return out
|
|
171
|
+
return None
|
|
172
|
+
|
|
173
|
+
|
|
174
|
+
# --- IDS source download + parse ---
|
|
175
|
+
|
|
176
|
+
# jsDelivr CDN mirror of github.com/yi-bai/ids (MIT licensed).
|
|
177
|
+
IDS_SOURCES: dict[str, str] = {
|
|
178
|
+
"lv0": "https://cdn.jsdelivr.net/gh/yi-bai/ids@master/ids_lv0.txt",
|
|
179
|
+
"lv1": "https://cdn.jsdelivr.net/gh/yi-bai/ids@master/ids_lv1.txt",
|
|
180
|
+
"lv2": "https://cdn.jsdelivr.net/gh/yi-bai/ids@master/ids_lv2.txt",
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
|
|
184
|
+
def fetch_text(url: str) -> str:
|
|
185
|
+
req = urllib.request.Request(url, headers={"User-Agent": "chinese_decompose/0.1"})
|
|
186
|
+
with urllib.request.urlopen(req, timeout=120) as resp:
|
|
187
|
+
return resp.read().decode("utf-8")
|
|
188
|
+
|
|
189
|
+
|
|
190
|
+
def parse_ids_source(text: str, compose_fn) -> tuple[dict[str, list[tuple[str, int]]], set[str]]:
|
|
191
|
+
"""Parse yi-bai/ids .txt format, return (entries, primitive_refs).
|
|
192
|
+
|
|
193
|
+
Format per line: ``<char>\\t<ids-alt-1>\\t<ids-alt-2>...``
|
|
194
|
+
|
|
195
|
+
Each ``ids-alt-N`` is an IDS expression possibly followed by ";alt2;alt3...".
|
|
196
|
+
We pick the first alt that parses cleanly.
|
|
197
|
+
"""
|
|
198
|
+
entries: dict[str, list[tuple[str, int]]] = {}
|
|
199
|
+
refs: set[str] = set()
|
|
200
|
+
for line in text.splitlines():
|
|
201
|
+
if not line.strip():
|
|
202
|
+
continue
|
|
203
|
+
parts = line.split("\t")
|
|
204
|
+
char_field = parts[0]
|
|
205
|
+
if len(char_field) != 1 or not _is_data_char(char_field):
|
|
206
|
+
continue
|
|
207
|
+
char = char_field
|
|
208
|
+
# Pick first alt that parses
|
|
209
|
+
for ids_field in parts[1:]:
|
|
210
|
+
# Take only the first alt (before ';')
|
|
211
|
+
first_alt = ids_field.split(";", 1)[0]
|
|
212
|
+
cleaned = _clean_atom_string(first_alt)
|
|
213
|
+
if not cleaned:
|
|
214
|
+
continue
|
|
215
|
+
result = _parse_kids(cleaned, compose_fn)
|
|
216
|
+
if result is not None and result:
|
|
217
|
+
entries[char] = result
|
|
218
|
+
for c, _ in result:
|
|
219
|
+
refs.add(c)
|
|
220
|
+
break
|
|
221
|
+
return entries, refs
|
|
222
|
+
|
|
223
|
+
|
|
224
|
+
def emit_tier1(output_path: Path, compose_fn) -> dict:
|
|
225
|
+
"""Download IDS dataset, parse, write decomp_l1.json and extend primitives.json."""
|
|
226
|
+
print(f"Downloading {IDS_SOURCES['lv0']}...", file=sys.stderr)
|
|
227
|
+
text = fetch_text(IDS_SOURCES["lv0"])
|
|
228
|
+
print(f" fetched {len(text.splitlines())} lines", file=sys.stderr)
|
|
229
|
+
|
|
230
|
+
print("Parsing IDS entries...", file=sys.stderr)
|
|
231
|
+
entries, refs = parse_ids_source(text, compose_fn)
|
|
232
|
+
print(f" entries: {len(entries)}, primitive refs: {len(refs)}", file=sys.stderr)
|
|
233
|
+
|
|
234
|
+
output_path.parent.mkdir(parents=True, exist_ok=True)
|
|
235
|
+
payload = {"_meta": {"tier": 1, "source": "github.com/yi-bai/ids", "license": "MIT"}}
|
|
236
|
+
payload.update({char: [[c, p] for c, p in v] for char, v in entries.items()})
|
|
237
|
+
output_path.write_text(
|
|
238
|
+
json.dumps(payload, ensure_ascii=False, separators=(",", ":")), encoding="utf-8"
|
|
239
|
+
)
|
|
240
|
+
size_kb = output_path.stat().st_size // 1024
|
|
241
|
+
print(f" wrote {output_path} ({size_kb} KB)", file=sys.stderr)
|
|
242
|
+
|
|
243
|
+
# Find all characters that get further decomposed in `entries`
|
|
244
|
+
# (recursively, since chars may appear nested). These MUST be removed
|
|
245
|
+
# from the primitive set so walker behavior is unambiguous — if a char
|
|
246
|
+
# is in both primitives and l1, l1 wins (per last-wins loader order).
|
|
247
|
+
def _collect_decomposable(char: str, seen: set[str], into: set[str]) -> None:
|
|
248
|
+
if char in seen:
|
|
249
|
+
return
|
|
250
|
+
seen.add(char)
|
|
251
|
+
direct = entries.get(char)
|
|
252
|
+
if direct is None:
|
|
253
|
+
return
|
|
254
|
+
into.add(char)
|
|
255
|
+
for c, _ in direct:
|
|
256
|
+
if _is_data_char(c) and len(c) == 1:
|
|
257
|
+
_collect_decomposable(c, seen, into)
|
|
258
|
+
|
|
259
|
+
decomposable: set[str] = set()
|
|
260
|
+
for char in entries:
|
|
261
|
+
_collect_decomposable(char, set(), decomposable)
|
|
262
|
+
|
|
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
|
+
leaves_without_decomp = {
|
|
267
|
+
c for c in refs if c not in decomposable and _is_data_char(c)
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
# Append new primitives to primitives.json. The bundled primitives.json is
|
|
271
|
+
# the AUTHORITATIVE atomicity definition (per wikimedia commons' visual
|
|
272
|
+
# decomposition view): any character in it is treated as atomic, even if
|
|
273
|
+
# the IDS source happens to have a further decomposition. We only ADD
|
|
274
|
+
# new leaves — never remove from the curated set.
|
|
275
|
+
primitives_path = (
|
|
276
|
+
Path(__file__).parent.parent / "src" / "chinese_decompose" / "data" / "primitives.json"
|
|
277
|
+
)
|
|
278
|
+
if primitives_path.exists():
|
|
279
|
+
existing = set(json.loads(primitives_path.read_text(encoding="utf-8"))["primitives"])
|
|
280
|
+
else:
|
|
281
|
+
existing = set()
|
|
282
|
+
new_primitives = sorted(existing | leaves_without_decomp)
|
|
283
|
+
primitives_path.write_text(
|
|
284
|
+
json.dumps(
|
|
285
|
+
{"primitives": new_primitives},
|
|
286
|
+
ensure_ascii=False,
|
|
287
|
+
separators=(",", ":"),
|
|
288
|
+
),
|
|
289
|
+
encoding="utf-8",
|
|
290
|
+
)
|
|
291
|
+
added = len(new_primitives) - len(existing)
|
|
292
|
+
print(
|
|
293
|
+
f" appended {added} new primitives (leaves without IDS expansion); "
|
|
294
|
+
f"total = {len(new_primitives)} "
|
|
295
|
+
f"({primitives_path.stat().st_size // 1024} KB)",
|
|
296
|
+
file=sys.stderr,
|
|
297
|
+
)
|
|
298
|
+
return entries
|
|
299
|
+
|
|
300
|
+
|
|
301
|
+
def main(argv: list[str] | None = None) -> int:
|
|
302
|
+
parser = argparse.ArgumentParser(description="Harvest CJK decomposition data.")
|
|
303
|
+
parser.add_argument("--output", type=Path, required=True, help="Output JSON path.")
|
|
304
|
+
parser.add_argument(
|
|
305
|
+
"--tier",
|
|
306
|
+
choices=["l1"],
|
|
307
|
+
default="l1",
|
|
308
|
+
help="Which tier to harvest. Currently only 'l1' (yi-bai/ids lv0) is supported.",
|
|
309
|
+
)
|
|
310
|
+
args = parser.parse_args(argv)
|
|
311
|
+
|
|
312
|
+
if args.tier != "l1":
|
|
313
|
+
print(
|
|
314
|
+
f"Tier {args.tier!r} not implemented; only 'l1' is currently supported.",
|
|
315
|
+
file=sys.stderr,
|
|
316
|
+
)
|
|
317
|
+
return 2
|
|
318
|
+
|
|
319
|
+
compose_fn, _ = _import_for_compose()
|
|
320
|
+
emit_tier1(args.output, compose_fn)
|
|
321
|
+
return 0
|
|
322
|
+
|
|
323
|
+
|
|
324
|
+
if __name__ == "__main__":
|
|
325
|
+
sys.exit(main())
|