Harley-ml commited on
Commit
c01050f
Β·
verified Β·
1 Parent(s): df7e9f0

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +523 -333
app.py CHANGED
@@ -9,19 +9,19 @@ import gradio as gr
9
  import torch
10
  from transformers import AutoModel, AutoTokenizer
11
 
12
- # ─── constants ────────────────────────────────────────────────────────────────
13
 
14
  MODEL_ID = "fromziro/JetonCount"
15
  DEFAULT_VOCAB_SIZE = 32_000
16
 
17
  PUNCTUATION_CHARS = set(r""".,!?;:'"`~@#$%^&*()-_=+[]{}<>/\|""")
18
- SYMBOL_CHARS = set(r"""@#$%^&*()-_=+[]{}<>/\|~`""")
19
 
20
  DEVICE = torch.device("cuda" if torch.cuda.is_available() else "cpu")
21
  if DEVICE.type == "cuda":
22
  torch.backends.cudnn.benchmark = True
23
 
24
- # ─── model / tokenizer loading ────────────────────────────────────────────────
25
 
26
  @functools.lru_cache(maxsize=1)
27
  def load_model():
@@ -45,7 +45,7 @@ def get_vocab_size(tokenizer) -> int:
45
  except Exception:
46
  return DEFAULT_VOCAB_SIZE
47
 
48
- # ─── feature computation ──────────────────────────────────────────────────────
49
 
50
  def compute_stats(text: str, vocab_size: int) -> dict:
51
  chars = len(text)
@@ -59,61 +59,56 @@ def compute_stats(text: str, vocab_size: int) -> dict:
59
  else:
60
  punct = sym = 0.0
61
  return dict(
62
- chars=float(chars),
63
- words=float(words),
64
- avg_chars_per_word=float(avg_cw),
65
- punctuation_ratio=float(punct),
66
- symbol_ratio=float(sym),
67
- longest_word_chars=float(longest),
68
  vocab_size=float(vocab_size),
69
  )
70
 
71
- # ─── inference ────────────────────────────────────────────────────────────────
72
 
73
  @torch.inference_mode()
74
  def predict(stats: dict) -> float:
75
- """Pass 7 base features β€” model handles engineering + standardization internally."""
76
  x = torch.tensor(
77
- [[
78
- stats["chars"],
79
- stats["words"],
80
- stats["avg_chars_per_word"],
81
- stats["punctuation_ratio"],
82
- stats["symbol_ratio"],
83
- stats["longest_word_chars"],
84
- stats["vocab_size"],
85
- ]],
86
- dtype=torch.float32,
87
- device=DEVICE,
88
  )
89
  out = load_model()(input_features=x)
90
- raw = float(out.logits.squeeze().item())
91
- return max(0.0, raw) # token counts are non-negative
 
 
 
 
 
 
 
92
 
93
- # ─── event handlers ───────────────────────────────────────────────────────────
94
 
95
  def on_tokenizer_change(tokenizer_id: str):
96
  tid = (tokenizer_id or "").strip()
97
  if not tid:
98
- return gr.update(interactive=True), "", None
99
  try:
100
  tok = load_tokenizer(tid)
101
  vs = get_vocab_size(tok)
102
  return (
103
  gr.update(value=vs, interactive=False),
104
- f"βœ“ Locked to `{tid}` β€” vocab size {vs:,}",
105
  vs,
106
  )
107
  except Exception as exc:
108
  return (
109
  gr.update(interactive=True),
110
- f"βœ— Could not load `{tid}`: {exc}",
111
  None,
112
  )
113
 
114
 
115
  def on_clear(current_vocab):
116
- return gr.update(value=""), gr.update(interactive=True), None, ""
117
 
118
 
119
  def run(text: str, vocab_size_val, tokenizer_id: str, locked_vocab: Optional[int]):
