File size: 22,774 Bytes
dfe210b
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
#!/usr/bin/env python3
# Copyright 2026 Everpaper. Licensed under the Apache License, Version 2.0 (see ../LICENSE).
"""Generate the tokenizer conformance fixture `tokenizer-cases.json`: >= 3,000 deterministic, FICTIONAL texts that
stress the Gemma tokenizer (CRLF, NFD accents, emoji ZWJ / skin tones / flags / keycaps, Arabic with diacritics, Thai,
Hindi, CJK, whitespace runs > 31, tabs, control characters, special-token look-alikes, very long inputs), with the ids
produced by the Hugging Face `tokenizers` library for `<bos> + encode(prefix + text) + <eos>` (no truncation).

    python3 make_tokenizer_cases.py --tokenizer ../model/tokenizer/tokenizer.json --out tokenizer-cases.json

The people, companies, events and numbers below are invented. Most place names are invented too; a few real
place names and words appear in the accent and script stress strings (e.g. Łódź, Đà Nẵng, Ångström).
"""
from __future__ import annotations

import argparse
import json
import random
import unicodedata
from pathlib import Path

PREFIX = {"q": "task: search result | query: ", "d": "title: none | text: ", "raw": ""}

FIRST = ["Orla", "Teodor", "Mirela", "Kasimir", "Ysolde", "Bram", "Fenna", "Ruaridh", "Ottilie", "Severin", "Jolan",
         "Liesl", "Corvin", "Tamsin", "Ilse", "Quillon", "Saskia", "Elowen", "Thaddeus", "Perpetua"]
LAST = ["Vantrell", "Dunholm", "Brecklow", "Szarvas", "Penhallow", "Lindqvar", "Castelmoro", "Ravensby", "Quarterman",
        "Halloran-Bey", "Møllergaard", "Åkerlund", "Nowakówna", "Çelikkaya", "Đặng", "O'Rourke-Vale"]
ORGS = ["Lumenvale", "Brightwater Mills", "Okkam Freight", "Velmora Labs", "Quillfeather", "Saltmarsh & Co",
        "Norrow Analytics", "Cobaltine", "Haverly Bikes", "Studio Morello", "Tavola Verde", "Pinecrest Dental"]
TOWNS = ["Brindlemoor", "Castel Ferrino", "Ostwick", "Valdorna", "Pellham Cross", "Marrowgate", "San Lucetto",
         "Hadleigh Fen", "Rocca Bruna", "Thistleby", "Søndervik", "Łęczyca Nowa", "Çamlıdere", "Žďár Horní"]
THINGS = ["the quarterly invoice", "the boiler service", "the onboarding checklist", "the API rate limits",
          "the warehouse lease", "the beta feedback", "the dentist appointment", "the hiring plan", "the tax return",
          "the server migration", "the book club", "the roof estimate", "the ferry timetable"]
COSE = ["la fattura trimestrale", "la revisione della caldaia", "il contratto d'affitto", "il preventivo del tetto",
        "la riunione di condominio", "il volo per Valdorna", "la visita dal dentista", "il piano ferie",
        "il trasloco di sabato", "il corso di ceramica"]
DAYS = ["Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday", "Sunday"]
MONTHS = ["January", "March", "May", "July", "September", "October", "November", "December"]

