logiglyph 0.1.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.
- logiglyph-0.1.0/LICENSE +22 -0
- logiglyph-0.1.0/PKG-INFO +144 -0
- logiglyph-0.1.0/README.md +125 -0
- logiglyph-0.1.0/logiglyph/__init__.py +6 -0
- logiglyph-0.1.0/logiglyph/__main__.py +6 -0
- logiglyph-0.1.0/logiglyph/cli.py +4 -0
- logiglyph-0.1.0/logiglyph.egg-info/PKG-INFO +144 -0
- logiglyph-0.1.0/logiglyph.egg-info/SOURCES.txt +20 -0
- logiglyph-0.1.0/logiglyph.egg-info/dependency_links.txt +1 -0
- logiglyph-0.1.0/logiglyph.egg-info/entry_points.txt +2 -0
- logiglyph-0.1.0/logiglyph.egg-info/requires.txt +2 -0
- logiglyph-0.1.0/logiglyph.egg-info/top_level.txt +2 -0
- logiglyph-0.1.0/pyproject.toml +30 -0
- logiglyph-0.1.0/setup.cfg +4 -0
- logiglyph-0.1.0/src/__init__.py +5 -0
- logiglyph-0.1.0/src/api.py +40 -0
- logiglyph-0.1.0/src/dictionary_tool.py +191 -0
- logiglyph-0.1.0/src/font_builder.py +194 -0
- logiglyph-0.1.0/src/specimen.py +51 -0
- logiglyph-0.1.0/src/symbol_logic.py +90 -0
- logiglyph-0.1.0/src/tool.py +88 -0
- logiglyph-0.1.0/src/translator.py +47 -0
logiglyph-0.1.0/LICENSE
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Augusto
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
22
|
+
|
logiglyph-0.1.0/PKG-INFO
ADDED
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: logiglyph
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Agglutinative language toolkit with logical symbols, translator, and font generator.
|
|
5
|
+
Author: Augusto
|
|
6
|
+
License-Expression: MIT
|
|
7
|
+
Keywords: conlang,translator,font,agglutinative-language
|
|
8
|
+
Classifier: Programming Language :: Python :: 3
|
|
9
|
+
Classifier: Programming Language :: Python :: 3 :: Only
|
|
10
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
11
|
+
Classifier: Operating System :: OS Independent
|
|
12
|
+
Classifier: Topic :: Text Processing :: Linguistic
|
|
13
|
+
Requires-Python: >=3.10
|
|
14
|
+
Description-Content-Type: text/markdown
|
|
15
|
+
License-File: LICENSE
|
|
16
|
+
Requires-Dist: g4f>=0.3.2.7
|
|
17
|
+
Requires-Dist: fonttools>=4.56.0
|
|
18
|
+
Dynamic: license-file
|
|
19
|
+
|
|
20
|
+
# Agglutinative Language Toolkit
|
|
21
|
+
|
|
22
|
+
This project bootstraps a synthetic agglutinative language with:
|
|
23
|
+
|
|
24
|
+
- A GPT-powered dictionary generator and translator (`g4f` API)
|
|
25
|
+
- A logical symbol system where glyph form encodes meaning + sound
|
|
26
|
+
- A font generator that compiles symbols into a `.ttf` font
|
|
27
|
+
|
|
28
|
+
## 1) Setup
|
|
29
|
+
|
|
30
|
+
```powershell
|
|
31
|
+
python -m venv .venv
|
|
32
|
+
.venv\Scripts\Activate.ps1
|
|
33
|
+
pip install -r requirements.txt
|
|
34
|
+
pip install -e .
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
After editable install, you can use:
|
|
38
|
+
|
|
39
|
+
- `logiglyph ...` (console command)
|
|
40
|
+
- `python -m logiglyph ...`
|
|
41
|
+
- `from logiglyph import LanguageModule`
|
|
42
|
+
|
|
43
|
+
## 2) Generate dictionary
|
|
44
|
+
|
|
45
|
+
```powershell
|
|
46
|
+
python -m src.tool generate-dictionary --count 96 --out data/dictionary.json
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
This calls `g4f` (`gpt-5-mini`) and enriches each root with:
|
|
50
|
+
|
|
51
|
+
- `semantic_class`
|
|
52
|
+
- `onset`, `vowel`, `coda` (sound parts)
|
|
53
|
+
- `symbol_id` (logical glyph key)
|
|
54
|
+
- `char` (Private Use Area codepoint)
|
|
55
|
+
|
|
56
|
+
If API output is malformed/unavailable, the tool falls back to deterministic local roots.
|
|
57
|
+
|
|
58
|
+
## 3) Build font
|
|
59
|
+
|
|
60
|
+
```powershell
|
|
61
|
+
python -m src.tool build-font --dict data/dictionary.json --out dist/LogiGlyph.ttf
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
This creates:
|
|
65
|
+
|
|
66
|
+
- `dist/LogiGlyph.ttf`
|
|
67
|
+
- `dist/glyph_map.json`
|
|
68
|
+
|
|
69
|
+
## 4) Translate text
|
|
70
|
+
|
|
71
|
+
```powershell
|
|
72
|
+
python -m src.tool translate --dict data/dictionary.json --text "the person builds a house"
|
|
73
|
+
python -m src.tool translate --dict data/dictionary.json --text "" --reverse
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
## 5) Build specimen (visual check)
|
|
77
|
+
|
|
78
|
+
```powershell
|
|
79
|
+
python -m src.tool build-specimen --dict data/dictionary.json --font dist/LogiGlyph.ttf --out dist/specimen.html
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
Open `dist/specimen.html` in a browser. Keep `LogiGlyph.ttf` in the same folder so `@font-face` can load it.
|
|
83
|
+
|
|
84
|
+
## Font rendering troubleshooting
|
|
85
|
+
|
|
86
|
+
These symbols are in Private Use Area (`U+E100+`), so they only render when your app uses `LogiGlyph.ttf`.
|
|
87
|
+
|
|
88
|
+
- Terminal/editor showing squares or fallback chars: set font family to include `LogiGlyph`.
|
|
89
|
+
- Browser: use CSS `@font-face` and set `font-family: 'LogiGlyph'` on the target element.
|
|
90
|
+
- Windows app: install `dist/LogiGlyph.ttf` and select that font in the control/widget.
|
|
91
|
+
|
|
92
|
+
Example VS Code setting:
|
|
93
|
+
|
|
94
|
+
```json
|
|
95
|
+
{
|
|
96
|
+
"editor.fontFamily": "LogiGlyph, Consolas, 'Courier New', monospace"
|
|
97
|
+
}
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
## Use as a Python module
|
|
101
|
+
|
|
102
|
+
```python
|
|
103
|
+
from logiglyph import LanguageModule
|
|
104
|
+
|
|
105
|
+
lang = LanguageModule("data/dictionary.json")
|
|
106
|
+
res = lang.translate("person_0 house_1 water_2")
|
|
107
|
+
print(res.text) # e.g.
|
|
108
|
+
print(res.codepoints) # ['U+E100', 'U+E101', 'U+E102']
|
|
109
|
+
|
|
110
|
+
print(lang.reverse(res.text)) # person_0 house_1 water_2
|
|
111
|
+
print(lang.install_note())
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
## Logical symbol design
|
|
115
|
+
|
|
116
|
+
Each glyph is compositional and fully rule-driven:
|
|
117
|
+
|
|
118
|
+
- Semantic radical (base shape):
|
|
119
|
+
- `entity`: box
|
|
120
|
+
- `action`: diagonal cross
|
|
121
|
+
- `quality`: diamond
|
|
122
|
+
- `relation`: bridge bar
|
|
123
|
+
- `abstract`: circle-ish octagon
|
|
124
|
+
- Onset class adds a stem direction.
|
|
125
|
+
- Vowel places a dot mark in one of 5 anchor positions.
|
|
126
|
+
- Coda adds a terminal notch/tick style.
|
|
127
|
+
|
|
128
|
+
So related meanings and sounds share visible structure.
|
|
129
|
+
|
|
130
|
+
## Direct g4f usage
|
|
131
|
+
|
|
132
|
+
The dictionary generator internally uses this same API pattern:
|
|
133
|
+
|
|
134
|
+
```python
|
|
135
|
+
from g4f.client import Client
|
|
136
|
+
|
|
137
|
+
client = Client()
|
|
138
|
+
response = client.chat.completions.create(
|
|
139
|
+
model="gpt-5-mini",
|
|
140
|
+
messages=[{"role": "user", "content": "..." }],
|
|
141
|
+
web_search=False
|
|
142
|
+
)
|
|
143
|
+
print(response.choices[0].message.content)
|
|
144
|
+
```
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
# Agglutinative Language Toolkit
|
|
2
|
+
|
|
3
|
+
This project bootstraps a synthetic agglutinative language with:
|
|
4
|
+
|
|
5
|
+
- A GPT-powered dictionary generator and translator (`g4f` API)
|
|
6
|
+
- A logical symbol system where glyph form encodes meaning + sound
|
|
7
|
+
- A font generator that compiles symbols into a `.ttf` font
|
|
8
|
+
|
|
9
|
+
## 1) Setup
|
|
10
|
+
|
|
11
|
+
```powershell
|
|
12
|
+
python -m venv .venv
|
|
13
|
+
.venv\Scripts\Activate.ps1
|
|
14
|
+
pip install -r requirements.txt
|
|
15
|
+
pip install -e .
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
After editable install, you can use:
|
|
19
|
+
|
|
20
|
+
- `logiglyph ...` (console command)
|
|
21
|
+
- `python -m logiglyph ...`
|
|
22
|
+
- `from logiglyph import LanguageModule`
|
|
23
|
+
|
|
24
|
+
## 2) Generate dictionary
|
|
25
|
+
|
|
26
|
+
```powershell
|
|
27
|
+
python -m src.tool generate-dictionary --count 96 --out data/dictionary.json
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
This calls `g4f` (`gpt-5-mini`) and enriches each root with:
|
|
31
|
+
|
|
32
|
+
- `semantic_class`
|
|
33
|
+
- `onset`, `vowel`, `coda` (sound parts)
|
|
34
|
+
- `symbol_id` (logical glyph key)
|
|
35
|
+
- `char` (Private Use Area codepoint)
|
|
36
|
+
|
|
37
|
+
If API output is malformed/unavailable, the tool falls back to deterministic local roots.
|
|
38
|
+
|
|
39
|
+
## 3) Build font
|
|
40
|
+
|
|
41
|
+
```powershell
|
|
42
|
+
python -m src.tool build-font --dict data/dictionary.json --out dist/LogiGlyph.ttf
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
This creates:
|
|
46
|
+
|
|
47
|
+
- `dist/LogiGlyph.ttf`
|
|
48
|
+
- `dist/glyph_map.json`
|
|
49
|
+
|
|
50
|
+
## 4) Translate text
|
|
51
|
+
|
|
52
|
+
```powershell
|
|
53
|
+
python -m src.tool translate --dict data/dictionary.json --text "the person builds a house"
|
|
54
|
+
python -m src.tool translate --dict data/dictionary.json --text "" --reverse
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
## 5) Build specimen (visual check)
|
|
58
|
+
|
|
59
|
+
```powershell
|
|
60
|
+
python -m src.tool build-specimen --dict data/dictionary.json --font dist/LogiGlyph.ttf --out dist/specimen.html
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
Open `dist/specimen.html` in a browser. Keep `LogiGlyph.ttf` in the same folder so `@font-face` can load it.
|
|
64
|
+
|
|
65
|
+
## Font rendering troubleshooting
|
|
66
|
+
|
|
67
|
+
These symbols are in Private Use Area (`U+E100+`), so they only render when your app uses `LogiGlyph.ttf`.
|
|
68
|
+
|
|
69
|
+
- Terminal/editor showing squares or fallback chars: set font family to include `LogiGlyph`.
|
|
70
|
+
- Browser: use CSS `@font-face` and set `font-family: 'LogiGlyph'` on the target element.
|
|
71
|
+
- Windows app: install `dist/LogiGlyph.ttf` and select that font in the control/widget.
|
|
72
|
+
|
|
73
|
+
Example VS Code setting:
|
|
74
|
+
|
|
75
|
+
```json
|
|
76
|
+
{
|
|
77
|
+
"editor.fontFamily": "LogiGlyph, Consolas, 'Courier New', monospace"
|
|
78
|
+
}
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
## Use as a Python module
|
|
82
|
+
|
|
83
|
+
```python
|
|
84
|
+
from logiglyph import LanguageModule
|
|
85
|
+
|
|
86
|
+
lang = LanguageModule("data/dictionary.json")
|
|
87
|
+
res = lang.translate("person_0 house_1 water_2")
|
|
88
|
+
print(res.text) # e.g.
|
|
89
|
+
print(res.codepoints) # ['U+E100', 'U+E101', 'U+E102']
|
|
90
|
+
|
|
91
|
+
print(lang.reverse(res.text)) # person_0 house_1 water_2
|
|
92
|
+
print(lang.install_note())
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
## Logical symbol design
|
|
96
|
+
|
|
97
|
+
Each glyph is compositional and fully rule-driven:
|
|
98
|
+
|
|
99
|
+
- Semantic radical (base shape):
|
|
100
|
+
- `entity`: box
|
|
101
|
+
- `action`: diagonal cross
|
|
102
|
+
- `quality`: diamond
|
|
103
|
+
- `relation`: bridge bar
|
|
104
|
+
- `abstract`: circle-ish octagon
|
|
105
|
+
- Onset class adds a stem direction.
|
|
106
|
+
- Vowel places a dot mark in one of 5 anchor positions.
|
|
107
|
+
- Coda adds a terminal notch/tick style.
|
|
108
|
+
|
|
109
|
+
So related meanings and sounds share visible structure.
|
|
110
|
+
|
|
111
|
+
## Direct g4f usage
|
|
112
|
+
|
|
113
|
+
The dictionary generator internally uses this same API pattern:
|
|
114
|
+
|
|
115
|
+
```python
|
|
116
|
+
from g4f.client import Client
|
|
117
|
+
|
|
118
|
+
client = Client()
|
|
119
|
+
response = client.chat.completions.create(
|
|
120
|
+
model="gpt-5-mini",
|
|
121
|
+
messages=[{"role": "user", "content": "..." }],
|
|
122
|
+
web_search=False
|
|
123
|
+
)
|
|
124
|
+
print(response.choices[0].message.content)
|
|
125
|
+
```
|
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: logiglyph
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Agglutinative language toolkit with logical symbols, translator, and font generator.
|
|
5
|
+
Author: Augusto
|
|
6
|
+
License-Expression: MIT
|
|
7
|
+
Keywords: conlang,translator,font,agglutinative-language
|
|
8
|
+
Classifier: Programming Language :: Python :: 3
|
|
9
|
+
Classifier: Programming Language :: Python :: 3 :: Only
|
|
10
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
11
|
+
Classifier: Operating System :: OS Independent
|
|
12
|
+
Classifier: Topic :: Text Processing :: Linguistic
|
|
13
|
+
Requires-Python: >=3.10
|
|
14
|
+
Description-Content-Type: text/markdown
|
|
15
|
+
License-File: LICENSE
|
|
16
|
+
Requires-Dist: g4f>=0.3.2.7
|
|
17
|
+
Requires-Dist: fonttools>=4.56.0
|
|
18
|
+
Dynamic: license-file
|
|
19
|
+
|
|
20
|
+
# Agglutinative Language Toolkit
|
|
21
|
+
|
|
22
|
+
This project bootstraps a synthetic agglutinative language with:
|
|
23
|
+
|
|
24
|
+
- A GPT-powered dictionary generator and translator (`g4f` API)
|
|
25
|
+
- A logical symbol system where glyph form encodes meaning + sound
|
|
26
|
+
- A font generator that compiles symbols into a `.ttf` font
|
|
27
|
+
|
|
28
|
+
## 1) Setup
|
|
29
|
+
|
|
30
|
+
```powershell
|
|
31
|
+
python -m venv .venv
|
|
32
|
+
.venv\Scripts\Activate.ps1
|
|
33
|
+
pip install -r requirements.txt
|
|
34
|
+
pip install -e .
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
After editable install, you can use:
|
|
38
|
+
|
|
39
|
+
- `logiglyph ...` (console command)
|
|
40
|
+
- `python -m logiglyph ...`
|
|
41
|
+
- `from logiglyph import LanguageModule`
|
|
42
|
+
|
|
43
|
+
## 2) Generate dictionary
|
|
44
|
+
|
|
45
|
+
```powershell
|
|
46
|
+
python -m src.tool generate-dictionary --count 96 --out data/dictionary.json
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
This calls `g4f` (`gpt-5-mini`) and enriches each root with:
|
|
50
|
+
|
|
51
|
+
- `semantic_class`
|
|
52
|
+
- `onset`, `vowel`, `coda` (sound parts)
|
|
53
|
+
- `symbol_id` (logical glyph key)
|
|
54
|
+
- `char` (Private Use Area codepoint)
|
|
55
|
+
|
|
56
|
+
If API output is malformed/unavailable, the tool falls back to deterministic local roots.
|
|
57
|
+
|
|
58
|
+
## 3) Build font
|
|
59
|
+
|
|
60
|
+
```powershell
|
|
61
|
+
python -m src.tool build-font --dict data/dictionary.json --out dist/LogiGlyph.ttf
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
This creates:
|
|
65
|
+
|
|
66
|
+
- `dist/LogiGlyph.ttf`
|
|
67
|
+
- `dist/glyph_map.json`
|
|
68
|
+
|
|
69
|
+
## 4) Translate text
|
|
70
|
+
|
|
71
|
+
```powershell
|
|
72
|
+
python -m src.tool translate --dict data/dictionary.json --text "the person builds a house"
|
|
73
|
+
python -m src.tool translate --dict data/dictionary.json --text "" --reverse
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
## 5) Build specimen (visual check)
|
|
77
|
+
|
|
78
|
+
```powershell
|
|
79
|
+
python -m src.tool build-specimen --dict data/dictionary.json --font dist/LogiGlyph.ttf --out dist/specimen.html
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
Open `dist/specimen.html` in a browser. Keep `LogiGlyph.ttf` in the same folder so `@font-face` can load it.
|
|
83
|
+
|
|
84
|
+
## Font rendering troubleshooting
|
|
85
|
+
|
|
86
|
+
These symbols are in Private Use Area (`U+E100+`), so they only render when your app uses `LogiGlyph.ttf`.
|
|
87
|
+
|
|
88
|
+
- Terminal/editor showing squares or fallback chars: set font family to include `LogiGlyph`.
|
|
89
|
+
- Browser: use CSS `@font-face` and set `font-family: 'LogiGlyph'` on the target element.
|
|
90
|
+
- Windows app: install `dist/LogiGlyph.ttf` and select that font in the control/widget.
|
|
91
|
+
|
|
92
|
+
Example VS Code setting:
|
|
93
|
+
|
|
94
|
+
```json
|
|
95
|
+
{
|
|
96
|
+
"editor.fontFamily": "LogiGlyph, Consolas, 'Courier New', monospace"
|
|
97
|
+
}
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
## Use as a Python module
|
|
101
|
+
|
|
102
|
+
```python
|
|
103
|
+
from logiglyph import LanguageModule
|
|
104
|
+
|
|
105
|
+
lang = LanguageModule("data/dictionary.json")
|
|
106
|
+
res = lang.translate("person_0 house_1 water_2")
|
|
107
|
+
print(res.text) # e.g.
|
|
108
|
+
print(res.codepoints) # ['U+E100', 'U+E101', 'U+E102']
|
|
109
|
+
|
|
110
|
+
print(lang.reverse(res.text)) # person_0 house_1 water_2
|
|
111
|
+
print(lang.install_note())
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
## Logical symbol design
|
|
115
|
+
|
|
116
|
+
Each glyph is compositional and fully rule-driven:
|
|
117
|
+
|
|
118
|
+
- Semantic radical (base shape):
|
|
119
|
+
- `entity`: box
|
|
120
|
+
- `action`: diagonal cross
|
|
121
|
+
- `quality`: diamond
|
|
122
|
+
- `relation`: bridge bar
|
|
123
|
+
- `abstract`: circle-ish octagon
|
|
124
|
+
- Onset class adds a stem direction.
|
|
125
|
+
- Vowel places a dot mark in one of 5 anchor positions.
|
|
126
|
+
- Coda adds a terminal notch/tick style.
|
|
127
|
+
|
|
128
|
+
So related meanings and sounds share visible structure.
|
|
129
|
+
|
|
130
|
+
## Direct g4f usage
|
|
131
|
+
|
|
132
|
+
The dictionary generator internally uses this same API pattern:
|
|
133
|
+
|
|
134
|
+
```python
|
|
135
|
+
from g4f.client import Client
|
|
136
|
+
|
|
137
|
+
client = Client()
|
|
138
|
+
response = client.chat.completions.create(
|
|
139
|
+
model="gpt-5-mini",
|
|
140
|
+
messages=[{"role": "user", "content": "..." }],
|
|
141
|
+
web_search=False
|
|
142
|
+
)
|
|
143
|
+
print(response.choices[0].message.content)
|
|
144
|
+
```
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
LICENSE
|
|
2
|
+
README.md
|
|
3
|
+
pyproject.toml
|
|
4
|
+
logiglyph/__init__.py
|
|
5
|
+
logiglyph/__main__.py
|
|
6
|
+
logiglyph/cli.py
|
|
7
|
+
logiglyph.egg-info/PKG-INFO
|
|
8
|
+
logiglyph.egg-info/SOURCES.txt
|
|
9
|
+
logiglyph.egg-info/dependency_links.txt
|
|
10
|
+
logiglyph.egg-info/entry_points.txt
|
|
11
|
+
logiglyph.egg-info/requires.txt
|
|
12
|
+
logiglyph.egg-info/top_level.txt
|
|
13
|
+
src/__init__.py
|
|
14
|
+
src/api.py
|
|
15
|
+
src/dictionary_tool.py
|
|
16
|
+
src/font_builder.py
|
|
17
|
+
src/specimen.py
|
|
18
|
+
src/symbol_logic.py
|
|
19
|
+
src/tool.py
|
|
20
|
+
src/translator.py
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=69", "wheel"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "logiglyph"
|
|
7
|
+
version = "0.1.0"
|
|
8
|
+
description = "Agglutinative language toolkit with logical symbols, translator, and font generator."
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
requires-python = ">=3.10"
|
|
11
|
+
license = "MIT"
|
|
12
|
+
authors = [{ name = "Augusto" }]
|
|
13
|
+
keywords = ["conlang", "translator", "font", "agglutinative-language"]
|
|
14
|
+
classifiers = [
|
|
15
|
+
"Programming Language :: Python :: 3",
|
|
16
|
+
"Programming Language :: Python :: 3 :: Only",
|
|
17
|
+
"Programming Language :: Python :: 3.10",
|
|
18
|
+
"Operating System :: OS Independent",
|
|
19
|
+
"Topic :: Text Processing :: Linguistic",
|
|
20
|
+
]
|
|
21
|
+
dependencies = [
|
|
22
|
+
"g4f>=0.3.2.7",
|
|
23
|
+
"fonttools>=4.56.0",
|
|
24
|
+
]
|
|
25
|
+
|
|
26
|
+
[project.scripts]
|
|
27
|
+
logiglyph = "logiglyph.cli:main"
|
|
28
|
+
|
|
29
|
+
[tool.setuptools]
|
|
30
|
+
packages = ["src", "logiglyph"]
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from dataclasses import dataclass
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
from typing import Dict, List
|
|
6
|
+
|
|
7
|
+
from .dictionary_tool import load_dictionary
|
|
8
|
+
from .symbol_logic import RootSpec
|
|
9
|
+
from .translator import translate_en_to_lang, translate_lang_to_en
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
@dataclass
|
|
13
|
+
class TranslationResult:
|
|
14
|
+
text: str
|
|
15
|
+
codepoints: List[str]
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class LanguageModule:
|
|
19
|
+
"""Reusable API for integrating translation in other Python programs."""
|
|
20
|
+
|
|
21
|
+
def __init__(self, dictionary_path: str | Path = "data/dictionary.json"):
|
|
22
|
+
self.dictionary_path = Path(dictionary_path)
|
|
23
|
+
self.specs: List[RootSpec] = load_dictionary(self.dictionary_path)
|
|
24
|
+
self.char_to_gloss: Dict[str, str] = {s.char: s.gloss for s in self.specs}
|
|
25
|
+
self.gloss_to_char: Dict[str, str] = {s.gloss: s.char for s in self.specs}
|
|
26
|
+
|
|
27
|
+
def translate(self, text: str) -> TranslationResult:
|
|
28
|
+
out = translate_en_to_lang(text, self.specs)
|
|
29
|
+
cps = [f"U+{ord(ch):04X}" for ch in out if not ch.isspace()]
|
|
30
|
+
return TranslationResult(text=out, codepoints=cps)
|
|
31
|
+
|
|
32
|
+
def reverse(self, text: str) -> str:
|
|
33
|
+
return translate_lang_to_en(text, self.specs)
|
|
34
|
+
|
|
35
|
+
def install_note(self) -> str:
|
|
36
|
+
return (
|
|
37
|
+
"Install/use LogiGlyph.ttf in the target app and ensure the rendered field uses that font. "
|
|
38
|
+
"Symbols are in the Unicode Private Use Area (U+E100+)."
|
|
39
|
+
)
|
|
40
|
+
|
|
@@ -0,0 +1,191 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
import re
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
from typing import Dict, List
|
|
7
|
+
|
|
8
|
+
from g4f.client import Client
|
|
9
|
+
|
|
10
|
+
from .symbol_logic import (
|
|
11
|
+
PUA_START,
|
|
12
|
+
RootSpec,
|
|
13
|
+
compose_root,
|
|
14
|
+
enumerate_symbol_space,
|
|
15
|
+
symbol_id,
|
|
16
|
+
)
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def _extract_json(text: str) -> List[Dict[str, str]]:
|
|
20
|
+
text = text.strip()
|
|
21
|
+
try:
|
|
22
|
+
data = json.loads(text)
|
|
23
|
+
if isinstance(data, list):
|
|
24
|
+
return data
|
|
25
|
+
except json.JSONDecodeError:
|
|
26
|
+
pass
|
|
27
|
+
match = re.search(r"\[[\s\S]*\]", text)
|
|
28
|
+
if not match:
|
|
29
|
+
raise ValueError("No JSON list found in model response.")
|
|
30
|
+
data = json.loads(match.group(0))
|
|
31
|
+
if not isinstance(data, list):
|
|
32
|
+
raise ValueError("JSON payload is not a list.")
|
|
33
|
+
return data
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def _fallback_entries(count: int) -> List[Dict[str, str]]:
|
|
37
|
+
glosses = [
|
|
38
|
+
"person",
|
|
39
|
+
"house",
|
|
40
|
+
"water",
|
|
41
|
+
"fire",
|
|
42
|
+
"earth",
|
|
43
|
+
"sky",
|
|
44
|
+
"day",
|
|
45
|
+
"night",
|
|
46
|
+
"eat",
|
|
47
|
+
"drink",
|
|
48
|
+
"go",
|
|
49
|
+
"come",
|
|
50
|
+
"see",
|
|
51
|
+
"know",
|
|
52
|
+
"make",
|
|
53
|
+
"give",
|
|
54
|
+
"big",
|
|
55
|
+
"small",
|
|
56
|
+
"good",
|
|
57
|
+
"bad",
|
|
58
|
+
"inside",
|
|
59
|
+
"outside",
|
|
60
|
+
"near",
|
|
61
|
+
"far",
|
|
62
|
+
"mind",
|
|
63
|
+
"time",
|
|
64
|
+
"name",
|
|
65
|
+
"group",
|
|
66
|
+
"path",
|
|
67
|
+
"tool",
|
|
68
|
+
]
|
|
69
|
+
out: List[Dict[str, str]] = []
|
|
70
|
+
i = 0
|
|
71
|
+
while len(out) < count:
|
|
72
|
+
out.append({"gloss": glosses[i % len(glosses)] + f"_{i}", "semantic_class": ""})
|
|
73
|
+
i += 1
|
|
74
|
+
return out
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
def _semantic_guess(gloss: str) -> str:
|
|
78
|
+
g = gloss.lower()
|
|
79
|
+
action_hints = ("eat", "drink", "go", "come", "see", "know", "make", "give", "do", "build")
|
|
80
|
+
quality_hints = ("big", "small", "good", "bad", "hot", "cold", "new", "old")
|
|
81
|
+
relation_hints = ("inside", "outside", "near", "far", "with", "from", "to", "between")
|
|
82
|
+
abstract_hints = ("mind", "time", "name", "idea", "rule", "number")
|
|
83
|
+
if any(k in g for k in action_hints):
|
|
84
|
+
return "action"
|
|
85
|
+
if any(k in g for k in quality_hints):
|
|
86
|
+
return "quality"
|
|
87
|
+
if any(k in g for k in relation_hints):
|
|
88
|
+
return "relation"
|
|
89
|
+
if any(k in g for k in abstract_hints):
|
|
90
|
+
return "abstract"
|
|
91
|
+
return "entity"
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
def generate_dictionary(count: int = 96, model: str = "gpt-5-mini") -> List[RootSpec]:
|
|
95
|
+
prompt = (
|
|
96
|
+
f"Create {count} base roots for a tiny agglutinative language.\n"
|
|
97
|
+
"Return ONLY JSON list. Each item: "
|
|
98
|
+
'{"gloss":"english concept", "semantic_class":"entity|action|quality|relation|abstract"}.\n'
|
|
99
|
+
"Concepts must be common and concrete when possible."
|
|
100
|
+
)
|
|
101
|
+
|
|
102
|
+
entries: List[Dict[str, str]]
|
|
103
|
+
try:
|
|
104
|
+
client = Client()
|
|
105
|
+
response = client.chat.completions.create(
|
|
106
|
+
model=model,
|
|
107
|
+
messages=[{"role": "user", "content": prompt}],
|
|
108
|
+
web_search=False,
|
|
109
|
+
)
|
|
110
|
+
content = response.choices[0].message.content
|
|
111
|
+
entries = _extract_json(content)
|
|
112
|
+
except Exception:
|
|
113
|
+
entries = _fallback_entries(count)
|
|
114
|
+
|
|
115
|
+
slots = enumerate_symbol_space(max(count, len(entries)))
|
|
116
|
+
out: List[RootSpec] = []
|
|
117
|
+
|
|
118
|
+
for idx, raw in enumerate(entries[:count]):
|
|
119
|
+
gloss = str(raw.get("gloss", f"concept_{idx}")).strip().lower().replace(" ", "_")
|
|
120
|
+
sc = str(raw.get("semantic_class", "")).strip().lower()
|
|
121
|
+
if sc not in {"entity", "action", "quality", "relation", "abstract"}:
|
|
122
|
+
sc = _semantic_guess(gloss)
|
|
123
|
+
|
|
124
|
+
_, onset, vowel, coda = slots[idx]
|
|
125
|
+
root = compose_root(onset, vowel, coda)
|
|
126
|
+
out.append(
|
|
127
|
+
RootSpec(
|
|
128
|
+
root=root,
|
|
129
|
+
gloss=gloss,
|
|
130
|
+
semantic_class=sc,
|
|
131
|
+
onset=onset,
|
|
132
|
+
vowel=vowel,
|
|
133
|
+
coda=coda,
|
|
134
|
+
symbol_id=symbol_id(sc, onset, vowel, coda),
|
|
135
|
+
codepoint=PUA_START + idx,
|
|
136
|
+
)
|
|
137
|
+
)
|
|
138
|
+
return out
|
|
139
|
+
|
|
140
|
+
|
|
141
|
+
def save_dictionary(specs: List[RootSpec], out_path: Path) -> None:
|
|
142
|
+
out_path.parent.mkdir(parents=True, exist_ok=True)
|
|
143
|
+
payload = [
|
|
144
|
+
{
|
|
145
|
+
"root": s.root,
|
|
146
|
+
"gloss": s.gloss,
|
|
147
|
+
"semantic_class": s.semantic_class,
|
|
148
|
+
"onset": s.onset,
|
|
149
|
+
"vowel": s.vowel,
|
|
150
|
+
"coda": s.coda,
|
|
151
|
+
"symbol_id": s.symbol_id,
|
|
152
|
+
"codepoint": f"U+{s.codepoint:04X}",
|
|
153
|
+
"char": s.char,
|
|
154
|
+
}
|
|
155
|
+
for s in specs
|
|
156
|
+
]
|
|
157
|
+
out_path.write_text(json.dumps(payload, indent=2, ensure_ascii=False), encoding="utf-8")
|
|
158
|
+
|
|
159
|
+
|
|
160
|
+
def load_dictionary(path: Path) -> List[RootSpec]:
|
|
161
|
+
data = json.loads(path.read_text(encoding="utf-8"))
|
|
162
|
+
out: List[RootSpec] = []
|
|
163
|
+
for item in data:
|
|
164
|
+
cp_str = str(item["codepoint"]).replace("U+", "")
|
|
165
|
+
cp = int(cp_str, 16)
|
|
166
|
+
out.append(
|
|
167
|
+
RootSpec(
|
|
168
|
+
root=item["root"],
|
|
169
|
+
gloss=item["gloss"],
|
|
170
|
+
semantic_class=item["semantic_class"],
|
|
171
|
+
onset=item["onset"],
|
|
172
|
+
vowel=item["vowel"],
|
|
173
|
+
coda=item["coda"],
|
|
174
|
+
symbol_id=item["symbol_id"],
|
|
175
|
+
codepoint=cp,
|
|
176
|
+
)
|
|
177
|
+
)
|
|
178
|
+
return out
|
|
179
|
+
|
|
180
|
+
|
|
181
|
+
def build_lookup(specs: List[RootSpec]) -> Dict[str, RootSpec]:
|
|
182
|
+
return {s.gloss: s for s in specs}
|
|
183
|
+
|
|
184
|
+
|
|
185
|
+
def build_reverse_lookup(specs: List[RootSpec]) -> Dict[str, RootSpec]:
|
|
186
|
+
out: Dict[str, RootSpec] = {}
|
|
187
|
+
for s in specs:
|
|
188
|
+
out[s.root] = s
|
|
189
|
+
out[s.char] = s
|
|
190
|
+
return out
|
|
191
|
+
|
|
@@ -0,0 +1,194 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
from typing import Dict, Iterable, List, Tuple
|
|
6
|
+
|
|
7
|
+
from fontTools.fontBuilder import FontBuilder
|
|
8
|
+
from fontTools.pens.ttGlyphPen import TTGlyphPen
|
|
9
|
+
|
|
10
|
+
from .dictionary_tool import load_dictionary
|
|
11
|
+
from .symbol_logic import RootSpec, coda_style, onset_vector, vowel_anchor
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
UNITS_PER_EM = 1000
|
|
15
|
+
ASCENT = 820
|
|
16
|
+
DESCENT = -180
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def _draw_octagon(pen: TTGlyphPen, cx: int, cy: int, r: int) -> None:
|
|
20
|
+
pts = [
|
|
21
|
+
(cx, cy + r),
|
|
22
|
+
(cx + int(r * 0.7), cy + int(r * 0.7)),
|
|
23
|
+
(cx + r, cy),
|
|
24
|
+
(cx + int(r * 0.7), cy - int(r * 0.7)),
|
|
25
|
+
(cx, cy - r),
|
|
26
|
+
(cx - int(r * 0.7), cy - int(r * 0.7)),
|
|
27
|
+
(cx - r, cy),
|
|
28
|
+
(cx - int(r * 0.7), cy + int(r * 0.7)),
|
|
29
|
+
]
|
|
30
|
+
pen.moveTo(pts[0])
|
|
31
|
+
for p in pts[1:]:
|
|
32
|
+
pen.lineTo(p)
|
|
33
|
+
pen.closePath()
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def _draw_square(pen: TTGlyphPen, x0: int, y0: int, x1: int, y1: int) -> None:
|
|
37
|
+
pen.moveTo((x0, y0))
|
|
38
|
+
pen.lineTo((x1, y0))
|
|
39
|
+
pen.lineTo((x1, y1))
|
|
40
|
+
pen.lineTo((x0, y1))
|
|
41
|
+
pen.closePath()
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def _draw_diamond(pen: TTGlyphPen, cx: int, cy: int, rx: int, ry: int) -> None:
|
|
45
|
+
pen.moveTo((cx, cy + ry))
|
|
46
|
+
pen.lineTo((cx + rx, cy))
|
|
47
|
+
pen.lineTo((cx, cy - ry))
|
|
48
|
+
pen.lineTo((cx - rx, cy))
|
|
49
|
+
pen.closePath()
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def _draw_line_box(pen: TTGlyphPen, p0: Tuple[int, int], p1: Tuple[int, int], w: int = 30) -> None:
|
|
53
|
+
x0, y0 = p0
|
|
54
|
+
x1, y1 = p1
|
|
55
|
+
dx, dy = x1 - x0, y1 - y0
|
|
56
|
+
if dx == 0 and dy == 0:
|
|
57
|
+
return
|
|
58
|
+
if abs(dx) >= abs(dy):
|
|
59
|
+
# Horizontal-ish segment.
|
|
60
|
+
_draw_square(pen, min(x0, x1), y0 - w, max(x0, x1), y0 + w)
|
|
61
|
+
else:
|
|
62
|
+
# Vertical-ish segment.
|
|
63
|
+
_draw_square(pen, x0 - w, min(y0, y1), x0 + w, max(y0, y1))
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def _semantic_base(pen: TTGlyphPen, semantic_class: str) -> None:
|
|
67
|
+
if semantic_class == "entity":
|
|
68
|
+
_draw_square(pen, 260, 260, 740, 740)
|
|
69
|
+
elif semantic_class == "action":
|
|
70
|
+
_draw_line_box(pen, (280, 300), (720, 700))
|
|
71
|
+
_draw_line_box(pen, (280, 700), (720, 300))
|
|
72
|
+
elif semantic_class == "quality":
|
|
73
|
+
_draw_diamond(pen, 500, 500, 250, 250)
|
|
74
|
+
elif semantic_class == "relation":
|
|
75
|
+
_draw_square(pen, 220, 460, 780, 540)
|
|
76
|
+
_draw_square(pen, 300, 300, 380, 700)
|
|
77
|
+
_draw_square(pen, 620, 300, 700, 700)
|
|
78
|
+
else:
|
|
79
|
+
_draw_octagon(pen, 500, 500, 240)
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
def _draw_onset_stem(pen: TTGlyphPen, onset: str) -> Tuple[int, int]:
|
|
83
|
+
cx, cy = 500, 500
|
|
84
|
+
dx, dy = onset_vector(onset)
|
|
85
|
+
ex, ey = cx + dx, cy + dy
|
|
86
|
+
_draw_line_box(pen, (cx, cy), (ex, ey), w=24)
|
|
87
|
+
return ex, ey
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
def _draw_vowel_mark(pen: TTGlyphPen, vowel: str) -> None:
|
|
91
|
+
x, y = vowel_anchor(vowel)
|
|
92
|
+
_draw_diamond(pen, x, y, 45, 45)
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
def _draw_coda_mark(pen: TTGlyphPen, endpoint: Tuple[int, int], coda: str) -> None:
|
|
96
|
+
style = coda_style(coda)
|
|
97
|
+
x, y = endpoint
|
|
98
|
+
if style == "none":
|
|
99
|
+
return
|
|
100
|
+
if style == "short_tick":
|
|
101
|
+
_draw_square(pen, x - 20, y - 20, x + 80, y + 20)
|
|
102
|
+
elif style == "angle_tick":
|
|
103
|
+
_draw_line_box(pen, (x, y), (x + 90, y - 70), w=18)
|
|
104
|
+
elif style == "double_tick":
|
|
105
|
+
_draw_square(pen, x - 20, y - 50, x + 90, y - 10)
|
|
106
|
+
_draw_square(pen, x - 20, y + 10, x + 90, y + 50)
|
|
107
|
+
elif style == "bar":
|
|
108
|
+
_draw_square(pen, x - 70, y - 16, x + 70, y + 16)
|
|
109
|
+
elif style == "hook":
|
|
110
|
+
_draw_square(pen, x - 20, y - 20, x + 20, y + 90)
|
|
111
|
+
_draw_square(pen, x - 20, y + 50, x + 80, y + 90)
|
|
112
|
+
elif style == "cross_tick":
|
|
113
|
+
_draw_square(pen, x - 20, y - 20, x + 80, y + 20)
|
|
114
|
+
_draw_square(pen, x + 20, y - 80, x + 60, y + 80)
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
def _glyph_for_root(spec: RootSpec):
|
|
118
|
+
pen = TTGlyphPen(None)
|
|
119
|
+
_semantic_base(pen, spec.semantic_class)
|
|
120
|
+
endpoint = _draw_onset_stem(pen, spec.onset)
|
|
121
|
+
_draw_vowel_mark(pen, spec.vowel)
|
|
122
|
+
_draw_coda_mark(pen, endpoint, spec.coda)
|
|
123
|
+
return pen.glyph()
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
def build_font(specs: Iterable[RootSpec], out_path: Path, family_name: str = "LogiGlyph") -> Path:
|
|
127
|
+
specs = list(specs)
|
|
128
|
+
glyph_order = [".notdef", "space"] + [f"uni{spec.codepoint:04X}" for spec in specs]
|
|
129
|
+
cmap: Dict[int, str] = {32: "space"}
|
|
130
|
+
glyf = {}
|
|
131
|
+
hmtx = {}
|
|
132
|
+
|
|
133
|
+
notdef_pen = TTGlyphPen(None)
|
|
134
|
+
_draw_square(notdef_pen, 120, 120, 880, 880)
|
|
135
|
+
_draw_square(notdef_pen, 240, 240, 760, 760)
|
|
136
|
+
glyf[".notdef"] = notdef_pen.glyph()
|
|
137
|
+
hmtx[".notdef"] = (1000, 0)
|
|
138
|
+
|
|
139
|
+
space_pen = TTGlyphPen(None)
|
|
140
|
+
glyf["space"] = space_pen.glyph()
|
|
141
|
+
hmtx["space"] = (500, 0)
|
|
142
|
+
|
|
143
|
+
glyph_map = []
|
|
144
|
+
for spec in specs:
|
|
145
|
+
name = f"uni{spec.codepoint:04X}"
|
|
146
|
+
glyf[name] = _glyph_for_root(spec)
|
|
147
|
+
hmtx[name] = (1000, 0)
|
|
148
|
+
cmap[spec.codepoint] = name
|
|
149
|
+
glyph_map.append(
|
|
150
|
+
{
|
|
151
|
+
"gloss": spec.gloss,
|
|
152
|
+
"root": spec.root,
|
|
153
|
+
"char": spec.char,
|
|
154
|
+
"codepoint": f"U+{spec.codepoint:04X}",
|
|
155
|
+
"symbol_id": spec.symbol_id,
|
|
156
|
+
}
|
|
157
|
+
)
|
|
158
|
+
|
|
159
|
+
fb = FontBuilder(UNITS_PER_EM, isTTF=True)
|
|
160
|
+
fb.setupGlyphOrder(glyph_order)
|
|
161
|
+
fb.setupCharacterMap(cmap)
|
|
162
|
+
fb.setupGlyf(glyf)
|
|
163
|
+
fb.setupHorizontalMetrics(hmtx)
|
|
164
|
+
fb.setupHorizontalHeader(ascent=ASCENT, descent=DESCENT)
|
|
165
|
+
fb.setupNameTable(
|
|
166
|
+
{
|
|
167
|
+
"familyName": family_name,
|
|
168
|
+
"styleName": "Regular",
|
|
169
|
+
"fullName": f"{family_name} Regular",
|
|
170
|
+
"psName": f"{family_name}-Regular",
|
|
171
|
+
"uniqueFontIdentifier": f"{family_name}-Regular-001",
|
|
172
|
+
}
|
|
173
|
+
)
|
|
174
|
+
fb.setupOS2(
|
|
175
|
+
sTypoAscender=ASCENT,
|
|
176
|
+
sTypoDescender=DESCENT,
|
|
177
|
+
usWinAscent=ASCENT,
|
|
178
|
+
usWinDescent=abs(DESCENT),
|
|
179
|
+
)
|
|
180
|
+
fb.setupPost()
|
|
181
|
+
fb.setupMaxp()
|
|
182
|
+
|
|
183
|
+
out_path.parent.mkdir(parents=True, exist_ok=True)
|
|
184
|
+
fb.save(str(out_path))
|
|
185
|
+
|
|
186
|
+
map_path = out_path.with_name("glyph_map.json")
|
|
187
|
+
map_path.write_text(json.dumps(glyph_map, indent=2, ensure_ascii=False), encoding="utf-8")
|
|
188
|
+
return out_path
|
|
189
|
+
|
|
190
|
+
|
|
191
|
+
def build_font_from_dictionary(dict_path: Path, out_path: Path, family_name: str = "LogiGlyph") -> Path:
|
|
192
|
+
specs = load_dictionary(dict_path)
|
|
193
|
+
return build_font(specs, out_path=out_path, family_name=family_name)
|
|
194
|
+
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
|
|
5
|
+
from .dictionary_tool import load_dictionary
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def build_specimen(dict_path: Path, font_path: Path, out_path: Path) -> Path:
|
|
9
|
+
specs = load_dictionary(dict_path)
|
|
10
|
+
rows = []
|
|
11
|
+
for s in specs:
|
|
12
|
+
rows.append(
|
|
13
|
+
f"<tr><td class='glyph'>{s.char}</td><td>{s.gloss}</td><td>{s.root}</td><td>{s.symbol_id}</td><td>U+{s.codepoint:04X}</td></tr>"
|
|
14
|
+
)
|
|
15
|
+
html = f"""<!doctype html>
|
|
16
|
+
<html lang="en">
|
|
17
|
+
<head>
|
|
18
|
+
<meta charset="utf-8"/>
|
|
19
|
+
<meta name="viewport" content="width=device-width,initial-scale=1"/>
|
|
20
|
+
<title>LogiGlyph Specimen</title>
|
|
21
|
+
<style>
|
|
22
|
+
@font-face {{
|
|
23
|
+
font-family: 'LogiGlyph';
|
|
24
|
+
src: url('{font_path.name}') format('truetype');
|
|
25
|
+
}}
|
|
26
|
+
body {{ font-family: 'Segoe UI', sans-serif; background: #f4f7f8; color: #1f2a30; margin: 24px; }}
|
|
27
|
+
h1 {{ margin: 0 0 12px; }}
|
|
28
|
+
table {{ border-collapse: collapse; width: 100%; background: #ffffff; }}
|
|
29
|
+
td, th {{ border: 1px solid #d8e1e4; padding: 8px 10px; text-align: left; }}
|
|
30
|
+
th {{ background: #ecf3f5; }}
|
|
31
|
+
.glyph {{ font-family: 'LogiGlyph', sans-serif; font-size: 34px; width: 68px; text-align: center; }}
|
|
32
|
+
</style>
|
|
33
|
+
</head>
|
|
34
|
+
<body>
|
|
35
|
+
<h1>LogiGlyph Specimen</h1>
|
|
36
|
+
<p>Each symbol is compositional: semantic base + onset + vowel + coda.</p>
|
|
37
|
+
<table>
|
|
38
|
+
<thead>
|
|
39
|
+
<tr><th>Glyph</th><th>Gloss</th><th>Root</th><th>Symbol ID</th><th>Codepoint</th></tr>
|
|
40
|
+
</thead>
|
|
41
|
+
<tbody>
|
|
42
|
+
{''.join(rows)}
|
|
43
|
+
</tbody>
|
|
44
|
+
</table>
|
|
45
|
+
</body>
|
|
46
|
+
</html>
|
|
47
|
+
"""
|
|
48
|
+
out_path.parent.mkdir(parents=True, exist_ok=True)
|
|
49
|
+
out_path.write_text(html, encoding="utf-8")
|
|
50
|
+
return out_path
|
|
51
|
+
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from dataclasses import dataclass
|
|
4
|
+
from typing import Dict, List, Tuple
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
SEMANTIC_CLASSES = ["entity", "action", "quality", "relation", "abstract"]
|
|
8
|
+
ONSETS = ["p", "t", "k", "m", "n", "s", "l", "r", "h", "w", "y"]
|
|
9
|
+
VOWELS = ["a", "e", "i", "o", "u"]
|
|
10
|
+
CODAS = ["", "n", "k", "s", "m", "l", "r"]
|
|
11
|
+
|
|
12
|
+
PUA_START = 0xE100
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
@dataclass(frozen=True)
|
|
16
|
+
class RootSpec:
|
|
17
|
+
root: str
|
|
18
|
+
gloss: str
|
|
19
|
+
semantic_class: str
|
|
20
|
+
onset: str
|
|
21
|
+
vowel: str
|
|
22
|
+
coda: str
|
|
23
|
+
symbol_id: str
|
|
24
|
+
codepoint: int
|
|
25
|
+
|
|
26
|
+
@property
|
|
27
|
+
def char(self) -> str:
|
|
28
|
+
return chr(self.codepoint)
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def compose_root(onset: str, vowel: str, coda: str) -> str:
|
|
32
|
+
return f"{onset}{vowel}{coda}"
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def symbol_id(semantic_class: str, onset: str, vowel: str, coda: str) -> str:
|
|
36
|
+
return f"{semantic_class}:{onset}:{vowel}:{coda or '_'}"
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def enumerate_symbol_space(limit: int) -> List[Tuple[str, str, str, str]]:
|
|
40
|
+
out: List[Tuple[str, str, str, str]] = []
|
|
41
|
+
for sc in SEMANTIC_CLASSES:
|
|
42
|
+
for o in ONSETS:
|
|
43
|
+
for v in VOWELS:
|
|
44
|
+
for c in CODAS:
|
|
45
|
+
out.append((sc, o, v, c))
|
|
46
|
+
if len(out) >= limit:
|
|
47
|
+
return out
|
|
48
|
+
return out
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def vowel_anchor(vowel: str) -> Tuple[int, int]:
|
|
52
|
+
anchors: Dict[str, Tuple[int, int]] = {
|
|
53
|
+
"a": (500, 760),
|
|
54
|
+
"e": (780, 500),
|
|
55
|
+
"i": (500, 240),
|
|
56
|
+
"o": (220, 500),
|
|
57
|
+
"u": (500, 500),
|
|
58
|
+
}
|
|
59
|
+
return anchors[vowel]
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def onset_vector(onset: str) -> Tuple[int, int]:
|
|
63
|
+
vecs: Dict[str, Tuple[int, int]] = {
|
|
64
|
+
"p": (0, 260),
|
|
65
|
+
"t": (260, 0),
|
|
66
|
+
"k": (0, -260),
|
|
67
|
+
"m": (-260, 0),
|
|
68
|
+
"n": (180, 180),
|
|
69
|
+
"s": (180, -180),
|
|
70
|
+
"l": (-180, -180),
|
|
71
|
+
"r": (-180, 180),
|
|
72
|
+
"h": (0, 320),
|
|
73
|
+
"w": (320, 0),
|
|
74
|
+
"y": (-320, 0),
|
|
75
|
+
}
|
|
76
|
+
return vecs[onset]
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
def coda_style(coda: str) -> str:
|
|
80
|
+
styles = {
|
|
81
|
+
"": "none",
|
|
82
|
+
"n": "short_tick",
|
|
83
|
+
"k": "angle_tick",
|
|
84
|
+
"s": "double_tick",
|
|
85
|
+
"m": "bar",
|
|
86
|
+
"l": "hook",
|
|
87
|
+
"r": "cross_tick",
|
|
88
|
+
}
|
|
89
|
+
return styles[coda]
|
|
90
|
+
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import argparse
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
|
|
6
|
+
from .dictionary_tool import generate_dictionary, load_dictionary, save_dictionary
|
|
7
|
+
from .font_builder import build_font_from_dictionary
|
|
8
|
+
from .specimen import build_specimen
|
|
9
|
+
from .translator import translate_en_to_lang, translate_lang_to_en
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def _cmd_generate_dictionary(args: argparse.Namespace) -> int:
|
|
13
|
+
specs = generate_dictionary(count=args.count, model=args.model)
|
|
14
|
+
save_dictionary(specs, Path(args.out))
|
|
15
|
+
print(f"Dictionary saved: {args.out} ({len(specs)} roots)")
|
|
16
|
+
return 0
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def _cmd_build_font(args: argparse.Namespace) -> int:
|
|
20
|
+
out = build_font_from_dictionary(
|
|
21
|
+
dict_path=Path(args.dict),
|
|
22
|
+
out_path=Path(args.out),
|
|
23
|
+
family_name=args.family,
|
|
24
|
+
)
|
|
25
|
+
print(f"Font saved: {out}")
|
|
26
|
+
print(f"Glyph map: {out.with_name('glyph_map.json')}")
|
|
27
|
+
return 0
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def _cmd_translate(args: argparse.Namespace) -> int:
|
|
31
|
+
specs = load_dictionary(Path(args.dict))
|
|
32
|
+
if args.reverse:
|
|
33
|
+
translated = translate_lang_to_en(args.text, specs)
|
|
34
|
+
else:
|
|
35
|
+
translated = translate_en_to_lang(args.text, specs)
|
|
36
|
+
print(translated)
|
|
37
|
+
return 0
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def _cmd_build_specimen(args: argparse.Namespace) -> int:
|
|
41
|
+
out = build_specimen(
|
|
42
|
+
dict_path=Path(args.dict),
|
|
43
|
+
font_path=Path(args.font),
|
|
44
|
+
out_path=Path(args.out),
|
|
45
|
+
)
|
|
46
|
+
print(f"Specimen saved: {out}")
|
|
47
|
+
return 0
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def build_parser() -> argparse.ArgumentParser:
|
|
51
|
+
p = argparse.ArgumentParser(description="Agglutinative language toolkit")
|
|
52
|
+
sp = p.add_subparsers(dest="command", required=True)
|
|
53
|
+
|
|
54
|
+
g = sp.add_parser("generate-dictionary", help="Generate root dictionary using g4f")
|
|
55
|
+
g.add_argument("--count", type=int, default=96, help="Number of roots")
|
|
56
|
+
g.add_argument("--model", default="gpt-5-mini", help="g4f model name")
|
|
57
|
+
g.add_argument("--out", default="data/dictionary.json", help="Output dictionary JSON")
|
|
58
|
+
g.set_defaults(func=_cmd_generate_dictionary)
|
|
59
|
+
|
|
60
|
+
b = sp.add_parser("build-font", help="Build .ttf font from dictionary")
|
|
61
|
+
b.add_argument("--dict", default="data/dictionary.json", help="Input dictionary JSON")
|
|
62
|
+
b.add_argument("--out", default="dist/LogiGlyph.ttf", help="Output TTF path")
|
|
63
|
+
b.add_argument("--family", default="LogiGlyph", help="Font family name")
|
|
64
|
+
b.set_defaults(func=_cmd_build_font)
|
|
65
|
+
|
|
66
|
+
t = sp.add_parser("translate", help="Translate between English and symbol language")
|
|
67
|
+
t.add_argument("--dict", default="data/dictionary.json", help="Input dictionary JSON")
|
|
68
|
+
t.add_argument("--text", required=True, help="Text to translate")
|
|
69
|
+
t.add_argument("--reverse", action="store_true", help="Translate from language to English")
|
|
70
|
+
t.set_defaults(func=_cmd_translate)
|
|
71
|
+
|
|
72
|
+
s = sp.add_parser("build-specimen", help="Build HTML specimen for the symbol font")
|
|
73
|
+
s.add_argument("--dict", default="data/dictionary.json", help="Input dictionary JSON")
|
|
74
|
+
s.add_argument("--font", default="dist/LogiGlyph.ttf", help="Font path used in @font-face")
|
|
75
|
+
s.add_argument("--out", default="dist/specimen.html", help="Output HTML path")
|
|
76
|
+
s.set_defaults(func=_cmd_build_specimen)
|
|
77
|
+
|
|
78
|
+
return p
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
def main() -> int:
|
|
82
|
+
parser = build_parser()
|
|
83
|
+
args = parser.parse_args()
|
|
84
|
+
return args.func(args)
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
if __name__ == "__main__":
|
|
88
|
+
raise SystemExit(main())
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import re
|
|
4
|
+
from typing import List
|
|
5
|
+
|
|
6
|
+
from .dictionary_tool import build_lookup, build_reverse_lookup
|
|
7
|
+
from .symbol_logic import RootSpec
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
TOKEN_RE = re.compile(r"[A-Za-z0-9_]+|[^\sA-Za-z0-9_]")
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def _tokenize(text: str) -> List[str]:
|
|
14
|
+
return TOKEN_RE.findall(text.lower())
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def translate_en_to_lang(text: str, specs: List[RootSpec]) -> str:
|
|
18
|
+
lookup = build_lookup(specs)
|
|
19
|
+
tokens = _tokenize(text)
|
|
20
|
+
out: List[str] = []
|
|
21
|
+
for t in tokens:
|
|
22
|
+
if t in lookup:
|
|
23
|
+
out.append(lookup[t].char)
|
|
24
|
+
continue
|
|
25
|
+
# Basic morphology fallback:
|
|
26
|
+
# plural -> root + relation marker if available
|
|
27
|
+
if t.endswith("s") and t[:-1] in lookup:
|
|
28
|
+
out.append(lookup[t[:-1]].char)
|
|
29
|
+
rel = lookup.get("group_27") or next((s for s in specs if s.semantic_class == "relation"), None)
|
|
30
|
+
if rel:
|
|
31
|
+
out.append(rel.char)
|
|
32
|
+
continue
|
|
33
|
+
out.append(t)
|
|
34
|
+
return " ".join(out)
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def translate_lang_to_en(text: str, specs: List[RootSpec]) -> str:
|
|
38
|
+
reverse = build_reverse_lookup(specs)
|
|
39
|
+
out: List[str] = []
|
|
40
|
+
for ch in text:
|
|
41
|
+
if ch in reverse:
|
|
42
|
+
out.append(reverse[ch].gloss + " ")
|
|
43
|
+
elif ch.isspace():
|
|
44
|
+
out.append(" ")
|
|
45
|
+
else:
|
|
46
|
+
out.append(ch)
|
|
47
|
+
return re.sub(r"\s+", " ", "".join(out)).strip()
|