glitchlings 0.2.5__cp312-cp312-win_amd64.whl → 0.9.3__cp312-cp312-win_amd64.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.
Files changed (85) hide show
  1. glitchlings/__init__.py +36 -17
  2. glitchlings/__main__.py +0 -1
  3. glitchlings/_zoo_rust/__init__.py +12 -0
  4. glitchlings/_zoo_rust.cp312-win_amd64.pyd +0 -0
  5. glitchlings/assets/__init__.py +180 -0
  6. glitchlings/assets/apostrofae_pairs.json +32 -0
  7. glitchlings/assets/ekkokin_homophones.json +2014 -0
  8. glitchlings/assets/hokey_assets.json +193 -0
  9. glitchlings/assets/lexemes/academic.json +1049 -0
  10. glitchlings/assets/lexemes/colors.json +1333 -0
  11. glitchlings/assets/lexemes/corporate.json +716 -0
  12. glitchlings/assets/lexemes/cyberpunk.json +22 -0
  13. glitchlings/assets/lexemes/lovecraftian.json +23 -0
  14. glitchlings/assets/lexemes/synonyms.json +3354 -0
  15. glitchlings/assets/mim1c_homoglyphs.json.gz.b64 +1064 -0
  16. glitchlings/assets/pipeline_assets.json +29 -0
  17. glitchlings/attack/__init__.py +53 -0
  18. glitchlings/attack/compose.py +299 -0
  19. glitchlings/attack/core.py +465 -0
  20. glitchlings/attack/encode.py +114 -0
  21. glitchlings/attack/metrics.py +104 -0
  22. glitchlings/attack/metrics_dispatch.py +70 -0
  23. glitchlings/attack/tokenization.py +157 -0
  24. glitchlings/auggie.py +283 -0
  25. glitchlings/compat/__init__.py +9 -0
  26. glitchlings/compat/loaders.py +355 -0
  27. glitchlings/compat/types.py +41 -0
  28. glitchlings/conf/__init__.py +41 -0
  29. glitchlings/conf/loaders.py +331 -0
  30. glitchlings/conf/schema.py +156 -0
  31. glitchlings/conf/types.py +72 -0
  32. glitchlings/config.toml +2 -0
  33. glitchlings/constants.py +59 -0
  34. glitchlings/dev/__init__.py +3 -0
  35. glitchlings/dev/docs.py +45 -0
  36. glitchlings/dlc/__init__.py +17 -3
  37. glitchlings/dlc/_shared.py +296 -0
  38. glitchlings/dlc/gutenberg.py +400 -0
  39. glitchlings/dlc/huggingface.py +37 -65
  40. glitchlings/dlc/prime.py +55 -114
  41. glitchlings/dlc/pytorch.py +98 -0
  42. glitchlings/dlc/pytorch_lightning.py +173 -0
  43. glitchlings/internal/__init__.py +16 -0
  44. glitchlings/internal/rust.py +159 -0
  45. glitchlings/internal/rust_ffi.py +432 -0
  46. glitchlings/main.py +123 -32
  47. glitchlings/runtime_config.py +24 -0
  48. glitchlings/util/__init__.py +29 -176
  49. glitchlings/util/adapters.py +65 -0
  50. glitchlings/util/keyboards.py +311 -0
  51. glitchlings/util/transcripts.py +108 -0
  52. glitchlings/zoo/__init__.py +47 -24
  53. glitchlings/zoo/assets/__init__.py +29 -0
  54. glitchlings/zoo/core.py +301 -167
  55. glitchlings/zoo/core_execution.py +98 -0
  56. glitchlings/zoo/core_planning.py +451 -0
  57. glitchlings/zoo/corrupt_dispatch.py +295 -0
  58. glitchlings/zoo/ekkokin.py +118 -0
  59. glitchlings/zoo/hokey.py +137 -0
  60. glitchlings/zoo/jargoyle.py +179 -274
  61. glitchlings/zoo/mim1c.py +106 -68
  62. glitchlings/zoo/pedant/__init__.py +107 -0
  63. glitchlings/zoo/pedant/core.py +105 -0
  64. glitchlings/zoo/pedant/forms.py +74 -0
  65. glitchlings/zoo/pedant/stones.py +74 -0
  66. glitchlings/zoo/redactyl.py +44 -175
  67. glitchlings/zoo/rng.py +259 -0
  68. glitchlings/zoo/rushmore.py +359 -116
  69. glitchlings/zoo/scannequin.py +18 -125
  70. glitchlings/zoo/transforms.py +386 -0
  71. glitchlings/zoo/typogre.py +76 -162
  72. glitchlings/zoo/validation.py +477 -0
  73. glitchlings/zoo/zeedub.py +33 -86
  74. glitchlings-0.9.3.dist-info/METADATA +334 -0
  75. glitchlings-0.9.3.dist-info/RECORD +80 -0
  76. {glitchlings-0.2.5.dist-info → glitchlings-0.9.3.dist-info}/entry_points.txt +1 -0
  77. glitchlings/zoo/_ocr_confusions.py +0 -34
  78. glitchlings/zoo/_rate.py +0 -21
  79. glitchlings/zoo/reduple.py +0 -169
  80. glitchlings-0.2.5.dist-info/METADATA +0 -490
  81. glitchlings-0.2.5.dist-info/RECORD +0 -27
  82. /glitchlings/{zoo → assets}/ocr_confusions.tsv +0 -0
  83. {glitchlings-0.2.5.dist-info → glitchlings-0.9.3.dist-info}/WHEEL +0 -0
  84. {glitchlings-0.2.5.dist-info → glitchlings-0.9.3.dist-info}/licenses/LICENSE +0 -0
  85. {glitchlings-0.2.5.dist-info → glitchlings-0.9.3.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,477 @@
1
+ """Boundary validation layer for glitchling parameters.
2
+
3
+ This module centralizes all input validation, type coercion, and defensive checks
4
+ for glitchling parameters. Functions here are called at module boundaries (CLI,
5
+ public API entry points, configuration loaders) to ensure that invalid data is
6
+ rejected early.
7
+
8
+ **Design Philosophy:**
9
+
10
+ All functions in this module are *pure* - they perform validation and coercion
11
+ based solely on their inputs, without side effects. They are intended to be
12
+ called once at the boundary where untrusted input enters the system. Core
13
+ transformation functions that call these validation helpers can then trust
14
+ their inputs without re-validating.
15
+
16
+ See AGENTS.md "Functional Purity Architecture" for full details.
17
+ """
18
+
19
+ from __future__ import annotations
20
+
21
+ import math
22
+ import re
23
+ from collections.abc import Collection, Iterable, Mapping, Sequence
24
+ from dataclasses import dataclass
25
+ from typing import Literal, TypeVar, cast
26
+
27
+ # ---------------------------------------------------------------------------
28
+ # Rate Validation (universal)
29
+ # ---------------------------------------------------------------------------
30
+
31
+
32
+ def clamp_rate(value: float, *, allow_nan: bool = False) -> float:
33
+ """Clamp a rate value to [0.0, infinity), optionally treating NaN as 0.0.
34
+
35
+ Args:
36
+ value: The rate to clamp.
37
+ allow_nan: If False (default), NaN values become 0.0.
38
+
39
+ Returns:
40
+ The clamped rate value.
41
+ """
42
+ if math.isnan(value):
43
+ return 0.0 if not allow_nan else value
44
+ return max(0.0, value)
45
+
46
+
47
+ def clamp_rate_unit(value: float, *, allow_nan: bool = False) -> float:
48
+ """Clamp a rate value to [0.0, 1.0], optionally treating NaN as 0.0.
49
+
50
+ Args:
51
+ value: The rate to clamp.
52
+ allow_nan: If False (default), NaN values become 0.0.
53
+
54
+ Returns:
55
+ The clamped rate value in range [0.0, 1.0].
56
+ """
57
+ if math.isnan(value):
58
+ return 0.0 if not allow_nan else value
59
+ return max(0.0, min(1.0, value))
60
+
61
+
62
+ def resolve_rate(
63
+ value: float | None,
64
+ default: float,
65
+ *,
66
+ clamp: bool = True,
67
+ unit_interval: bool = False,
68
+ ) -> float:
69
+ """Resolve a rate parameter, applying defaults and optional clamping.
70
+
71
+ Args:
72
+ value: The user-provided rate, or None for default.
73
+ default: The default rate to use when value is None.
74
+ clamp: Whether to clamp the result to non-negative.
75
+ unit_interval: If True, clamp to [0.0, 1.0] instead of [0.0, inf).
76
+
77
+ Returns:
78
+ The resolved, optionally clamped rate.
79
+ """
80
+ effective = default if value is None else value
81
+ if not clamp:
82
+ return effective
83
+ return clamp_rate_unit(effective) if unit_interval else clamp_rate(effective)
84
+
85
+
86
+ # ---------------------------------------------------------------------------
87
+ # Mim1c Validation
88
+ # ---------------------------------------------------------------------------
89
+
90
+
91
+ def normalise_mim1c_classes(
92
+ value: object,
93
+ ) -> tuple[str, ...] | Literal["all"] | None:
94
+ """Normalize Mim1c homoglyph class specification.
95
+
96
+ Args:
97
+ value: User input - None, "all", a single class name, or an iterable.
98
+
99
+ Returns:
100
+ Normalized tuple of class names, literal "all", or None.
101
+
102
+ Raises:
103
+ TypeError: If value is not None, string, or iterable.
104
+ """
105
+ if value is None:
106
+ return None
107
+ if isinstance(value, str):
108
+ if value.lower() == "all":
109
+ return "all"
110
+ return (value,)
111
+ if isinstance(value, Iterable):
112
+ return tuple(str(item) for item in value)
113
+ raise TypeError("classes must be an iterable of strings or 'all'")
114
+
115
+
116
+ def normalise_mim1c_banned(value: object) -> tuple[str, ...] | None:
117
+ """Normalize Mim1c banned character specification.
118
+
119
+ Args:
120
+ value: User input - None, a string of characters, or an iterable.
121
+
122
+ Returns:
123
+ Normalized tuple of banned characters, or None.
124
+
125
+ Raises:
126
+ TypeError: If value is not None, string, or iterable.
127
+ """
128
+ if value is None:
129
+ return None
130
+ if isinstance(value, str):
131
+ return tuple(value)
132
+ if isinstance(value, Iterable):
133
+ return tuple(str(item) for item in value)
134
+ raise TypeError("banned_characters must be an iterable of strings")
135
+
136
+
137
+ # ---------------------------------------------------------------------------
138
+ # Ekkokin Validation
139
+ # ---------------------------------------------------------------------------
140
+
141
+
142
+ def normalise_homophone_group(group: Sequence[str]) -> tuple[str, ...]:
143
+ """Return a tuple of lowercase homophones preserving original order.
144
+
145
+ Uses dict.fromkeys to preserve ordering while de-duplicating.
146
+
147
+ Args:
148
+ group: Sequence of homophone words.
149
+
150
+ Returns:
151
+ De-duplicated tuple of lowercase words.
152
+ """
153
+ return tuple(dict.fromkeys(word.lower() for word in group if word))
154
+
155
+
156
+ def build_homophone_lookup(
157
+ groups: Iterable[Sequence[str]],
158
+ ) -> Mapping[str, tuple[str, ...]]:
159
+ """Return a mapping from word -> homophone group.
160
+
161
+ Args:
162
+ groups: Iterable of homophone word groups.
163
+
164
+ Returns:
165
+ Dictionary mapping each word to its normalized group.
166
+ """
167
+ lookup: dict[str, tuple[str, ...]] = {}
168
+ for group in groups:
169
+ normalised = normalise_homophone_group(group)
170
+ if len(normalised) < 2:
171
+ continue
172
+ for word in normalised:
173
+ lookup[word] = normalised
174
+ return lookup
175
+
176
+
177
+ # ---------------------------------------------------------------------------
178
+ # Rushmore Validation
179
+ # ---------------------------------------------------------------------------
180
+
181
+ # Import enum locally to avoid circular dependencies at module level
182
+ # The RushmoreMode enum is defined in rushmore.py but we need its values here
183
+ # for mode validation. We use string-based validation to avoid the import cycle.
184
+
185
+ _RUSHMORE_MODE_ALIASES: dict[str, str] = {
186
+ "delete": "delete",
187
+ "drop": "delete",
188
+ "rushmore": "delete",
189
+ "duplicate": "duplicate",
190
+ "reduplicate": "duplicate",
191
+ "repeat": "duplicate",
192
+ "swap": "swap",
193
+ "adjacent": "swap",
194
+ }
195
+
196
+ _RUSHMORE_EXECUTION_ORDER: tuple[str, ...] = ("delete", "duplicate", "swap")
197
+
198
+
199
+ def normalize_rushmore_mode_item(value: str) -> list[str]:
200
+ """Parse a single Rushmore mode specification into canonical mode names.
201
+
202
+ Args:
203
+ value: A mode name, alias, or compound expression like "delete+duplicate".
204
+
205
+ Returns:
206
+ List of canonical mode names ("delete", "duplicate", "swap").
207
+
208
+ Raises:
209
+ ValueError: If the mode name is not recognized.
210
+ """
211
+ text = str(value).strip().lower()
212
+ if not text:
213
+ return []
214
+
215
+ if text in {"all", "any", "full"}:
216
+ return list(_RUSHMORE_EXECUTION_ORDER)
217
+
218
+ tokens = [token for token in re.split(r"[+,\s]+", text) if token]
219
+ if not tokens:
220
+ return []
221
+
222
+ modes: list[str] = []
223
+ for token in tokens:
224
+ mode = _RUSHMORE_MODE_ALIASES.get(token)
225
+ if mode is None:
226
+ raise ValueError(f"Unsupported Rushmore mode '{value}'")
227
+ modes.append(mode)
228
+ return modes
229
+
230
+
231
+ def normalize_rushmore_modes(
232
+ modes: str | Iterable[str] | None,
233
+ *,
234
+ default: str = "delete",
235
+ ) -> tuple[str, ...]:
236
+ """Normalize Rushmore mode specification to canonical tuple.
237
+
238
+ Args:
239
+ modes: User input - None, single mode string, or iterable of modes.
240
+ default: Default mode when input is None or empty.
241
+
242
+ Returns:
243
+ Tuple of unique canonical mode names in insertion order.
244
+ """
245
+ if modes is None:
246
+ candidates: Sequence[str] = (default,)
247
+ elif isinstance(modes, str):
248
+ candidates = (modes,)
249
+ else:
250
+ collected = tuple(modes)
251
+ candidates = collected if collected else (default,)
252
+
253
+ resolved: list[str] = []
254
+ seen: set[str] = set()
255
+ for candidate in candidates:
256
+ for mode in normalize_rushmore_mode_item(candidate):
257
+ if mode not in seen:
258
+ seen.add(mode)
259
+ resolved.append(mode)
260
+
261
+ if not resolved:
262
+ return (default,)
263
+ return tuple(resolved)
264
+
265
+
266
+ @dataclass(frozen=True)
267
+ class RushmoreRateConfig:
268
+ """Resolved rate configuration for a single Rushmore mode."""
269
+
270
+ mode: str
271
+ rate: float
272
+ is_default: bool = False
273
+
274
+
275
+ def resolve_rushmore_mode_rate(
276
+ *,
277
+ mode: str,
278
+ global_rate: float | None,
279
+ specific_rate: float | None,
280
+ default_rates: Mapping[str, float],
281
+ allow_default: bool,
282
+ ) -> float | None:
283
+ """Resolve the effective rate for a single Rushmore mode.
284
+
285
+ Args:
286
+ mode: The canonical mode name ("delete", "duplicate", "swap").
287
+ global_rate: User-provided global rate, or None.
288
+ specific_rate: User-provided mode-specific rate, or None.
289
+ default_rates: Mapping of mode names to default rates.
290
+ allow_default: Whether to fall back to defaults when no rate provided.
291
+
292
+ Returns:
293
+ The resolved rate, or None if no rate available and defaults disallowed.
294
+ """
295
+ baseline = specific_rate if specific_rate is not None else global_rate
296
+ if baseline is None:
297
+ if not allow_default:
298
+ return None
299
+ baseline = default_rates.get(mode)
300
+ if baseline is None:
301
+ return None
302
+
303
+ value = float(baseline)
304
+ value = max(0.0, value)
305
+ if mode == "swap":
306
+ value = min(1.0, value)
307
+ return value
308
+
309
+
310
+ # ---------------------------------------------------------------------------
311
+ # Keyboard Layout Validation
312
+ # ---------------------------------------------------------------------------
313
+
314
+ T = TypeVar("T")
315
+
316
+
317
+ def validate_keyboard_layout(
318
+ keyboard: str,
319
+ layouts: object,
320
+ *,
321
+ context: str = "keyboard layout",
322
+ ) -> Mapping[str, Sequence[str]]:
323
+ """Validate that a keyboard layout name exists and return its mapping.
324
+
325
+ Args:
326
+ keyboard: The layout name to look up.
327
+ layouts: Object with layout names as attributes (e.g., KEYNEIGHBORS).
328
+ context: Description for error messages.
329
+
330
+ Returns:
331
+ The keyboard neighbor mapping.
332
+
333
+ Raises:
334
+ RuntimeError: If the layout name is not found.
335
+ """
336
+ layout = getattr(layouts, keyboard, None)
337
+ if layout is None:
338
+ raise RuntimeError(f"Unknown {context} '{keyboard}'")
339
+ return cast(Mapping[str, Sequence[str]], layout)
340
+
341
+
342
+ def get_keyboard_layout_or_default(
343
+ keyboard: str,
344
+ layouts: object,
345
+ *,
346
+ default: Mapping[str, Sequence[str]] | None = None,
347
+ ) -> Mapping[str, Sequence[str]] | None:
348
+ """Look up a keyboard layout, returning None or default if not found.
349
+
350
+ Args:
351
+ keyboard: The layout name to look up.
352
+ layouts: Object with layout names as attributes.
353
+ default: Value to return if layout not found.
354
+
355
+ Returns:
356
+ The keyboard neighbor mapping, or default if not found.
357
+ """
358
+ layout = getattr(layouts, keyboard, None)
359
+ if layout is None:
360
+ return default
361
+ return cast(Mapping[str, Sequence[str]], layout)
362
+
363
+
364
+ # ---------------------------------------------------------------------------
365
+ # Zeedub Validation
366
+ # ---------------------------------------------------------------------------
367
+
368
+
369
+ def normalize_zero_width_palette(
370
+ characters: Sequence[str] | None,
371
+ default: tuple[str, ...],
372
+ ) -> tuple[str, ...]:
373
+ """Normalize zero-width character palette, filtering empty entries.
374
+
375
+ Args:
376
+ characters: User-provided character sequence, or None for default.
377
+ default: Default character palette.
378
+
379
+ Returns:
380
+ Tuple of non-empty characters.
381
+ """
382
+ palette: Sequence[str] = tuple(characters) if characters is not None else default
383
+ return tuple(char for char in palette if char)
384
+
385
+
386
+ # ---------------------------------------------------------------------------
387
+ # Redactyl Validation
388
+ # ---------------------------------------------------------------------------
389
+
390
+
391
+ def normalize_replacement_char(
392
+ replacement_char: str | None,
393
+ default: str,
394
+ ) -> str:
395
+ """Normalize redaction replacement character.
396
+
397
+ Args:
398
+ replacement_char: User-provided character, or None for default.
399
+ default: Default replacement character.
400
+
401
+ Returns:
402
+ The replacement character as a string.
403
+ """
404
+ return default if replacement_char is None else str(replacement_char)
405
+
406
+
407
+ # ---------------------------------------------------------------------------
408
+ # Boolean Flag Helpers
409
+ # ---------------------------------------------------------------------------
410
+
411
+
412
+ def resolve_bool_flag(
413
+ specific: bool | None,
414
+ global_default: bool,
415
+ ) -> bool:
416
+ """Resolve a boolean flag with specific/global precedence.
417
+
418
+ Args:
419
+ specific: Specific override value, or None to use global.
420
+ global_default: Global default when specific is None.
421
+
422
+ Returns:
423
+ The resolved boolean flag.
424
+ """
425
+ return bool(specific if specific is not None else global_default)
426
+
427
+
428
+ # ---------------------------------------------------------------------------
429
+ # Collection Helpers
430
+ # ---------------------------------------------------------------------------
431
+
432
+
433
+ def normalize_string_collection(
434
+ value: str | Collection[str] | None,
435
+ ) -> tuple[str, ...] | None:
436
+ """Normalize a string or collection of strings to a tuple.
437
+
438
+ Args:
439
+ value: Single string, collection of strings, or None.
440
+
441
+ Returns:
442
+ Tuple of strings, or None if input is None.
443
+ """
444
+ if value is None:
445
+ return None
446
+ if isinstance(value, str):
447
+ return (value,)
448
+ return tuple(value)
449
+
450
+
451
+ __all__ = [
452
+ # Rate validation
453
+ "clamp_rate",
454
+ "clamp_rate_unit",
455
+ "resolve_rate",
456
+ # Mim1c
457
+ "normalise_mim1c_classes",
458
+ "normalise_mim1c_banned",
459
+ # Ekkokin
460
+ "normalise_homophone_group",
461
+ "build_homophone_lookup",
462
+ # Rushmore
463
+ "normalize_rushmore_mode_item",
464
+ "normalize_rushmore_modes",
465
+ "resolve_rushmore_mode_rate",
466
+ "RushmoreRateConfig",
467
+ # Keyboard
468
+ "validate_keyboard_layout",
469
+ "get_keyboard_layout_or_default",
470
+ # Zeedub
471
+ "normalize_zero_width_palette",
472
+ # Redactyl
473
+ "normalize_replacement_char",
474
+ # Flags and helpers
475
+ "resolve_bool_flag",
476
+ "normalize_string_collection",
477
+ ]
glitchlings/zoo/zeedub.py CHANGED
@@ -1,71 +1,18 @@
1
1
  from __future__ import annotations
2
2
 
3
- import math
4
3
  import random
5
4
  from collections.abc import Sequence
5
+ from typing import cast
6
6
 
7
- from .core import Glitchling, AttackWave, AttackOrder
8
- from ._rate import resolve_rate
9
-
10
- try:
11
- from glitchlings._zoo_rust import inject_zero_widths as _inject_zero_widths_rust
12
- except ImportError: # pragma: no cover - compiled extension not present
13
- _inject_zero_widths_rust = None
14
-
15
- _DEFAULT_ZERO_WIDTH_CHARACTERS: tuple[str, ...] = (
16
- "\u200b", # ZERO WIDTH SPACE
17
- "\u200c", # ZERO WIDTH NON-JOINER
18
- "\u200d", # ZERO WIDTH JOINER
19
- "\ufeff", # ZERO WIDTH NO-BREAK SPACE
20
- "\u2060", # WORD JOINER
7
+ from glitchlings.constants import DEFAULT_ZEEDUB_RATE, ZEEDUB_DEFAULT_ZERO_WIDTHS
8
+ from glitchlings.internal.rust_ffi import (
9
+ inject_zero_widths_rust,
10
+ resolve_seed,
21
11
  )
22
12
 
13
+ from .core import AttackOrder, AttackWave, Glitchling, PipelineOperationPayload
23
14
 
24
- def _python_insert_zero_widths(
25
- text: str,
26
- *,
27
- rate: float,
28
- rng: random.Random,
29
- characters: Sequence[str],
30
- ) -> str:
31
- if not text:
32
- return text
33
-
34
- palette = [char for char in characters if char]
35
- if not palette:
36
- return text
37
-
38
- positions = [
39
- index + 1
40
- for index in range(len(text) - 1)
41
- if not text[index].isspace() and not text[index + 1].isspace()
42
- ]
43
- if not positions:
44
- return text
45
-
46
- total = len(positions)
47
- clamped_rate = max(0.0, rate)
48
- if clamped_rate <= 0.0:
49
- return text
50
-
51
- target = clamped_rate * total
52
- count = math.floor(target)
53
- remainder = target - count
54
- if remainder > 0.0 and rng.random() < remainder:
55
- count += 1
56
- count = min(total, count)
57
-
58
- if count <= 0:
59
- return text
60
-
61
- chosen = rng.sample(positions, count)
62
- chosen.sort()
63
-
64
- chars = list(text)
65
- for position in reversed(chosen):
66
- chars.insert(position, rng.choice(palette))
67
-
68
- return "".join(chars)
15
+ _DEFAULT_ZERO_WIDTH_CHARACTERS: tuple[str, ...] = ZEEDUB_DEFAULT_ZERO_WIDTHS
69
16
 
70
17
 
71
18
  def insert_zero_widths(
@@ -77,19 +24,10 @@ def insert_zero_widths(
77
24
  characters: Sequence[str] | None = None,
78
25
  ) -> str:
79
26
  """Inject zero-width characters between non-space character pairs."""
80
-
81
- effective_rate = resolve_rate(
82
- rate=rate,
83
- legacy_value=None,
84
- default=0.02,
85
- legacy_name="rate",
86
- )
87
-
88
- if rng is None:
89
- rng = random.Random(seed)
27
+ effective_rate = DEFAULT_ZEEDUB_RATE if rate is None else rate
90
28
 
91
29
  palette: Sequence[str] = (
92
- tuple(characters) if characters is not None else _DEFAULT_ZERO_WIDTH_CHARACTERS
30
+ tuple(characters) if characters is not None else ZEEDUB_DEFAULT_ZERO_WIDTHS
93
31
  )
94
32
 
95
33
  cleaned_palette = tuple(char for char in palette if char)
@@ -100,20 +38,15 @@ def insert_zero_widths(
100
38
  if clamped_rate == 0.0:
101
39
  return text
102
40
 
103
- if _inject_zero_widths_rust is not None:
104
- return _inject_zero_widths_rust(text, clamped_rate, list(cleaned_palette), rng)
105
-
106
- return _python_insert_zero_widths(
107
- text,
108
- rate=clamped_rate,
109
- rng=rng,
110
- characters=cleaned_palette,
111
- )
41
+ seed_value = resolve_seed(seed, rng)
42
+ return inject_zero_widths_rust(text, clamped_rate, list(cleaned_palette), seed_value)
112
43
 
113
44
 
114
45
  class Zeedub(Glitchling):
115
46
  """Glitchling that plants zero-width glyphs inside words."""
116
47
 
48
+ flavor = "I'm invoking my right to remain silent."
49
+
117
50
  def __init__(
118
51
  self,
119
52
  *,
@@ -121,12 +54,7 @@ class Zeedub(Glitchling):
121
54
  seed: int | None = None,
122
55
  characters: Sequence[str] | None = None,
123
56
  ) -> None:
124
- effective_rate = resolve_rate(
125
- rate=rate,
126
- legacy_value=None,
127
- default=0.02,
128
- legacy_name="rate",
129
- )
57
+ effective_rate = DEFAULT_ZEEDUB_RATE if rate is None else rate
130
58
  super().__init__(
131
59
  name="Zeedub",
132
60
  corruption_function=insert_zero_widths,
@@ -137,6 +65,25 @@ class Zeedub(Glitchling):
137
65
  characters=tuple(characters) if characters is not None else None,
138
66
  )
139
67
 
68
+ def pipeline_operation(self) -> PipelineOperationPayload:
69
+ rate = float(self.kwargs.get("rate", DEFAULT_ZEEDUB_RATE))
70
+
71
+ raw_characters = self.kwargs.get("characters")
72
+ palette = (
73
+ tuple(ZEEDUB_DEFAULT_ZERO_WIDTHS)
74
+ if raw_characters is None
75
+ else tuple(str(char) for char in raw_characters if char)
76
+ )
77
+
78
+ return cast(
79
+ PipelineOperationPayload,
80
+ {
81
+ "type": "zwj",
82
+ "rate": rate,
83
+ "characters": list(palette),
84
+ },
85
+ )
86
+
140
87
 
141
88
  zeedub = Zeedub()
142
89