EN_TPL = [
    "{p} from {o} moved {t} to {d} because the team in {town} is short-staffed this month.",
    "The budget for {t} is {n},{m:03d} and {p} wants a second quote before {d}.",
    "Remember to call {p} about {t}; the office in {town} closes at {h}:30.",
    "{o} signed the renewal: {n} seats at {m} per seat, billed every {mo}.",
    "On {d} the deploy failed twice because a config flag was read as seconds instead of milliseconds.",
    "Minutes: {p} presented the roadmap, {p2} raised concerns about {t}, and the decision was postponed to {mo}.",
    "Our p99 latency rose to {m} ms after the {d} release; rollback is ready if it passes {h}0 ms.",
    "Grocery list: oat milk, two lemons, basmati rice, coriander, and dog treats for Biscuit.",
    "Flight {o2}{n} to {town} departs at {h}:{m2:02d}; seat {m2}C, one checked bag.",
    "Draft reply to {p}: thanks for the notes on {t}, we will send the revised version by {d}.",
    "The landlord in {town} agreed to fix the window before {mo}; the rent stays at {n},{m:03d} a year.",
]
IT_TPL = [
    "{p} di {o} ha spostato {c} a {d} perché l'ufficio di {town} è sotto organico.",
    "Il preventivo per {c} è di {n}.{m:03d} euro e {p} vuole un secondo parere entro {d}.",
    "Ricordati di chiamare {p} per {c}: lo studio di {town} chiude alle {h}:30.",
    "Verbale: {p} ha presentato il piano, {p2} ha sollevato dubbi su {c} e la decisione slitta a {mo}.",
    "La caldaia perde ancora; il tecnico dice che la valvola costa {m} euro e serve una settimana.",
    "Lista della spesa: latte d'avena, due limoni, riso basmati, coriandolo e croccantini per il cane.",
    "Il volo per {town} parte alle {h}:{m2:02d}; posto {m2}C, un bagaglio in stiva. Perché così presto?",
]
OTHER = {
    "de": ["Die Heizung in {town} fällt seit {d} ständig aus; {p} ruft morgen den Installateur an.",
           "Größere Änderungen am Vertrag mit {o} müssen bis {mo} unterschrieben werden."],
    "fr": ["{p} a déplacé la réunion à {h} h parce que le train pour {town} était en retard.",
           "Le devis de {o} s'élève à {n} {m:03d} € ; l'échéance est fixée à {mo}."],
    "es": ["¿Quién se encarga de {t} en {o}? {p} dijo que el plazo termina el {d}.",
           "El alquiler en {town} sube a {n}.{m:03d} al año; ¡hay que renegociar antes de {mo}!"],
    "pt": ["{p} avisou que a entrega de {o} chega a {town} na {d}, por volta das {h}h.",
           "O orçamento para {t} ficou em {n}.{m:03d}; a decisão foi adiada para {mo}."],
    "nl": ["{p} belt {d} met {o} over {t}; het kantoor in {town} sluit om {h}:30.",
           "De huur in {town} blijft {n}.{m:03d} per jaar als het raam vóór {mo} gemaakt is."],
    "pl": ["{p} przeniósł spotkanie na {d}, bo zespół w {town} ma za mało ludzi.",
           "Faktura od {o} opiewa na {n} {m:03d} zł; termin płatności mija w {mo}."],
    "tr": ["{p}, {o} ile yapılan sözleşmeyi {mo} ayına kadar imzalamalı; ofis {h}:30'da kapanıyor.",
           "{town}'daki kira yılda {n}.{m:03d} olarak kalıyor; pencere {mo} öncesi tamir edilecek."],
    "vi": ["{p} đã dời cuộc họp sang {d} vì đội ở {town} đang thiếu người.",
           "Hóa đơn của {o} là {n}.{m:03d}; hạn thanh toán là tháng {m2}."],
    "ru": ["{p} перенёс встречу на {d}, потому что в {town} не хватает людей.",
           "Счёт от {o} на {n} {m:03d}; оплатить нужно до {mo}."],
    "el": ["Ο {p} μετέφερε τη συνάντηση την {d} επειδή η ομάδα στο {town} είναι λίγη.",
           "Το τιμολόγιο της {o} είναι {n}.{m:03d}· πληρωμή μέχρι {mo}."],
    "he": ["{p} הזיז את הפגישה ל-{d} כי הצוות ב-{town} קטן מדי.",
           "החשבונית מ-{o} היא {n},{m:03d}; לשלם עד {mo}."],
    "ar": ["نَقَلَ {p} الاجْتِماعَ إلى يَوْمِ {d} لِأَنَّ الفَريقَ في {town} قَليلُ العَدَدِ هذا الشَّهْرَ.",
           "الفاتُورَةُ مِنْ {o} بِقيمَةِ {n}٬{m:03d}؛ يَجِبُ الدَّفْعُ قَبْلَ {mo}.",
           "تَذَكَّرْ أَنْ تَتَّصِلَ بِـ{p} بِخُصُوصِ {t}؛ المَكْتَبُ يُغْلِقُ السّاعَةَ {h}:30."],
    "fa": ["{p} جلسه را به {d} موکول کرد چون تیم {town} این ماه کم‌نیرو است.",
           "صورت‌حساب {o} به مبلغ {n}٬{m:03d} است؛ پرداخت تا {mo}."],
    "th": ["{p} เลื่อนการประชุมไปวัน{d} เพราะทีมที่{town}มีคนไม่พอในเดือนนี้",
           "ใบแจ้งหนี้จาก {o} เป็นเงิน {n},{m:03d} บาท ต้องชำระก่อน{mo}",
           "อย่าลืมโทรหา{p}เรื่อง{t} สำนักงานปิดเวลา {h}:30 น."],
    "hi": ["{p} ने बैठक {d} को टाल दी क्योंकि {town} की टीम में इस महीने लोग कम हैं।",
           "{o} का चालान {n},{m:03d} रुपये का है; भुगतान {mo} से पहले करना है।",
           "{p} को {t} के बारे में फ़ोन करना याद रखें; दफ़्तर {h}:30 बजे बंद होता है।"],
    "bn": ["{p} সভাটি {d}-এ পিছিয়ে দিয়েছেন কারণ {town}-এর দলে এই মাসে লোক কম।",
           "{o}-এর চালান {n},{m:03d} টাকার; {mo}-এর আগে পরিশোধ করতে হবে।"],
    "ta": ["{p} கூட்டத்தை {d} அன்று ஒத்திவைத்தார்; {town} குழுவில் இந்த மாதம் ஆட்கள் குறைவு.",
           "{o} விலைப்பட்டியல் {n},{m:03d}; {mo}க்கு முன் செலுத்த வேண்டும்."],
    "zh": ["{p}把会议改到了{d},因为{town}的团队这个月人手不够。",
           "{o}的发票金额是{n},{m:03d}元,必须在{mo}之前付款。",
           "记得给{p}打电话谈{t};办公室{h}点半关门。"],
    "zh_tw": ["{p}把會議改到了{d},因為{town}的團隊這個月人手不夠。",
              "{o}的發票金額是{n},{m:03d}元,必須在{mo}之前付款。"],
    "ja": ["{p}さんは{town}のチームが今月人手不足なので、会議を{d}に延期しました。",
           "{o}からの請求書は{n},{m:03d}円で、{mo}までに支払う必要があります。",
           "{t}について{p}さんに電話するのを忘れないで。オフィスは{h}時半に閉まります。"],
    "ko": ["{p}은(는) {town} 팀이 이번 달 인력이 부족해서 회의를 {d}로 미뤘습니다.",
           "{o}의 청구서는 {n},{m:03d}원이며 {mo} 전에 결제해야 합니다.",
           "{t} 건으로 {p}에게 전화하는 것 잊지 마세요. 사무실은 {h}시 30분에 닫습니다."],
    "ka": ["{p}-მა შეხვედრა {d}-ზე გადადო, რადგან {town}-ის გუნდი ამ თვეში მცირეა."],
    "hy": ["{p}-ը հանդիպումը տեղափոխեց {d}, քանի որ {town}-ի թիմը այս ամիս փոքր է։"],
    "am": ["{p} ስብሰባውን ወደ {d} አዛወረው፣ ምክንያቱም በ{town} ያለው ቡድን በዚህ ወር ትንሽ ነው።"],
    "uk": ["{p} переніс зустріч на {d}, бо команді в {town} бракує людей."],
    "cs": ["{p} přesunul schůzku na {d}, protože tým v {town} má tento měsíc málo lidí."],
    "fi": ["{p} siirsi kokouksen {d}, koska {town}n tiimi on tässä kuussa alimiehitetty."],
    "hu": ["{p} áthelyezte a megbeszélést {d}-ra, mert a {town}-i csapat ebben a hónapban kevés."],
    "sw": ["{p} alihamisha mkutano hadi {d} kwa sababu timu ya {town} ina watu wachache mwezi huu."],
    "id": ["{p} memindahkan rapat ke {d} karena tim di {town} kekurangan orang bulan ini."],
}
QUERIES = ["When is {t} due?", "Who handles {t} at {o}?", "How much did {o} quote for {t}?",
           "What did {p} decide about the {town} office?", "Which flight goes to {town} on {d}?",
           "Why was the deploy rolled back?", "What is the rent in {town}?", "Quando scade {c}?",
           "Chi si occupa di {c} a {town}?", "¿Cuánto cuesta {t}?", "Wann endet die Testphase von {o}?",
           "{town} 사무실 임대료는 얼마인가요?", "{o}の請求書はいつまで?", "{p}是谁?", "ما هو إيجار {town}؟",
           "{town}में किराया कितना है?", "ค่าเช่าใน{town}เท่าไหร่", "p99 latency", "SOC 2", "INC-{m}", "wifi password"]
