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.
@@ -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
+
@@ -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,6 @@
1
+ """Public package API for LogiGlyph."""
2
+
3
+ from src.api import LanguageModule, TranslationResult
4
+
5
+ __all__ = ["LanguageModule", "TranslationResult"]
6
+
@@ -0,0 +1,6 @@
1
+ from src.tool import main
2
+
3
+
4
+ if __name__ == "__main__":
5
+ raise SystemExit(main())
6
+
@@ -0,0 +1,4 @@
1
+ from src.tool import main
2
+
3
+ __all__ = ["main"]
4
+
@@ -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,2 @@
1
+ [console_scripts]
2
+ logiglyph = logiglyph.cli:main
@@ -0,0 +1,2 @@
1
+ g4f>=0.3.2.7
2
+ fonttools>=4.56.0
@@ -0,0 +1,2 @@
1
+ logiglyph
2
+ src
@@ -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,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -0,0 +1,5 @@
1
+ """Agglutinative language toolkit package."""
2
+
3
+ from .api import LanguageModule, TranslationResult
4
+
5
+ __all__ = ["LanguageModule", "TranslationResult"]
@@ -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()