glitchlings 0.2.3__cp310-cp310-manylinux_2_28_x86_64.whl
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.
- glitchlings/__init__.py +42 -0
- glitchlings/__main__.py +9 -0
- glitchlings/_zoo_rust.cpython-310-x86_64-linux-gnu.so +0 -0
- glitchlings/dlc/__init__.py +5 -0
- glitchlings/dlc/huggingface.py +96 -0
- glitchlings/dlc/prime.py +274 -0
- glitchlings/main.py +218 -0
- glitchlings/util/__init__.py +181 -0
- glitchlings/zoo/__init__.py +134 -0
- glitchlings/zoo/_ocr_confusions.py +34 -0
- glitchlings/zoo/_rate.py +21 -0
- glitchlings/zoo/core.py +405 -0
- glitchlings/zoo/jargoyle.py +336 -0
- glitchlings/zoo/mim1c.py +108 -0
- glitchlings/zoo/ocr_confusions.tsv +30 -0
- glitchlings/zoo/redactyl.py +165 -0
- glitchlings/zoo/reduple.py +128 -0
- glitchlings/zoo/rushmore.py +136 -0
- glitchlings/zoo/scannequin.py +171 -0
- glitchlings/zoo/typogre.py +212 -0
- glitchlings-0.2.3.dist-info/METADATA +478 -0
- glitchlings-0.2.3.dist-info/RECORD +26 -0
- glitchlings-0.2.3.dist-info/WHEEL +5 -0
- glitchlings-0.2.3.dist-info/entry_points.txt +2 -0
- glitchlings-0.2.3.dist-info/licenses/LICENSE +201 -0
- glitchlings-0.2.3.dist-info/top_level.txt +1 -0
@@ -0,0 +1,181 @@
|
|
1
|
+
import difflib
|
2
|
+
from collections.abc import Iterable
|
3
|
+
|
4
|
+
SAMPLE_TEXT = "One morning, when Gregor Samsa woke from troubled dreams, he found himself transformed in his bed into a horrible vermin. He lay on his armour-like back, and if he lifted his head a little he could see his brown belly, slightly domed and divided by arches into stiff sections. The bedding was hardly able to cover it and seemed ready to slide off any moment. His many legs, pitifully thin compared with the size of the rest of him, waved about helplessly as he looked."
|
5
|
+
|
6
|
+
|
7
|
+
def string_diffs(a: str, b: str) -> list[list[tuple[str, str, str]]]:
|
8
|
+
"""
|
9
|
+
Compare two strings using SequenceMatcher and return
|
10
|
+
grouped adjacent opcodes (excluding 'equal' tags).
|
11
|
+
|
12
|
+
Each element is a tuple: (tag, a_text, b_text).
|
13
|
+
"""
|
14
|
+
sm = difflib.SequenceMatcher(None, a, b)
|
15
|
+
ops: list[list[tuple[str, str, str]]] = []
|
16
|
+
buffer: list[tuple[str, str, str]] = []
|
17
|
+
|
18
|
+
for tag, i1, i2, j1, j2 in sm.get_opcodes():
|
19
|
+
if tag == "equal":
|
20
|
+
# flush any buffered operations before skipping
|
21
|
+
if buffer:
|
22
|
+
ops.append(buffer)
|
23
|
+
buffer = []
|
24
|
+
continue
|
25
|
+
|
26
|
+
# append operation to buffer
|
27
|
+
buffer.append((tag, a[i1:i2], b[j1:j2]))
|
28
|
+
|
29
|
+
# flush trailing buffer
|
30
|
+
if buffer:
|
31
|
+
ops.append(buffer)
|
32
|
+
|
33
|
+
return ops
|
34
|
+
|
35
|
+
|
36
|
+
KeyNeighborMap = dict[str, list[str]]
|
37
|
+
KeyboardLayouts = dict[str, KeyNeighborMap]
|
38
|
+
|
39
|
+
|
40
|
+
def _build_neighbor_map(rows: Iterable[str]) -> KeyNeighborMap:
|
41
|
+
"""Derive 8-neighbour adjacency lists from keyboard layout rows."""
|
42
|
+
|
43
|
+
grid: dict[tuple[int, int], str] = {}
|
44
|
+
for y, row in enumerate(rows):
|
45
|
+
for x, char in enumerate(row):
|
46
|
+
if char == " ":
|
47
|
+
continue
|
48
|
+
grid[(x, y)] = char.lower()
|
49
|
+
|
50
|
+
neighbors: KeyNeighborMap = {}
|
51
|
+
for (x, y), char in grid.items():
|
52
|
+
seen: list[str] = []
|
53
|
+
for dy in (-1, 0, 1):
|
54
|
+
for dx in (-1, 0, 1):
|
55
|
+
if dx == 0 and dy == 0:
|
56
|
+
continue
|
57
|
+
candidate = grid.get((x + dx, y + dy))
|
58
|
+
if candidate is None:
|
59
|
+
continue
|
60
|
+
seen.append(candidate)
|
61
|
+
# Preserve encounter order but drop duplicates for determinism
|
62
|
+
deduped = list(dict.fromkeys(seen))
|
63
|
+
neighbors[char] = deduped
|
64
|
+
|
65
|
+
return neighbors
|
66
|
+
|
67
|
+
|
68
|
+
_KEYNEIGHBORS: KeyboardLayouts = {
|
69
|
+
"CURATOR_QWERTY": {
|
70
|
+
"a": [*"qwsz"],
|
71
|
+
"b": [*"vghn "],
|
72
|
+
"c": [*"xdfv "],
|
73
|
+
"d": [*"serfcx"],
|
74
|
+
"e": [*"wsdrf34"],
|
75
|
+
"f": [*"drtgvc"],
|
76
|
+
"g": [*"ftyhbv"],
|
77
|
+
"h": [*"gyujnb"],
|
78
|
+
"i": [*"ujko89"],
|
79
|
+
"j": [*"huikmn"],
|
80
|
+
"k": [*"jilom,"],
|
81
|
+
"l": [*"kop;.,"],
|
82
|
+
"m": [*"njk, "],
|
83
|
+
"n": [*"bhjm "],
|
84
|
+
"o": [*"iklp90"],
|
85
|
+
"p": [*"o0-[;l"],
|
86
|
+
"q": [*"was 12"],
|
87
|
+
"r": [*"edft45"],
|
88
|
+
"s": [*"awedxz"],
|
89
|
+
"t": [*"r56ygf"],
|
90
|
+
"u": [*"y78ijh"],
|
91
|
+
"v": [*"cfgb "],
|
92
|
+
"w": [*"q23esa"],
|
93
|
+
"x": [*"zsdc "],
|
94
|
+
"y": [*"t67uhg"],
|
95
|
+
"z": [*"asx"],
|
96
|
+
}
|
97
|
+
}
|
98
|
+
|
99
|
+
|
100
|
+
def _register_layout(name: str, rows: Iterable[str]) -> None:
|
101
|
+
_KEYNEIGHBORS[name] = _build_neighbor_map(rows)
|
102
|
+
|
103
|
+
|
104
|
+
_register_layout(
|
105
|
+
"DVORAK",
|
106
|
+
(
|
107
|
+
"`1234567890[]\\",
|
108
|
+
" ',.pyfgcrl/=\\",
|
109
|
+
" aoeuidhtns-",
|
110
|
+
" ;qjkxbmwvz",
|
111
|
+
),
|
112
|
+
)
|
113
|
+
|
114
|
+
_register_layout(
|
115
|
+
"COLEMAK",
|
116
|
+
(
|
117
|
+
"`1234567890-=",
|
118
|
+
" qwfpgjluy;[]\\",
|
119
|
+
" arstdhneio'",
|
120
|
+
" zxcvbkm,./",
|
121
|
+
),
|
122
|
+
)
|
123
|
+
|
124
|
+
_register_layout(
|
125
|
+
"QWERTY",
|
126
|
+
(
|
127
|
+
"`1234567890-=",
|
128
|
+
" qwertyuiop[]\\",
|
129
|
+
" asdfghjkl;'",
|
130
|
+
" zxcvbnm,./",
|
131
|
+
),
|
132
|
+
)
|
133
|
+
|
134
|
+
_register_layout(
|
135
|
+
"AZERTY",
|
136
|
+
(
|
137
|
+
"²&é\"'(-è_çà)=",
|
138
|
+
" azertyuiop^$",
|
139
|
+
" qsdfghjklmù*",
|
140
|
+
" <wxcvbn,;:!",
|
141
|
+
),
|
142
|
+
)
|
143
|
+
|
144
|
+
_register_layout(
|
145
|
+
"QWERTZ",
|
146
|
+
(
|
147
|
+
"^1234567890ß´",
|
148
|
+
" qwertzuiopü+",
|
149
|
+
" asdfghjklöä#",
|
150
|
+
" yxcvbnm,.-",
|
151
|
+
),
|
152
|
+
)
|
153
|
+
|
154
|
+
_register_layout(
|
155
|
+
"SPANISH_QWERTY",
|
156
|
+
(
|
157
|
+
"º1234567890'¡",
|
158
|
+
" qwertyuiop´+",
|
159
|
+
" asdfghjklñ´",
|
160
|
+
" <zxcvbnm,.-",
|
161
|
+
),
|
162
|
+
)
|
163
|
+
|
164
|
+
_register_layout(
|
165
|
+
"SWEDISH_QWERTY",
|
166
|
+
(
|
167
|
+
"§1234567890+´",
|
168
|
+
" qwertyuiopå¨",
|
169
|
+
" asdfghjklöä'",
|
170
|
+
" <zxcvbnm,.-",
|
171
|
+
),
|
172
|
+
)
|
173
|
+
|
174
|
+
|
175
|
+
class KeyNeighbors:
|
176
|
+
def __init__(self) -> None:
|
177
|
+
for layout_name, layout in _KEYNEIGHBORS.items():
|
178
|
+
setattr(self, layout_name, layout)
|
179
|
+
|
180
|
+
|
181
|
+
KEYNEIGHBORS: KeyNeighbors = KeyNeighbors()
|
@@ -0,0 +1,134 @@
|
|
1
|
+
from __future__ import annotations
|
2
|
+
|
3
|
+
import ast
|
4
|
+
from typing import Any
|
5
|
+
|
6
|
+
from .typogre import Typogre, typogre
|
7
|
+
from .mim1c import Mim1c, mim1c
|
8
|
+
from .jargoyle import Jargoyle, jargoyle, dependencies_available as _jargoyle_available
|
9
|
+
from .reduple import Reduple, reduple
|
10
|
+
from .rushmore import Rushmore, rushmore
|
11
|
+
from .redactyl import Redactyl, redactyl
|
12
|
+
from .scannequin import Scannequin, scannequin
|
13
|
+
from .core import Glitchling, Gaggle
|
14
|
+
|
15
|
+
__all__ = [
|
16
|
+
"Typogre",
|
17
|
+
"typogre",
|
18
|
+
"Mim1c",
|
19
|
+
"mim1c",
|
20
|
+
"Jargoyle",
|
21
|
+
"jargoyle",
|
22
|
+
"Reduple",
|
23
|
+
"reduple",
|
24
|
+
"Rushmore",
|
25
|
+
"rushmore",
|
26
|
+
"Redactyl",
|
27
|
+
"redactyl",
|
28
|
+
"Scannequin",
|
29
|
+
"scannequin",
|
30
|
+
"Glitchling",
|
31
|
+
"Gaggle",
|
32
|
+
"summon",
|
33
|
+
"BUILTIN_GLITCHLINGS",
|
34
|
+
"DEFAULT_GLITCHLING_NAMES",
|
35
|
+
"parse_glitchling_spec",
|
36
|
+
]
|
37
|
+
|
38
|
+
_HAS_JARGOYLE = _jargoyle_available()
|
39
|
+
|
40
|
+
_BUILTIN_GLITCHLING_LIST: list[Glitchling] = [typogre, mim1c]
|
41
|
+
if _HAS_JARGOYLE:
|
42
|
+
_BUILTIN_GLITCHLING_LIST.append(jargoyle)
|
43
|
+
_BUILTIN_GLITCHLING_LIST.extend([reduple, rushmore, redactyl, scannequin])
|
44
|
+
|
45
|
+
BUILTIN_GLITCHLINGS: dict[str, Glitchling] = {
|
46
|
+
glitchling.name.lower(): glitchling for glitchling in _BUILTIN_GLITCHLING_LIST
|
47
|
+
}
|
48
|
+
|
49
|
+
_BUILTIN_GLITCHLING_TYPES: dict[str, type[Glitchling]] = {
|
50
|
+
typogre.name.lower(): Typogre,
|
51
|
+
mim1c.name.lower(): Mim1c,
|
52
|
+
reduple.name.lower(): Reduple,
|
53
|
+
rushmore.name.lower(): Rushmore,
|
54
|
+
redactyl.name.lower(): Redactyl,
|
55
|
+
scannequin.name.lower(): Scannequin,
|
56
|
+
}
|
57
|
+
if _HAS_JARGOYLE:
|
58
|
+
_BUILTIN_GLITCHLING_TYPES[jargoyle.name.lower()] = Jargoyle
|
59
|
+
|
60
|
+
DEFAULT_GLITCHLING_NAMES: list[str] = list(BUILTIN_GLITCHLINGS.keys())
|
61
|
+
|
62
|
+
|
63
|
+
def parse_glitchling_spec(specification: str) -> Glitchling:
|
64
|
+
"""Return a glitchling instance configured according to ``specification``."""
|
65
|
+
|
66
|
+
text = specification.strip()
|
67
|
+
if not text:
|
68
|
+
raise ValueError("Glitchling specification cannot be empty.")
|
69
|
+
|
70
|
+
if "(" not in text:
|
71
|
+
glitchling = BUILTIN_GLITCHLINGS.get(text.lower())
|
72
|
+
if glitchling is None:
|
73
|
+
raise ValueError(f"Glitchling '{text}' not found.")
|
74
|
+
return glitchling
|
75
|
+
|
76
|
+
if not text.endswith(")"):
|
77
|
+
raise ValueError(f"Invalid parameter syntax for glitchling '{text}'.")
|
78
|
+
|
79
|
+
name_part, arg_source = text[:-1].split("(", 1)
|
80
|
+
name = name_part.strip()
|
81
|
+
if not name:
|
82
|
+
raise ValueError(f"Invalid glitchling specification '{text}'.")
|
83
|
+
|
84
|
+
lower_name = name.lower()
|
85
|
+
glitchling_type = _BUILTIN_GLITCHLING_TYPES.get(lower_name)
|
86
|
+
if glitchling_type is None:
|
87
|
+
raise ValueError(f"Glitchling '{name}' not found.")
|
88
|
+
|
89
|
+
try:
|
90
|
+
call_expr = ast.parse(f"_({arg_source})", mode="eval").body
|
91
|
+
except SyntaxError as exc:
|
92
|
+
raise ValueError(
|
93
|
+
f"Invalid parameter syntax for glitchling '{name}': {exc.msg}"
|
94
|
+
) from exc
|
95
|
+
|
96
|
+
if not isinstance(call_expr, ast.Call) or call_expr.args:
|
97
|
+
raise ValueError(
|
98
|
+
f"Glitchling '{name}' parameters must be provided as keyword arguments."
|
99
|
+
)
|
100
|
+
|
101
|
+
kwargs: dict[str, Any] = {}
|
102
|
+
for keyword in call_expr.keywords:
|
103
|
+
if keyword.arg is None:
|
104
|
+
raise ValueError(
|
105
|
+
f"Glitchling '{name}' does not support unpacking arbitrary keyword arguments."
|
106
|
+
)
|
107
|
+
try:
|
108
|
+
kwargs[keyword.arg] = ast.literal_eval(keyword.value)
|
109
|
+
except (ValueError, SyntaxError) as exc:
|
110
|
+
raise ValueError(
|
111
|
+
f"Failed to parse value for parameter '{keyword.arg}' on glitchling '{name}': {exc}"
|
112
|
+
) from exc
|
113
|
+
|
114
|
+
try:
|
115
|
+
return glitchling_type(**kwargs)
|
116
|
+
except TypeError as exc:
|
117
|
+
raise ValueError(f"Failed to instantiate glitchling '{name}': {exc}") from exc
|
118
|
+
|
119
|
+
|
120
|
+
def summon(glitchlings: list[str | Glitchling], seed: int = 151) -> Gaggle:
|
121
|
+
"""Summon glitchlings by name (using defaults) or instance (to change parameters)."""
|
122
|
+
|
123
|
+
summoned: list[Glitchling] = []
|
124
|
+
for entry in glitchlings:
|
125
|
+
if isinstance(entry, Glitchling):
|
126
|
+
summoned.append(entry)
|
127
|
+
continue
|
128
|
+
|
129
|
+
try:
|
130
|
+
summoned.append(parse_glitchling_spec(entry))
|
131
|
+
except ValueError as exc:
|
132
|
+
raise ValueError(str(exc)) from exc
|
133
|
+
|
134
|
+
return Gaggle(summoned, seed=seed)
|
@@ -0,0 +1,34 @@
|
|
1
|
+
from __future__ import annotations
|
2
|
+
|
3
|
+
from importlib import resources
|
4
|
+
|
5
|
+
_CONFUSION_TABLE: list[tuple[str, list[str]]] | None = None
|
6
|
+
|
7
|
+
|
8
|
+
def load_confusion_table() -> list[tuple[str, list[str]]]:
|
9
|
+
"""Load the OCR confusion table shared by Python and Rust implementations."""
|
10
|
+
global _CONFUSION_TABLE
|
11
|
+
if _CONFUSION_TABLE is not None:
|
12
|
+
return _CONFUSION_TABLE
|
13
|
+
|
14
|
+
data = resources.files(__package__) / "ocr_confusions.tsv"
|
15
|
+
text = data.read_text(encoding="utf-8")
|
16
|
+
indexed_entries: list[tuple[int, tuple[str, list[str]]]] = []
|
17
|
+
for line_number, line in enumerate(text.splitlines()):
|
18
|
+
stripped = line.strip()
|
19
|
+
if not stripped or stripped.startswith("#"):
|
20
|
+
continue
|
21
|
+
parts = stripped.split()
|
22
|
+
if len(parts) < 2:
|
23
|
+
continue
|
24
|
+
source, *replacements = parts
|
25
|
+
indexed_entries.append((line_number, (source, replacements)))
|
26
|
+
|
27
|
+
# Sort longer patterns first to avoid overlapping matches, mirroring the
|
28
|
+
# behaviour of the Rust `confusion_table` helper.
|
29
|
+
indexed_entries.sort(
|
30
|
+
key=lambda item: (-len(item[1][0]), item[0])
|
31
|
+
)
|
32
|
+
entries = [entry for _, entry in indexed_entries]
|
33
|
+
_CONFUSION_TABLE = entries
|
34
|
+
return entries
|
glitchlings/zoo/_rate.py
ADDED
@@ -0,0 +1,21 @@
|
|
1
|
+
from __future__ import annotations
|
2
|
+
|
3
|
+
|
4
|
+
def resolve_rate(
|
5
|
+
*,
|
6
|
+
rate: float | None,
|
7
|
+
legacy_value: float | None,
|
8
|
+
default: float,
|
9
|
+
legacy_name: str,
|
10
|
+
) -> float:
|
11
|
+
"""Return the effective rate while enforcing mutual exclusivity."""
|
12
|
+
|
13
|
+
if rate is not None and legacy_value is not None:
|
14
|
+
raise ValueError(
|
15
|
+
f"Specify either 'rate' or '{legacy_name}', not both."
|
16
|
+
)
|
17
|
+
if rate is not None:
|
18
|
+
return rate
|
19
|
+
if legacy_value is not None:
|
20
|
+
return legacy_value
|
21
|
+
return default
|