CODE = [
    "def backoff(attempt: int, base: float = 0.5, cap: float = {m}.0) -> float:\n    return min(cap, base * 2 ** attempt)\n",
    "func retryDelay(_ attempt: Int) -> Duration {{\n\t.milliseconds(min({m}_000, 500 << attempt))\n}}\n",
    "SELECT customer_id, SUM(amount) AS total\nFROM invoices\nWHERE issued_at >= '2026-{m2:02d}-01'\nGROUP BY customer_id\nHAVING SUM(amount) > {n}{m};\n",
    "const queue = new Map();\nexport function enqueue(id, payload) {{\n  if (queue.size > {m}) throw new Error('queue full');\n  queue.set(id, {{ payload, tries: 0 }});\n}}\n",
    "#!/bin/sh\nset -eu\nfor f in logs/*.gz; do\n  zcat \"$f\" | grep -c 'status={h}0' || true\ndone\n",
    "{{\n  \"service\": \"{o2}-webhooks\",\n  \"region\": \"ams-1\",\n  \"retries\": {h},\n  \"timeout_ms\": {m}0\n}}\n",
    "class DeliveryQueue:\n    def __init__(self, limit={m}):\n        self.items = []\n\n    def push(self, item):\n        if len(self.items) >= self.limit:\n            raise OverflowError(item)\n",
    "fn parse_port(s: &str) -> Result<u16, String> {{\n    s.trim().parse::<u16>().map_err(|e| format!(\"bad port {{s}}: {{e}}\"))\n}}\n",
    "<div class=\"card\">\r\n\t<span id=\"x{m}\">&nbsp;</span>\r\n</div>\r\n",
    "x = [i**2 for i in range({m}) if i % {h} == 0]  # noqa\n\n\n\n",
]
EMOJI = ["👩‍💻", "👨‍👩‍👧‍👦", "🏳️‍🌈", "🏴‍☠️", "👍🏽", "🧑🏿‍🚀", "🇮🇹", "🇯🇵", "🇺🇳", "1️⃣", "#️⃣", "*️⃣", "❤️", "❤", "🫶🏼",
         "👩🏻‍❤️‍💋‍👨🏾", "🏃‍♀️", "🧘🏽‍♂️", "🫠", "🐈‍⬛", "🐻‍❄️", "😶‍🌫️", "🇿🇿", "🏴󠁧󠁢󠁳󠁣󠁴󠁿", "✌🏻", "☝🏿", "🅰️", "™", "©️", "⚠️"]