@@ -123,13 +118,12 @@ def run(text: str, vocab_size_val, tokenizer_id: str, locked_vocab: Optional[int
123
  actual_count: Optional[int] = None
124
  tok_error: Optional[str] = None
125
 
126
- # Resolve vocab size and (optionally) actual token count
127
  if tid:
128
  try:
129
- tok = load_tokenizer(tid)
130
  resolved_vocab = locked_vocab if locked_vocab is not None else get_vocab_size(tok)
131
- ids = tok(text, add_special_tokens=False).input_ids
132
- actual_count = len(ids)
133
  except Exception as exc:
134
  tok_error = str(exc)
135
  resolved_vocab = _safe_int(vocab_size_val, DEFAULT_VOCAB_SIZE)
@@ -137,20 +131,16 @@ def run(text: str, vocab_size_val, tokenizer_id: str, locked_vocab: Optional[int
137
  resolved_vocab = _safe_int(vocab_size_val, DEFAULT_VOCAB_SIZE)
138
 
139
  stats = compute_stats(text, resolved_vocab)
140
-
141
  try:
142
  pred = predict(stats)
143
  except Exception as exc:
144
  return _render_error(str(exc)), None
145
 
146
- result_data = {
147
- "prediction": pred,
148
- "actual_count": actual_count,
149
- "vocab_size": resolved_vocab,
150
- "tokenizer_id": tid,
151
- "stats": stats,
152
- "tok_error": tok_error,
153
- }
154
  return _render_results(result_data), result_data
155
 
156
 
@@ -160,337 +150,544 @@ def _safe_int(val, default: int) -> int:
160
  except Exception:
161
  return default
162
 
163
- # ─── HTML rendering ───────────────────────────────────────────────────────────
 
 
 
 
 
 
 
 
164
 
165
  def _render_error(msg: str) -> str:
166
  return f"""
167
- <div class="card error-card">
168
- <div class="card-title">Error</div>
169
- <div class="card-body">{html.escape(msg)}</div>
 
 
 
170
  </div>
171
- """
 
172
 
173
 
174
  def _render_results(r: dict) -> str:
175
  pred = r["prediction"]
176
  actual = r["actual_count"]
177
  vocab = r["vocab_size"]
178
- tid = html.escape(r["tokenizer_id"]) if r["tokenizer_id"] else "β€”"
179
  stats = r["stats"]
180
  tok_error = r["tok_error"]
 
181
 
182
- pred_rounded = round(pred)
 
 
183
 
184
- # Comparison block
185
  if actual is not None:
186
- diff = pred_rounded - actual
187
- sign = "+" if diff > 0 else ""
188
- cls = "diff-pos" if diff > 0 else ("diff-neg" if diff < 0 else "diff-zero")
189
- pct = abs(diff) / max(actual, 1) * 100
 
 
 
 
 
 
 
 
 
 
 
 
 
 
190
  comparison = f"""
191
- <div class="metric-card">
192
- <div class="metric-label">Actual tokens</div>
193
- <div class="metric-value">{actual:,}</div>
194
- <div class="metric-sub {cls}">{sign}{diff:,} ({pct:.1f}% off)</div>
 
195
  </div>
196
- """
197
- elif r["tokenizer_id"] and tok_error:
198
- comparison = f"""
199
- <div class="metric-card card-error">
200
- <div class="metric-label">Tokenizer error</div>
201
- <div class="metric-value">β€”</div>
202
- <div class="metric-sub">{html.escape(tok_error[:80])}</div>
 
 
 
 
 
 
203
  </div>
204
- """
 
 
 
 
 
 
 
 
205
  else:
206
  comparison = ""
207
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
208
  return f"""
209
- <div class="results">
210
- <div class="results-meta">
211
- <span class="mono">{html.escape(MODEL_ID)}</span>
212
- <span class="sep">Β·</span>
213
- tokenizer: <span class="mono">{tid}</span>
214
- <span class="sep">Β·</span>
215
- vocab: <span class="mono">{vocab:,}</span>
216
- </div>
217
 
218
- <div class="metric-row">
219
- <div class="metric-card primary-card">
220
- <div class="metric-label">Predicted tokens</div>
221
- <div class="metric-value hero-num">{pred_rounded:,}</div>
222
- <div class="metric-sub mono">{pred:.4f} raw</div>
223
- </div>
224
- {comparison}
225
- </div>
226
 
227
- <div class="card feature-card">
228
- <div class="card-title">Text features</div>
229
- <div class="feature-grid">
230
- <div class="fi"><span>Characters</span><span class="mono">{int(stats['chars']):,}</span></div>
231
- <div class="fi"><span>Words</span><span class="mono">{int(stats['words']):,}</span></div>
232
- <div class="fi"><span>Avg chars / word</span><span class="mono">{stats['avg_chars_per_word']:.3f}</span></div>
233
- <div class="fi"><span>Longest word</span><span class="mono">{int(stats['longest_word_chars'])} chars</span></div>
234
- <div class="fi"><span>Punctuation ratio</span><span class="mono">{stats['punctuation_ratio']:.4f}</span></div>
235
- <div class="fi"><span>Symbol ratio</span><span class="mono">{stats['symbol_ratio']:.4f}</span></div>
236
- </div>
237
- </div>
238
- </div>
239
- """
240
 
241
- # ─── CSS ──────────────────────────────────────────────────────────────────────
 
 
 
 
 
 
242
 
243
  CSS = """
244
- /* ── base ── */
245
  :root {
246
- --bg: #080808;
247
- --surface: #111111;
248
- --border: #252525;
249
- --text: #f0f0f0;
250
- --muted: #888;
251
- --accent: #ffffff;
252
- --green: #4ade80;
253
- --red: #f87171;
254
- --blue: #60a5fa;
255
- --radius: 14px;
256
- }
 
 
 
 
 
 
257
 
258
  body, .gradio-container {
259
- background: var(--bg) !important;
260
- color: var(--text) !important;
 
261
  }
262
-
263
  .gradio-container {
264
- max-width: 1100px !important;
265
- margin: 0 auto !important;
266
- padding: 20px !important;
267
  }
268
 
269
- /* ── text overrides ── */
270
- h1, h2, h3, h4, p, span, label, div, textarea, input {
271
- color: var(--text) !important;
272
  }
273
 
274
- .mono {
275
- font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace !important;
276
- }
277
 
278
  /* ── hero ── */
279
  .hero {
280
- padding: 20px 24px 18px;
281
- border: 1px solid var(--border);
282
- border-radius: 20px;
283
- background: linear-gradient(160deg, rgba(255,255,255,0.045) 0%, rgba(255,255,255,0.01) 100%);
284
- margin-bottom: 20px;
 
 
 
 
 
 
 
 
285
  }
286
  .hero-title {
287
- font-size: 26px;
288
- font-weight: 800;
289
- letter-spacing: -0.03em;
290
- margin-bottom: 6px;
291
- }
292
- .hero-sub {
293
- font-size: 13px;
294
- color: var(--muted) !important;
295
- line-height: 1.55;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
296
  }
297
 
298
  /* ── inputs ── */
299
- textarea, input, select {
300
- background: var(--surface) !important;
301
- border: 1px solid var(--border) !important;
302
- border-radius: var(--radius) !important;
303
- box-shadow: none !important;
 
 
304
  }
305
  textarea:focus, input:focus {
306
- border-color: #444 !important;
307
- outline: none !important;
308
  }
309
  textarea::placeholder, input::placeholder {
310
- color: var(--muted) !important;
 
 
 
 
 
 
 
 
311
  }
312
 
313
  /* ── buttons ── */
314
- button { border-radius: 12px !important; border: 1px solid var(--border) !important; }
 
 
 
 
 
315
  button.primary {
316
- background: #ffffff !important;
317
- color: #000 !important;
318
- border-color: #fff !important;
319
- font-weight: 700 !important;
320
  }
 
 
321
  button.secondary {
322
- background: var(--surface) !important;
323
- color: var(--muted) !important;
324
- }
325
-
326
- /* ── panels ── */
327
- .block, .block-container, .group, .wrap, .panel {
328
- background: transparent !important;
329
- }
330
-
331
- /* ── hint ── */
332
- .hint {
333
- font-size: 12px !important;
334
- color: var(--muted) !important;
335
- margin-top: 6px !important;
336
- line-height: 1.4 !important;
337
- }
338
-
339
- /* ── status strip ── */
340
- #status {
341
- font-size: 13px !important;
342
- color: var(--muted) !important;
343
- padding: 8px 12px !important;
344
- border: 1px solid var(--border) !important;
345
- border-radius: var(--radius) !important;
346
- background: var(--surface) !important;
347
- min-height: 36px !important;
348
- }
349
-
350
- /* ── results ── */
351
- .results {
352
- padding-top: 4px;
353
- }
354
- .results-meta {
355
- font-size: 12px;
356
- color: var(--muted) !important;
357
- margin-bottom: 14px;
358
- letter-spacing: 0.01em;
359
- }
360
- .results-meta .sep { margin: 0 6px; opacity: .4; }
361
-
362
- .metric-row {
363
- display: flex;
364
- gap: 12px;
365
- margin-bottom: 14px;
366
- flex-wrap: wrap;
367
- }
368
-
369
- .metric-card {
370
- flex: 1;
371
- min-width: 160px;
372
- padding: 16px 18px;
373
- border: 1px solid var(--border);
374
- border-radius: var(--radius);
375
- background: linear-gradient(160deg, rgba(255,255,255,0.04), rgba(255,255,255,0.015));
376
- }
377
- .primary-card {
378
- border-color: rgba(255,255,255,0.12);
379
- }
380
- .card-error {
381
- border-color: rgba(248,113,113,0.3);
382
- }
383
-
384
- .metric-label {
385
- font-size: 11px;
386
- text-transform: uppercase;
387
- letter-spacing: 0.08em;
388
- color: var(--muted) !important;
389
- margin-bottom: 8px;
390
- }
391
- .metric-value {
392
- font-size: 22px;
393
- font-weight: 800;
394
- line-height: 1;
395
- margin-bottom: 6px;
396
- font-variant-numeric: tabular-nums;
397
- }
398
- .hero-num { font-size: 32px; }
399
-
400
- .metric-sub {
401
- font-size: 12px;
402
- color: var(--muted) !important;
403
- }
404
- .diff-pos { color: var(--red) !important; }
405
- .diff-neg { color: var(--blue) !important; }
406
- .diff-zero { color: var(--green) !important; }
407
-
408
- /* ── feature card ── */
409
- .card {
410
- border: 1px solid var(--border);
411
- border-radius: var(--radius);
412
- background: linear-gradient(160deg, rgba(255,255,255,0.03), rgba(255,255,255,0.01));
413
- padding: 14px 16px;
414
- }
415
- .feature-card { margin-bottom: 6px; }
416
- .card-title {
417
- font-size: 13px;
418
- font-weight: 700;
419
- margin-bottom: 12px;
420
- color: var(--text) !important;
421
- }
422
- .card-body { font-size: 13px; color: var(--muted) !important; }
423
-
424
- .feature-grid {
425
- display: grid;
426
- grid-template-columns: repeat(3, 1fr);
427
- gap: 8px;
428
- }
429
- @media (max-width: 600px) {
430
- .feature-grid { grid-template-columns: repeat(2, 1fr); }
431
- }
432
-
433
- .fi {
434
- display: flex;
435
- justify-content: space-between;
436
- align-items: center;
437
- gap: 8px;
438
- padding: 9px 11px;
439
- border: 1px solid rgba(255,255,255,0.06);
440
- border-radius: 10px;
441
- background: rgba(255,255,255,0.02);
442
- font-size: 12px;
443
- }
444
- .fi span:first-child { color: var(--muted) !important; }
445
-
446
- .error-card {
447
- border: 1px solid rgba(248,113,113,0.4) !important;
448
- border-radius: var(--radius) !important;
449
- padding: 16px;
450
- background: rgba(248,113,113,0.06);
451
- }
452
- .error-card .card-title { color: var(--red) !important; margin-bottom: 4px; }
453
- .error-card .card-body { color: var(--muted) !important; font-size: 13px; }
454
-
455
- /* ── empty state ── */
456
- .empty {
457
- padding: 32px 24px;
458
- text-align: center;
459
- border: 1px solid var(--border);
460
- border-radius: var(--radius);
461
- background: linear-gradient(160deg, rgba(255,255,255,0.025), rgba(255,255,255,0.01));
462
- }
463
- .empty-title { font-size: 16px; font-weight: 700; margin-bottom: 6px; }
464
- .empty-body { font-size: 13px; color: var(--muted) !important; line-height: 1.55; }
465
 
466
  /* ── accordion ── */
467
- .accordion {
468
- border: 1px solid var(--border) !important;
469
- border-radius: var(--radius) !important;
470
- background: var(--surface) !important;
471
- }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
472
  """
473
 
474
  EMPTY_HTML = """
475
- <div class="empty">
476
- <div class="empty-title">No prediction yet</div>
477
- <div class="empty-body">
478
- Paste text and press <b>Predict</b>.<br>
479
- Optionally add a tokenizer repo ID to compare against ground-truth token count.
 
480
  </div>
481
  </div>
482
  """
483
 
484
- # ─── UI ───────────────────────────────────────────────────────────────────────
485
 
486
  with gr.Blocks(title="JetonCount") as demo:
487
 
488
- gr.HTML("""
489
  <div class="hero">
 
490
  <div class="hero-title">JetonCount</div>
491
- <div class="hero-sub">
492
- Predict token count from text statistics β€” no tokenizer required.
493
- Optionally provide a Hugging Face tokenizer repo to compare the prediction against the real count.
 
 
 
 
 
494
  </div>
495
  </div>
496
  """)
@@ -498,19 +695,22 @@ with gr.Blocks(title="JetonCount") as demo:
498
  locked_vocab = gr.State(None)
499
 
500
  with gr.Row(equal_height=False):
501
- # ── left: text input ──────────────────────────────────────────────────
502
- with gr.Column(scale=3, min_width=400):
 
503
  text_in = gr.Textbox(
504
  label="Text",
505
- lines=16,
506
  placeholder="Paste your text here…",
 
507
  )
508
- predict_btn = gr.Button("Predict", variant="primary", size="lg")
 
509
 
510
- # ── right: settings ───────────────────────────────────────────────────
511
- with gr.Column(scale=2, min_width=280):
512
  tokenizer_in = gr.Textbox(
513
- label="Tokenizer repo ID (optional)",
514
  placeholder="e.g. openai-community/gpt2",
515
  )
516
  vocab_in = gr.Number(
@@ -519,11 +719,9 @@ with gr.Blocks(title="JetonCount") as demo:
519
  precision=0,
520
  interactive=True,
521
  )
522
- gr.HTML('<div class="hint">When a tokenizer is entered, vocab size locks automatically.</div>')
523
- status = gr.Markdown(value="", elem_id="status")
524
  clear_btn = gr.Button("Clear tokenizer", variant="secondary", size="sm")
525
 
526
- # ── results ───────────────────────────────────────────────────────────────
527
  results_html = gr.HTML(value=EMPTY_HTML)
528
 
529
  with gr.Accordion("Raw JSON", open=False):
@@ -531,34 +729,26 @@ with gr.Blocks(title="JetonCount") as demo:
531
 
532
  # ── wiring ────────────────────────────────────────────────────────────────
533
 
534
- def _on_tok_change(tid):
535
- return on_tokenizer_change(tid)
536
 
537
  tokenizer_in.blur(
538
- fn=_on_tok_change,
539
- inputs=[tokenizer_in],
540
- outputs=[vocab_in, status, locked_vocab],
541
  )
542
  tokenizer_in.submit(
543
- fn=_on_tok_change,
544
- inputs=[tokenizer_in],
545
- outputs=[vocab_in, status, locked_vocab],
546
  )
547
-
548
  clear_btn.click(
549
- fn=on_clear,
550
- inputs=[vocab_in],
551
- outputs=[tokenizer_in, vocab_in, locked_vocab, status],
552
  )
553
-
554
  predict_btn.click(
555
- fn=run,
556
- inputs=[text_in, vocab_in, tokenizer_in, locked_vocab],
557
  outputs=[results_html, raw_json],
558
  )
559
  text_in.submit(
560
- fn=run,
561
- inputs=[text_in, vocab_in, tokenizer_in, locked_vocab],
562
  outputs=[results_html, raw_json],
563
  )
564
 
 
9
  import torch
10
  from transformers import AutoModel, AutoTokenizer
11
 
12
+ # ── constants ─────────────────────────────────────────────────────────────────
13
 
14
  MODEL_ID = "fromziro/JetonCount"
15
  DEFAULT_VOCAB_SIZE = 32_000
16
 
17
  PUNCTUATION_CHARS = set(r""".,!?;:'"`~@#$%^&*()-_=+[]{}<>/\|""")
18
+ SYMBOL_CHARS = set(r"""@#$%^&*()-_=+[]{}<>/\|~`""")
19
 
20
  DEVICE = torch.device("cuda" if torch.cuda.is_available() else "cpu")
21
  if DEVICE.type == "cuda":
22
  torch.backends.cudnn.benchmark = True
23
 
24
+ # ── model / tokenizer loading ─────────────────────────────────────────────────
25
 
26
  @functools.lru_cache(maxsize=1)
27
  def load_model():
 
45
  except Exception:
46
  return DEFAULT_VOCAB_SIZE
47
 
48
+ # ── feature computation ───────────────────────────────────────────────────────
49
 
50
  def compute_stats(text: str, vocab_size: int) -> dict:
51
  chars = len(text)
 
59
  else:
60
  punct = sym = 0.0
61
  return dict(
62
+ chars=float(chars), words=float(words),
63
+ avg_chars_per_word=float(avg_cw), punctuation_ratio=float(punct),
64
+ symbol_ratio=float(sym), longest_word_chars=float(longest),
 
 
 
65
  vocab_size=float(vocab_size),
66
  )
67
 
68
+ # ── inference ─────────────────────────────────────────────────────────────────
69
 
70
  @torch.inference_mode()
71
  def predict(stats: dict) -> float:
 
72
  x = torch.tensor(
73
+ [[stats["chars"], stats["words"], stats["avg_chars_per_word"],
74
+ stats["punctuation_ratio"], stats["symbol_ratio"],
75
+ stats["longest_word_chars"], stats["vocab_size"]]],
76
+ dtype=torch.float32, device=DEVICE,
 
 
 
 
 
 
 
77
  )
78
  out = load_model()(input_features=x)
79
+ return max(0.0, float(out.logits.squeeze().item()))
80
+
81
+ # ── event handlers ────────────────────────────────────────────────────────────
82
+
83
+ def on_text_change(text: str):
84
+ text = text or ""
85
+ chars = len(text)
86
+ words = len(re.findall(r"\b\w+\b", text, flags=re.UNICODE))
87
+ return f"<div class='live-counter'><span>{chars:,} chars</span><span class='sep'>Β·</span><span>{words:,} words</span></div>"
88
 
 
89
 
90
  def on_tokenizer_change(tokenizer_id: str):
91
  tid = (tokenizer_id or "").strip()
92
  if not tid:
93
+ return gr.update(interactive=True), _status(""), None
94
  try:
95
  tok = load_tokenizer(tid)
96
  vs = get_vocab_size(tok)
97
  return (
98
  gr.update(value=vs, interactive=False),
99
+ _status(f"Locked to <b>{html.escape(tid)}</b> β€” vocab {vs:,}", kind="ok"),
100
  vs,
101
  )
102
  except Exception as exc:
103
  return (
104
  gr.update(interactive=True),
105
+ _status(f"Could not load <b>{html.escape(tid)}</b>: {html.escape(str(exc)[:120])}", kind="err"),
106
  None,
107
  )
108
 
109
 
110
  def on_clear(current_vocab):
111
+ return gr.update(value=""), gr.update(interactive=True), None, _status("")
112
 
113
 
114
  def run(text: str, vocab_size_val, tokenizer_id: str, locked_vocab: Optional[int]):
 
118
  actual_count: Optional[int] = None
119
  tok_error: Optional[str] = None
120
 
 
121
  if tid:
122
  try:
123
+ tok = load_tokenizer(tid)
124
  resolved_vocab = locked_vocab if locked_vocab is not None else get_vocab_size(tok)
125
+ ids = tok(text, add_special_tokens=False).input_ids
126
+ actual_count = len(ids)
127
  except Exception as exc:
128
  tok_error = str(exc)
129
  resolved_vocab = _safe_int(vocab_size_val, DEFAULT_VOCAB_SIZE)
 
131
  resolved_vocab = _safe_int(vocab_size_val, DEFAULT_VOCAB_SIZE)
132
 
133
  stats = compute_stats(text, resolved_vocab)
 
134
  try:
135
  pred = predict(stats)
136
  except Exception as exc:
137
  return _render_error(str(exc)), None
138
 
139
+ result_data = dict(
140
+ prediction=pred, actual_count=actual_count,
141
+ vocab_size=resolved_vocab, tokenizer_id=tid,
142
+ stats=stats, tok_error=tok_error,
143
+ )
 
 
 
144
  return _render_results(result_data), result_data
145
 
146
 
 
150
  except Exception:
151
  return default
152
 
153
+
154
+ def _status(msg: str, kind: str = "") -> str:
155
+ if not msg:
156
+ return ""
157
+ icon = "βœ“" if kind == "ok" else ("βœ—" if kind == "err" else "β„Ή")
158
+ cls = f"status-{kind}" if kind else ""
159
+ return f"<div class='status-pill {cls}'><span class='status-icon'>{icon}</span>{msg}</div>"
160
+
161
+ # ── HTML rendering ─────────────────────────────────────────────────────────────
162
 
163
  def _render_error(msg: str) -> str:
164
  return f"""
165
+ <div class="result-wrap">
166
+ <div class="error-banner">
167
+ <span class="err-icon">⚠</span>
168
+ <div>
169
+ <div class="err-title">Something went wrong</div>
170
+ <div class="err-body">{html.escape(msg)}</div>
171
  </div>
172
+ </div>
173
+ </div>"""
174
 
175
 
176
  def _render_results(r: dict) -> str:
177
  pred = r["prediction"]
178
  actual = r["actual_count"]
179
  vocab = r["vocab_size"]
180
+ tid_raw = r["tokenizer_id"]
181
  stats = r["stats"]
182
  tok_error = r["tok_error"]
183
+ tid = html.escape(tid_raw) if tid_raw else None
184
 
185
+ pred_int = round(pred)
186
+ chars_int = int(stats["chars"])
187
+ words_int = int(stats["words"])
188
 
189
+ # ── comparison section ──
190
  if actual is not None:
191
+ diff = pred_int - actual
192
+ abs_diff = abs(diff)
193
+ pct = abs_diff / max(actual, 1) * 100
194
+ accuracy = max(0.0, 100.0 - pct)
195
+ bar_w = min(100, round(accuracy))
196
+
197
+ if abs_diff == 0:
198
+ diff_label = "exact match"
199
+ diff_cls = "diff-exact"
200
+ diff_sign = ""
201
+ else:
202
+ sign = "+" if diff > 0 else "βˆ’"
203
+ diff_sign = f"{sign}{abs_diff:,}"
204
+ diff_label = f"{pct:.1f}% off"
205
+ diff_cls = "diff-over" if diff > 0 else "diff-under"
206
+
207
+ bar_color = "#4ade80" if accuracy >= 95 else ("#facc15" if accuracy >= 80 else "#f87171")
208
+
209
  comparison = f"""
210
+ <div class="compare-block">
211
+ <div class="compare-cards">
212
+ <div class="ccard predicted">
213
+ <div class="ccard-label">Predicted</div>
214
+ <div class="ccard-num">{pred_int:,}</div>
215
  </div>
216
+ <div class="ccard-divider">vs</div>
217
+ <div class="ccard actual">
218
+ <div class="ccard-label">Actual</div>
219
+ <div class="ccard-num">{actual:,}</div>
220
+ </div>
221
+ </div>
222
+ <div class="accuracy-row">
223
+ <div class="accuracy-bar-bg">
224
+ <div class="accuracy-bar-fill" style="width:{bar_w}%; background:{bar_color};"></div>
225
+ </div>
226
+ <div class="accuracy-meta">
227
+ <span class="accuracy-pct">{accuracy:.1f}% accuracy</span>
228
+ <span class="{diff_cls}">{diff_sign and diff_sign + " Β· "}{diff_label}</span>
229
  </div>
230
+ </div>
231
+ </div>"""
232
+
233
+ elif tid_raw and tok_error:
234
+ comparison = f"""
235
+ <div class="tok-error">
236
+ <span class="tok-err-icon">⚠</span>
237
+ Tokenizer error: {html.escape((tok_error or "")[:120])}
238
+ </div>"""
239
  else:
240
  comparison = ""
241
 
242
+ # ── feature chips ──
243
+ def chip(label, value):
244
+ return f'<div class="chip"><span class="chip-label">{label}</span><span class="chip-val">{value}</span></div>'
245
+
246
+ chips = "".join([
247
+ chip("chars", f"{chars_int:,}"),
248
+ chip("words", f"{words_int:,}"),
249
+ chip("avg chars/wd", f"{stats['avg_chars_per_word']:.2f}"),
250
+ chip("longest word", f"{int(stats['longest_word_chars'])}"),
251
+ chip("punct ratio", f"{stats['punctuation_ratio']:.4f}"),
252
+ chip("symbol ratio", f"{stats['symbol_ratio']:.4f}"),
253
+ ])
254
+
255
+ meta_tok = f'<span class="meta-tag">{tid}</span>' if tid else '<span class="meta-tag muted">no tokenizer</span>'
256
+ meta_vocab = f'<span class="meta-tag">{vocab:,} vocab</span>'
257
+ device_tag = f'<span class="meta-tag">{DEVICE.type.upper()}</span>'
258
+
259
  return f"""
260
+ <div class="result-wrap">
261
+ <div class="result-header">
262
+ <div class="result-meta">{meta_tok}{meta_vocab}{device_tag}</div>
263
+ </div>
 
 
 
 
264
 
265
+ <div class="pred-hero">
266
+ <div class="pred-label">Estimated token count</div>
267
+ <div class="pred-number">{pred_int:,}</div>
268
+ <div class="pred-raw">{pred:.5f} Β· {chars_int / max(pred_int,1):.2f} chars/token</div>
269
+ </div>
 
 
 
270
 
271
+ {comparison}
 
 
 
 
 
 
 
 
 
 
 
 
272
 
273
+ <div class="features-section">
274
+ <div class="features-title">Text features</div>
275
+ <div class="chips">{chips}</div>
276
+ </div>
277
+ </div>"""
278
+
279
+ # ── CSS ───────────────────────────────────────────────────────────────────────
280
 
281
  CSS = """
 
282
  :root {
283
+ --bg: #070707;
284
+ --surf: #0f0f0f;
285
+ --surf2: #161616;
286
+ --border: #222;
287
+ --border2: #2e2e2e;
288
+ --text: #efefef;
289
+ --muted: #777;
290
+ --muted2: #555;
291
+ --green: #4ade80;
292
+ --yellow: #facc15;
293
+ --red: #f87171;
294
+ --blue: #60a5fa;
295
+ --r: 12px;
296
+ --r-sm: 8px;
297
+ }
298
+
299
+ *, *::before, *::after { box-sizing: border-box; }
300
 
301
  body, .gradio-container {
302
+ background: var(--bg) !important;
303
+ color: var(--text) !important;
304
+ font-family: ui-sans-serif, system-ui, -apple-system, sans-serif !important;
305
  }
 
306
  .gradio-container {
307
+ max-width: 1060px !important;
308
+ margin: 0 auto !important;
309
+ padding: 24px 20px !important;
310
  }
311
 
312
+ h1,h2,h3,h4,p,span,label,div,textarea,input,select,button {
313
+ color: var(--text) !important;
 
314
  }
315
 
316
+ .mono { font-family: ui-monospace, "SF Mono", Menlo, monospace !important; }
 
 
317
 
318
  /* ── hero ── */
319
  .hero {
320
+ padding: 22px 26px 20px;
321
+ border: 1px solid var(--border2);
322
+ border-radius: 18px;
323
+ background: linear-gradient(145deg, rgba(255,255,255,0.038) 0%, rgba(255,255,255,0.008) 100%);
324
+ margin-bottom: 22px;
325
+ }
326
+ .hero-eyebrow {
327
+ font-size: 11px;
328
+ font-weight: 600;
329
+ letter-spacing: 0.12em;
330
+ text-transform: uppercase;
331
+ color: var(--muted) !important;
332
+ margin-bottom: 8px;
333
  }
334
  .hero-title {
335
+ font-size: 28px;
336
+ font-weight: 800;
337
+ letter-spacing: -0.03em;
338
+ line-height: 1;
339
+ margin-bottom: 10px;
340
+ }
341
+ .hero-desc {
342
+ font-size: 13.5px;
343
+ color: var(--muted) !important;
344
+ line-height: 1.6;
345
+ max-width: 680px;
346
+ }
347
+ .hero-badges {
348
+ display: flex;
349
+ gap: 6px;
350
+ margin-top: 14px;
351
+ flex-wrap: wrap;
352
+ }
353
+ .badge {
354
+ font-size: 11px;
355
+ font-weight: 600;
356
+ padding: 3px 10px;
357
+ border-radius: 99px;
358
+ border: 1px solid var(--border2);
359
+ color: var(--muted) !important;
360
+ background: var(--surf);
361
+ letter-spacing: 0.04em;
362
+ }
363
+
364
+ /* ── gradio internals ── */
365
+ .block, .block-container, .group, .wrap, .panel, .form {
366
+ background: transparent !important;
367
+ border: none !important;
368
+ box-shadow: none !important;
369
  }
370
 
371
  /* ── inputs ── */
372
+ textarea, input[type=text], input[type=number], select {
373
+ background: var(--surf) !important;
374
+ border: 1px solid var(--border2) !important;
375
+ border-radius: var(--r) !important;
376
+ color: var(--text) !important;
377
+ box-shadow: none !important;
378
+ transition: border-color 0.15s !important;
379
  }
380
  textarea:focus, input:focus {
381
+ border-color: #3a3a3a !important;
382
+ outline: none !important;
383
  }
384
  textarea::placeholder, input::placeholder {
385
+ color: var(--muted2) !important;
386
+ }
387
+ .label-wrap label, .svelte-1gfkn6j {
388
+ font-size: 12px !important;
389
+ font-weight: 600 !important;
390
+ letter-spacing: 0.04em !important;
391
+ text-transform: uppercase !important;
392
+ color: var(--muted) !important;
393
+ margin-bottom: 6px !important;
394
  }
395
 
396
  /* ── buttons ── */
397
+ button {
398
+ border-radius: var(--r) !important;
399
+ border: 1px solid var(--border2) !important;
400
+ font-weight: 600 !important;
401
+ transition: opacity 0.15s, transform 0.1s !important;
402
+ }
403
  button.primary {
404
+ background: #fff !important;
405
+ color: #000 !important;
406
+ border-color: #fff !important;
407
+ letter-spacing: 0.01em !important;
408
  }
409
+ button.primary:hover { opacity: 0.88 !important; }
410
+ button.primary:active { transform: scale(0.98) !important; }
411
  button.secondary {
412
+ background: var(--surf) !important;
413
+ color: var(--muted) !important;
414
+ }
415
+ button.secondary:hover { border-color: #3a3a3a !important; color: var(--text) !important; }
416
+
417
+ /* ── live counter ── */
418
+ .live-counter {
419
+ font-size: 12px;
420
+ color: var(--muted) !important;
421
+ padding: 6px 2px 0;
422
+ display: flex;
423
+ gap: 0;
424
+ align-items: center;
425
+ }
426
+ .live-counter .sep { margin: 0 8px; opacity: 0.35; }
427
+
428
+ /* ── status pill ── */
429
+ .status-pill {
430
+ font-size: 12.5px;
431
+ line-height: 1.5;
432
+ padding: 8px 12px;
433
+ border-radius: var(--r-sm);
434
+ border: 1px solid var(--border);
435
+ background: var(--surf);
436
+ color: var(--muted) !important;
437
+ display: flex;
438
+ align-items: flex-start;
439
+ gap: 8px;
440
+ }
441
+ .status-pill.status-ok { border-color: rgba(74,222,128,0.25); background: rgba(74,222,128,0.06); }
442
+ .status-pill.status-err { border-color: rgba(248,113,113,0.25); background: rgba(248,113,113,0.06); }
443
+ .status-icon { opacity: 0.7; flex-shrink: 0; margin-top: 1px; }
444
+ .status-pill b { font-weight: 600; color: inherit !important; }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
445
 
446
  /* ── accordion ── */
447
+ details, .accordion {
448
+ border: 1px solid var(--border) !important;
449
+ border-radius: var(--r) !important;
450
+ background: var(--surf) !important;
451
+ }
452
+
453
+ /* ═══════════════════════════════
454
+ RESULT PANEL
455
+ ═══════════════════════════════ */
456
+ .result-wrap {
457
+ display: flex;
458
+ flex-direction: column;
459
+ gap: 14px;
460
+ }
461
+
462
+ /* ── header meta ── */
463
+ .result-header { display: flex; align-items: center; justify-content: space-between; }
464
+ .result-meta { display: flex; gap: 6px; flex-wrap: wrap; }
465
+ .meta-tag {
466
+ font-size: 11px;
467
+ font-weight: 600;
468
+ letter-spacing: 0.05em;
469
+ padding: 3px 10px;
470
+ border-radius: 99px;
471
+ border: 1px solid var(--border2);
472
+ background: var(--surf2);
473
+ color: var(--muted) !important;
474
+ }
475
+ .meta-tag.muted { opacity: 0.5; }
476
+
477
+ /* ── prediction hero ── */
478
+ .pred-hero {
479
+ padding: 28px 26px 24px;
480
+ border: 1px solid var(--border2);
481
+ border-radius: 16px;
482
+ background: linear-gradient(145deg, rgba(255,255,255,0.042) 0%, rgba(255,255,255,0.008) 100%);
483
+ text-align: center;
484
+ }
485
+ .pred-label {
486
+ font-size: 11px;
487
+ font-weight: 700;
488
+ letter-spacing: 0.12em;
489
+ text-transform: uppercase;
490
+ color: var(--muted) !important;
491
+ margin-bottom: 12px;
492
+ }
493
+ .pred-number {
494
+ font-size: 64px;
495
+ font-weight: 900;
496
+ line-height: 1;
497
+ letter-spacing: -0.04em;
498
+ font-variant-numeric: tabular-nums;
499
+ background: linear-gradient(135deg, #ffffff 0%, rgba(255,255,255,0.6) 100%);
500
+ -webkit-background-clip: text;
501
+ -webkit-text-fill-color: transparent;
502
+ background-clip: text;
503
+ margin-bottom: 10px;
504
+ }
505
+ .pred-raw {
506
+ font-size: 12px;
507
+ color: var(--muted2) !important;
508
+ font-family: ui-monospace, monospace;
509
+ letter-spacing: 0.02em;
510
+ }
511
+
512
+ /* ── comparison ── */
513
+ .compare-block {
514
+ border: 1px solid var(--border2);
515
+ border-radius: 16px;
516
+ background: var(--surf);
517
+ padding: 18px 20px 16px;
518
+ }
519
+ .compare-cards {
520
+ display: flex;
521
+ align-items: center;
522
+ gap: 12px;
523
+ margin-bottom: 16px;
524
+ }
525
+ .ccard {
526
+ flex: 1;
527
+ padding: 14px 16px;
528
+ border-radius: var(--r);
529
+ border: 1px solid var(--border);
530
+ background: var(--surf2);
531
+ text-align: center;
532
+ }
533
+ .ccard.predicted { border-color: rgba(255,255,255,0.1); }
534
+ .ccard.actual { border-color: rgba(255,255,255,0.06); }
535
+ .ccard-label {
536
+ font-size: 10px;
537
+ font-weight: 700;
538
+ letter-spacing: 0.1em;
539
+ text-transform: uppercase;
540
+ color: var(--muted) !important;
541
+ margin-bottom: 6px;
542
+ }
543
+ .ccard-num {
544
+ font-size: 28px;
545
+ font-weight: 800;
546
+ letter-spacing: -0.03em;
547
+ font-variant-numeric: tabular-nums;
548
+ }
549
+ .ccard-divider {
550
+ font-size: 12px;
551
+ font-weight: 600;
552
+ color: var(--muted2) !important;
553
+ letter-spacing: 0.08em;
554
+ flex-shrink: 0;
555
+ }
556
+ .accuracy-row { display: flex; flex-direction: column; gap: 6px; }
557
+ .accuracy-bar-bg {
558
+ height: 5px;
559
+ border-radius: 99px;
560
+ background: var(--border);
561
+ overflow: hidden;
562
+ }
563
+ .accuracy-bar-fill {
564
+ height: 100%;
565
+ border-radius: 99px;
566
+ transition: width 0.4s ease;
567
+ }
568
+ .accuracy-meta {
569
+ display: flex;
570
+ justify-content: space-between;
571
+ font-size: 12px;
572
+ }
573
+ .accuracy-pct { font-weight: 700; color: var(--text) !important; }
574
+ .diff-exact { color: var(--green) !important; font-weight: 600; }
575
+ .diff-over { color: var(--red) !important; }
576
+ .diff-under { color: var(--blue) !important; }
577
+
578
+ /* ── tokenizer error ── */
579
+ .tok-error {
580
+ padding: 12px 14px;
581
+ border-radius: var(--r);
582
+ border: 1px solid rgba(248,113,113,0.25);
583
+ background: rgba(248,113,113,0.05);
584
+ font-size: 12.5px;
585
+ color: var(--muted) !important;
586
+ display: flex;
587
+ gap: 10px;
588
+ align-items: flex-start;
589
+ }
590
+ .tok-err-icon { color: #f87171 !important; flex-shrink: 0; font-size: 14px; }
591
+
592
+ /* ── features ── */
593
+ .features-section {
594
+ border: 1px solid var(--border);
595
+ border-radius: 16px;
596
+ background: var(--surf);
597
+ padding: 16px 18px;
598
+ }
599
+ .features-title {
600
+ font-size: 11px;
601
+ font-weight: 700;
602
+ letter-spacing: 0.1em;
603
+ text-transform: uppercase;
604
+ color: var(--muted) !important;
605
+ margin-bottom: 12px;
606
+ }
607
+ .chips {
608
+ display: grid;
609
+ grid-template-columns: repeat(3, 1fr);
610
+ gap: 7px;
611
+ }
612
+ @media (max-width: 560px) { .chips { grid-template-columns: repeat(2, 1fr); } }
613
+ .chip {
614
+ display: flex;
615
+ justify-content: space-between;
616
+ align-items: center;
617
+ padding: 8px 11px;
618
+ border: 1px solid var(--border);
619
+ border-radius: var(--r-sm);
620
+ background: var(--surf2);
621
+ gap: 8px;
622
+ min-width: 0;
623
+ }
624
+ .chip-label {
625
+ font-size: 11px;
626
+ color: var(--muted) !important;
627
+ white-space: nowrap;
628
+ overflow: hidden;
629
+ text-overflow: ellipsis;
630
+ }
631
+ .chip-val {
632
+ font-size: 12px;
633
+ font-weight: 700;
634
+ font-family: ui-monospace, monospace;
635
+ flex-shrink: 0;
636
+ }
637
+
638
+ /* ── error banner ── */
639
+ .error-banner {
640
+ display: flex;
641
+ gap: 14px;
642
+ align-items: flex-start;
643
+ padding: 18px 20px;
644
+ border: 1px solid rgba(248,113,113,0.3);
645
+ border-radius: 16px;
646
+ background: rgba(248,113,113,0.06);
647
+ }
648
+ .err-icon { font-size: 18px; color: #f87171 !important; flex-shrink: 0; margin-top: 2px; }
649
+ .err-title { font-size: 14px; font-weight: 700; margin-bottom: 4px; }
650
+ .err-body { font-size: 13px; color: var(--muted) !important; line-height: 1.5; }
651
+
652
+ /* ── empty state ── */
653
+ .empty-state {
654
+ padding: 40px 24px;
655
+ text-align: center;
656
+ border: 1px dashed var(--border2);
657
+ border-radius: 16px;
658
+ }
659
+ .empty-icon { font-size: 28px; margin-bottom: 12px; opacity: 0.3; }
660
+ .empty-title { font-size: 15px; font-weight: 700; margin-bottom: 6px; }
661
+ .empty-desc { font-size: 13px; color: var(--muted) !important; line-height: 1.6; }
662
  """
663
 
664
  EMPTY_HTML = """
665
+ <div class="empty-state">
666
+ <div class="empty-icon">⬑</div>
667
+ <div class="empty-title">Ready to predict</div>
668
+ <div class="empty-desc">
669
+ Paste text above and press <b>Predict</b>.<br>
670
+ Add a tokenizer repo ID to compare against ground-truth token count.
671
  </div>
672
  </div>
673
  """
674
 
675
+ # ── UI layout ──────────────────────────────────────────────────────────────────
676
 
677
  with gr.Blocks(title="JetonCount") as demo:
678
 
679
+ gr.HTML(f"""
680
  <div class="hero">
681
+ <div class="hero-eyebrow">Token Count Estimator</div>
682
  <div class="hero-title">JetonCount</div>
683
+ <div class="hero-desc">
684
+ Predict how many tokens a text will produce β€” without running a full tokenizer.
685
+ Optionally compare against any Hugging Face tokenizer for accuracy metrics.
686
+ </div>
687
+ <div class="hero-badges">
688
+ <span class="badge">MLP regressor</span>
689
+ <span class="badge">fromziro/JetonCount</span>
690
+ <span class="badge">{DEVICE.type.upper()}</span>
691
  </div>
692
  </div>
693
  """)
 
695
  locked_vocab = gr.State(None)
696
 
697
  with gr.Row(equal_height=False):
698
+
699
+ # ── left ────────────────────────────────────────────────────────────
700
+ with gr.Column(scale=5, min_width=360):
701
  text_in = gr.Textbox(
702
  label="Text",
703
+ lines=14,
704
  placeholder="Paste your text here…",
705
+ container=True,
706
  )
707
+ counter_html = gr.HTML(value="<div class='live-counter'><span>0 chars</span><span class='sep'>Β·</span><span>0 words</span></div>")
708
+ predict_btn = gr.Button("⬑ Predict tokens", variant="primary", size="lg")
709
 
710
+ # ── right ───────────────────────────────────────────────────────────
711
+ with gr.Column(scale=3, min_width=260):
712
  tokenizer_in = gr.Textbox(
713
+ label="Tokenizer repo (optional)",
714
  placeholder="e.g. openai-community/gpt2",
715
  )
716
  vocab_in = gr.Number(
 
719
  precision=0,
720
  interactive=True,
721
  )
722
+ status_html = gr.HTML(value="")
 
723
  clear_btn = gr.Button("Clear tokenizer", variant="secondary", size="sm")
724
 
 
725
  results_html = gr.HTML(value=EMPTY_HTML)
726
 
727
  with gr.Accordion("Raw JSON", open=False):
 
729
 
730
  # ── wiring ────────────────────────────────────────────────────────────────
731
 
732
+ text_in.change(fn=on_text_change, inputs=[text_in], outputs=[counter_html])
 
733
 
734
  tokenizer_in.blur(
735
+ fn=on_tokenizer_change, inputs=[tokenizer_in],
736
+ outputs=[vocab_in, status_html, locked_vocab],
 
737
  )
738
  tokenizer_in.submit(
739
+ fn=on_tokenizer_change, inputs=[tokenizer_in],
740
+ outputs=[vocab_in, status_html, locked_vocab],
 
741
  )
 
742
  clear_btn.click(
743
+ fn=on_clear, inputs=[vocab_in],
744
+ outputs=[tokenizer_in, vocab_in, locked_vocab, status_html],
 
745
  )
 
746
  predict_btn.click(
747
+ fn=run, inputs=[text_in, vocab_in, tokenizer_in, locked_vocab],
 
748
  outputs=[results_html, raw_json],
749
  )
750
  text_in.submit(
751
+ fn=run, inputs=[text_in, vocab_in, tokenizer_in, locked_vocab],
 
752
  outputs=[results_html, raw_json],
753
  )
754