LOOKALIKES = ["<bos>", "<eos>", "<pad>", "<unk>", "<mask>", "<|tool>", "<tool|>", "<|tool_call>", "<tool_call|>",
              "<|tool_response>", "<tool_response|>", "<|\"|>", "<|think|>", "<|channel>", "<channel|>", "<|turn>",
              "<turn|>", "<|image>", "<|audio>", "<|image|>", "<|audio|>", "<image|>", "<audio|>", "<|video|>",
              "<start_of_turn>", "<end_of_turn>", "<bos", "bos>", "< bos >", "<BOS>", "<Bos>", "<eos><bos>", "<eos><eos>",
              "<|tool><tool|>", "<|tool", "<|tool>>", "<<bos>>", "<bos>text<eos>", "&lt;bos&gt;", "<0x0A>", "<0xFF>",
              "<unused0>", "[multimodal]", "<|im_start|>", "<s>", "</s>", "<|endoftext|>", "▁", "▁▁", "▁<bos>▁", "<▁bos>"]
CONTROLS = ["\x00", "\x01", "\x07", "\x08", "\x0b", "\x0c", "\x1b", "\x1f", "\x7f", "\x85", "\xa0", " ", " ",
            " ", "​", "‌", "‍", "‎", "‏", "
", "
", "‪", "‮", "⁠",
            "⁦", "⁩", " ", "", "￾", "￿", "�", "", "\U000e0001", "\U0010ffff",
            "\U0001d11e", "\U00020000", "\U0002a6d6", "\U0003134a"]
ACCENTED = ["crème brûlée à la façon de {town}", "Ñandú y pingüino en el Río Zañartú", "Søren Ångström, Þórður Ægir",
            "Łódź, Žilina, Ștefan, Țara", "ẞtraße, naïve, coöperate, résumé, exposé", "Việt Nam, Hồ Chí Minh? Đà Nẵng!",
            "Džemal, Ljubljana, Njegoš", "Ḥamid, Ṣalāḥ, Ṭāriq, Ẓāhir", "Ễ ô̂ ạ̈ ṓ ṩ ǟ", "Ά έ ή ί ό ύ ώ ϊ ϋ ΐ ΰ"]
WS = [" ", "\n", "\t", "\r\n", "\r", " \t", " ", "\xa0", "\n\n"]


def fill(tpl: str, r: random.Random) -> str:
    o = r.choice(ORGS)
    return tpl.format(p=f"{r.choice(FIRST)} {r.choice(LAST)}", p2=r.choice(FIRST), o=o, o2=o.split()[0].lower()[:6],
                      t=r.choice(THINGS), c=r.choice(COSE), d=r.choice(DAYS), mo=r.choice(MONTHS), town=r.choice(TOWNS),
                      n=r.randint(2, 98), m=r.randint(10, 999), m2=r.randint(1, 59), h=r.randint(7, 21))


def sentence(r: random.Random, langs=None) -> str:
    langs = langs or ["en", "en", "it"] + list(OTHER)
    lang = r.choice(langs)
    pool = {"en": EN_TPL, "it": IT_TPL}.get(lang) or OTHER[lang]
    return fill(r.choice(pool), r)


def rand_scalar(r: random.Random) -> str:
    """A random assigned-or-not scalar from a spread of blocks (never a surrogate)."""
    ranges = [(0x20, 0x7e), (0xa0, 0x24f), (0x370, 0x3ff), (0x400, 0x4ff), (0x590, 0x6ff), (0x900, 0x97f),
              (0xe00, 0xe7f), (0x1100, 0x11ff), (0x2000, 0x206f), (0x2190, 0x23ff), (0x2500, 0x27bf),
              (0x3000, 0x30ff), (0x4e00, 0x9fff), (0xac00, 0xd7a3), (0xe000, 0xf8ff), (0xfb00, 0xfeff),
              (0x10000, 0x1049f), (0x1f300, 0x1faff), (0x20000, 0x2a6df), (0xe0000, 0xe007f), (0x30000, 0x3134f),
              (0xf0000, 0xffffd), (0x100000, 0x10ffff)]
    lo, hi = r.choice(ranges)
    return chr(r.randint(lo, hi))


def build(r: random.Random) -> list[dict]:
    out: list[dict] = []

    def add(cat, text, kind=None):
        out.append({"cat": cat, "kind": kind or r.choice(["d", "q", "raw"]), "text": text})

    for _ in range(420):
        add("en", " ".join(sentence(r, ["en"]) for _ in range(r.randint(1, 4))), "d")
    for _ in range(220):
        add("it", " ".join(sentence(r, ["it"]) for _ in range(r.randint(1, 3))), "d")
    for lang in OTHER:
        for _ in range(14 if lang in ("ar", "th", "hi", "zh", "ja", "ko") else 7):
            add(lang, " ".join(sentence(r, [lang]) for _ in range(r.randint(1, 3))))
    for _ in range(300):
        add("query", fill(r.choice(QUERIES), r), "q")
    for _ in range(200):
        add("code", "".join(fill(r.choice(CODE), r) for _ in range(r.randint(1, 3))), "d")
    # CRLF and mixed line endings
    for _ in range(160):
        s = "\r\n".join(sentence(r) for _ in range(r.randint(1, 5)))
        s = r.choice([s, s + "\r\n", "\r\n" + s, s.replace("\r\n", "\r", 1), s.replace("\r\n", "\n\r", 1), s + "\r"])
        add("crlf", s)
    # NFD / NFKC / mixed normalization forms
    for _ in range(160):
        base = fill(r.choice(ACCENTED), r) + " " + sentence(r, ["it", "fr", "pl", "vi", "tr", "cs", "hu", "el", "ru"])
        form = r.choice(["NFD", "NFD", "NFKD", "NFKC", "NFC"])
        s = unicodedata.normalize(form, base)
        if r.random() < 0.4:  # mix NFC and NFD halves
            s = unicodedata.normalize("NFC", base[: len(base) // 2]) + unicodedata.normalize("NFD", base[len(base) // 2:])
        add("nfd", s)
    # emoji sequences
    for _ in range(220):
        parts = [r.choice(EMOJI) for _ in range(r.randint(1, 8))]
        words = sentence(r, ["en"]).split()
        s = " ".join(w + (r.choice(parts) if r.random() < 0.3 else "") for w in words) if r.random() < 0.5 else "".join(parts)
        add("emoji", s)
    # scripts with combining marks and no spaces
    for lang, n in (("ar", 100), ("fa", 40), ("he", 30), ("th", 110), ("hi", 100), ("bn", 40), ("ta", 40), ("zh", 70),
                    ("zh_tw", 30), ("ja", 70), ("ko", 70), ("ka", 20), ("hy", 20), ("am", 20)):
        for _ in range(n):
            s = sentence(r, [lang])
            if lang in ("ar", "fa") and r.random() < 0.5:
                s = "".join(c + (r.choice("ًٌٍَُِّْ") if r.random() < 0.3 and "ء" <= c <= "ي" else "") for c in s)
            if r.random() < 0.3:
                s = s + " " + sentence(r, ["en"])
            add(lang + "_x", s)
    # whitespace runs > 31, tabs, newline runs, mixed
    for _ in range(180):
        ws = r.choice(WS)
        k = r.choice([31, 32, 33, 40, 63, 64, 65, 100, 127, 128, 129, 200, 500])
        a, b = sentence(r, ["en", "it"]), sentence(r, ["en"])
        s = r.choice([a + ws * k + b, ws * k + a, a + ws * k, ws * k, a + " " * k + "\t" * (k // 3) + b, (ws * k + a) * 2])
        add("whitespace", s)
    for _ in range(100):
        s = "\t".join(sentence(r).split(" ")[: r.randint(1, 12)])
        s = r.choice([s, "\t" + s, s + "\t\t", s.replace("\t", "\t \t", 1), s.replace("\t", " \t ", 2)])
        add("tabs", s)
    # control characters and invisible code points
    for _ in range(120):
        words = sentence(r).split(" ")
        s = " ".join(w + (r.choice(CONTROLS) if r.random() < 0.35 else "") for w in words)
        s = r.choice([s, r.choice(CONTROLS) + s, s + r.choice(CONTROLS), "".join(r.choice(CONTROLS) for _ in range(r.randint(1, 12)))])
        add("control", s)
    # special-token look-alikes, alone, embedded, adjacent, repeated
    for _ in range(170):
        toks = [r.choice(LOOKALIKES) for _ in range(r.randint(1, 5))]
        words = sentence(r, ["en"]).split(" ")
        pos = r.randint(0, len(words))
        s = r.choice([" ".join(words[:pos] + toks + words[pos:]), "".join(toks), " ".join(toks),
                      "".join(words[:pos]) + "".join(toks) + "".join(words[pos:]), toks[0] * r.randint(2, 6)])
        add("lookalike", s)
    # very short / empty
    for s in ["", " ", "  ", "\n", "a", "é", "é", "▁", "▁a", "🙂", "<", ">", "|", "title:", "task:", "none", "x" * 1,
              "\u0000", "퟿", "", "\U0010ffff", "0", "٠١٢٣", "१२३", "一", "あ", "한"]:
        add("short", s, "raw")
        add("short", s, "d")
    # random scalar soup (assigned, unassigned, private use, non-characters)
    for _ in range(220):
        s = "".join(rand_scalar(r) for _ in range(r.randint(1, 60)))
        add("soup", s)
    # mixed everything
    for _ in range(200):
        bits = [sentence(r), r.choice(EMOJI), r.choice(LOOKALIKES), r.choice(CONTROLS), r.choice(WS) * r.randint(1, 40),
                fill(r.choice(CODE), r), unicodedata.normalize("NFD", fill(r.choice(ACCENTED), r)), "".join(rand_scalar(r) for _ in range(5))]
        r.shuffle(bits)
        add("mixed", "".join(bits[: r.randint(2, 8)]))
    # very long inputs (5k .. 80k characters)
    for i in range(22):
        n = [5000, 8000, 12000, 20000, 30000, 50000, 80000][i % 7]
        parts = []
        while sum(len(p) for p in parts) < n:
            parts.append(r.choice([sentence(r), fill(r.choice(CODE), r), r.choice(EMOJI), r.choice(WS) * r.randint(1, 50),
                                   "".join(rand_scalar(r) for _ in range(20)), r.choice(LOOKALIKES)]))
        add("long", " ".join(parts), "d")
    return out


def main() -> None:
    ap = argparse.ArgumentParser(description=__doc__.splitlines()[0])
    ap.add_argument("--tokenizer", required=True, help="path to tokenizer.json")
    ap.add_argument("--out", required=True)
    ap.add_argument("--seed", type=int, default=20261012)
    args = ap.parse_args()
    from tokenizers import Tokenizer

    tok = Tokenizer.from_file(args.tokenizer)
    cases = build(random.Random(args.seed))
    for c in cases:
        c["ids"] = [2] + tok.encode(PREFIX[c["kind"]] + c["text"], add_special_tokens=False).ids + [1]
    Path(args.out).write_text(json.dumps(cases, ensure_ascii=False), encoding="utf-8")
    from collections import Counter
    print(f"{len(cases)} cases, {sum(len(c['ids']) for c in cases)} ids ->", args.out)
    print(dict(Counter(c["cat"] for c in cases)))


if __name__ == "__main__":
    main()