jcfossati commited on
Commit
10848b9
·
verified ·
1 Parent(s): 7c8d35b

sync from GitHub repo (space/)

Browse files
Files changed (4) hide show
  1. MODEL_CARD.md +127 -0
  2. app.py +97 -38
  3. config_fr-en.json +10 -0
  4. tokenizer_fr-en.json +0 -0
MODEL_CARD.md ADDED
@@ -0,0 +1,127 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ ---
2
+ license: apache-2.0
3
+ language:
4
+ - fr
5
+ - en
6
+ library_name: onnx
7
+ pipeline_tag: text-classification
8
+ base_model: sentence-transformers/paraphrase-multilingual-MiniLM-L12-v2
9
+ tags:
10
+ - yes-no
11
+ - intent-classification
12
+ - distillation
13
+ - onnx
14
+ - edge
15
+ - quantization
16
+ - sentence-transformers
17
+ metrics:
18
+ - accuracy
19
+ model-index:
20
+ - name: ForSureLLM
21
+ results:
22
+ - task:
23
+ type: text-classification
24
+ name: Yes/No/Unknown classification
25
+ metrics:
26
+ - type: accuracy
27
+ value: 0.952
28
+ name: Adversarial accuracy (124 cases)
29
+ - type: accuracy
30
+ value: 0.917
31
+ name: Test accuracy (1178 cases)
32
+ ---
33
+
34
+ # ForSureLLM
35
+
36
+ Ultra-rapid `yes` / `no` / `unknown` classifier for short user replies, distilled from Claude Sonnet 4.6 into a multilingual MiniLM-L12. **2 ms on CPU**, **24-113 MB**, no API call needed.
37
+
38
+ - 🎯 **Try it live**: [HuggingFace Space demo](https://huggingface.co/spaces/jcfossati/ForSureLLM)
39
+ - 📦 **Source code**: [github.com/jcfossati/ForSureLLM](https://github.com/jcfossati/ForSureLLM)
40
+
41
+ ## What it does
42
+
43
+ Given a short French or English reply (1-30 words typically), returns whether the user is **agreeing**, **refusing**, or **hesitating** about a pending action. Designed as a consent-intent oracle for chatbots, IVR systems, CLI confirmations, and automation flows.
44
+
45
+ ```python
46
+ from forsurellm import classify
47
+
48
+ classify("carrément") # ("yes", 0.97)
49
+ classify("laisse tomber") # ("no", 0.98)
50
+ classify("je sais pas trop") # ("unknown", 0.96)
51
+ classify("oui mais non") # ("unknown", 0.92)
52
+ classify("yeah right") # ("no", 0.87) # sarcasm detected
53
+ classify("+1") # ("yes", 1.00) # symbolic preprocessor
54
+ classify("👍") # ("yes", 1.00)
55
+ ```
56
+
57
+ ## Numbers
58
+
59
+ | Metric | Value |
60
+ |---|---|
61
+ | Adversarial accuracy (124 trap phrases, 22 categories) | **95.2 %** |
62
+ | Surface-variant robustness (1227 variants) | 95.8 % |
63
+ | Test set accuracy (1178 phrases) | 91.7 % |
64
+ | Calibration ECE | 0.012 |
65
+ | **CPU latency p50** | **1.8 ms** |
66
+ | ONNX int8 size | 113 MB (multilingual) · **24 MB (FR+EN pruned variant)** |
67
+
68
+ **Head-to-head on the 124-case adversarial bench**:
69
+
70
+ | Classifier | Accuracy | p50 latency | API cost |
71
+ |---|---|---|---|
72
+ | **ForSureLLM** | **95.2 %** | **1.8 ms** | 0 |
73
+ | Haiku 4.5 zero-shot | 75.0 % | 602 ms | $$ |
74
+ | Cosine MiniLM-L12 (no fine-tune) | 67.7 % | 8 ms | 0 |
75
+
76
+ ForSureLLM beats Haiku 4.5 zero-shot by **+20.2 pts** while running **~330× faster**.
77
+
78
+ ## Strengths
79
+
80
+ Categories where ForSureLLM crushes a generalist LLM (Haiku 4.5):
81
+
82
+ - `modern_slang` (Gen-Z): `no cap`, `bet`, `say less`, `deadass` — 100 % vs 43 %
83
+ - `negated_verb`: `I wouldn't say no`, `ce n'est pas un non` — 83 % vs 17 %
84
+ - `sarcasm`: `oui bien sûr...`, `yeah right` — 100 % vs 40 %
85
+ - `symbolic`: `+1`, `100%`, `👍`, `10/10` — 100 % vs 40 % (deterministic preprocessor)
86
+ - `slang_abbrev`: `np`, `tkt`, `kk`, `nope` — 100 % vs 50 %
87
+
88
+ ## Files in this repo
89
+
90
+ - `forsurellm-int8.onnx` — full multilingual model, 113 MB (50+ languages supported via shared subwords, FR+EN tuned)
91
+ - (Optional) `forsurellm-int8_fr-en.onnx` — vocab-pruned FR+EN variant, 24 MB. Same predictions as the full model on FR+EN inputs, 5× lighter on disk and in RAM (+85 MB process memory vs +418 MB), latency unchanged. Tokens outside FR+EN become `<unk>`.
92
+
93
+ ## How to use (without the package)
94
+
95
+ ```python
96
+ import onnxruntime as ort
97
+ from huggingface_hub import hf_hub_download
98
+ from tokenizers import Tokenizer
99
+ import numpy as np
100
+
101
+ onnx_path = hf_hub_download("jcfossati/ForSureLLM", "forsurellm-int8.onnx")
102
+ session = ort.InferenceSession(onnx_path, providers=["CPUExecutionProvider"])
103
+ # tokenizer.json must be downloaded from the GitHub repo (space/tokenizer.json)
104
+ # or installed via the forsurellm package once published.
105
+ ```
106
+
107
+ For the full preprocessing pipeline (case normalisation, symbolic shortcuts, sarcasm-aware threshold), use the `forsurellm` Python package — see the [GitHub repo](https://github.com/jcfossati/ForSureLLM) for installation.
108
+
109
+ ## Training procedure
110
+
111
+ - **Backbone**: `sentence-transformers/paraphrase-multilingual-MiniLM-L12-v2` (12 layers, 384 hidden)
112
+ - **Teacher**: Claude Sonnet 4.6 (generation) + Claude Haiku 4.5 (labeling, with Sonnet fallback when confidence < 0.6)
113
+ - **Loss**: KL-divergence on soft labels (3 classes)
114
+ - **Dataset**: ~5,800 hand-curated + LLM-generated EN+FR phrases, balanced across 22 adversarial categories
115
+ - **Training**: 8 epochs, batch 32, lr 2e-5, warmup 10%, weight decay 0.01 (~2 min on RTX Blackwell)
116
+ - **Calibration**: temperature scaling (T = 0.680, fitted by LBFGS on val set NLL)
117
+ - **Export**: ONNX dynamic quantization (avx512-vnni, int8)
118
+
119
+ ## Limitations
120
+
121
+ - **EN + FR only**. The full model (113 MB) keeps the multilingual vocab and may produce reasonable cross-lingual outputs on related Latin-script languages (Spanish/Italian/German), but is not trained for them. The pruned variant (24 MB) drops non-FR/EN tokens entirely.
122
+ - **Short replies**. Optimized for 1-30 word answers. Long passages will be truncated at 64 tokens.
123
+ - **Sarcasm detection has cultural priors**. `yeah right` defaults to "no" because it's overwhelmingly sarcastic in modern English usage — a sincere user without punctuation might get the wrong call. Use `threshold=0.85` for action-confirmation contexts to fall back to `unknown` on borderline cases.
124
+
125
+ ## License
126
+
127
+ Apache 2.0 — same as the base MiniLM model.
app.py CHANGED
@@ -1,8 +1,10 @@
1
  """Gradio demo for ForSureLLM hosted on HuggingFace Spaces.
2
 
3
- Loads the ONNX model from the Model repo (jcfossati/ForSureLLM) at startup,
4
- exposes a yes/no/unknown classifier UI styled to match the static web/index.html
5
- demo (dark minimalist, color-coded bars, token list).
 
 
6
  """
7
  from __future__ import annotations
8
 
@@ -11,6 +13,7 @@ import json
11
  import re
12
  import time
13
  import unicodedata
 
14
  from pathlib import Path
15
 
16
  import gradio as gr
@@ -20,22 +23,62 @@ from huggingface_hub import hf_hub_download
20
  from tokenizers import Tokenizer
21
 
22
  MODEL_REPO = "jcfossati/ForSureLLM"
23
- ONNX_FILE = "forsurellm-int8.onnx"
24
 
25
- # --- Load artefacts ---------------------------------------------------------
26
  ROOT = Path(__file__).parent
27
- TOKENIZER = Tokenizer.from_file(str(ROOT / "tokenizer.json"))
28
- with (ROOT / "config.json").open(encoding="utf-8") as f:
29
- CFG = json.load(f)
30
- TOKENIZER.enable_truncation(max_length=CFG["max_length"])
31
- CLASSES = CFG["classes"]
32
- TEMPERATURE = float(CFG.get("temperature", 1.0))
33
-
34
- print(f"[boot] downloading {ONNX_FILE} from {MODEL_REPO}...")
35
- ONNX_PATH = hf_hub_download(MODEL_REPO, ONNX_FILE)
36
- SESSION = ort.InferenceSession(ONNX_PATH, providers=["CPUExecutionProvider"])
37
- INPUT_NAMES = {i.name for i in SESSION.get_inputs()}
38
- print(f"[boot] ready (model {Path(ONNX_PATH).stat().st_size / 1024 / 1024:.0f} MB)")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
39
 
40
  # --- Preprocessing (mirror forsurellm/classifier.py) ------------------------
41
  _HAS_LETTER_RE = re.compile(r"[^\W\d_]", re.UNICODE)
@@ -96,40 +139,42 @@ def _softmax(x: np.ndarray) -> np.ndarray:
96
  return e / e.sum(axis=-1, keepdims=True)
97
 
98
 
99
- def classify_full(phrase: str, threshold: float = 0.0) -> dict:
100
- """Returns {label, confidence, probs: {yes,no,unknown}, tokens, source}."""
 
101
  sym = _classify_symbolic(phrase or "")
102
  if sym is not None:
103
  label, conf = sym
104
- probs = {c: 0.0 for c in CLASSES}
105
  probs[label] = conf
106
- return {"label": label, "confidence": conf, "probs": probs, "tokens": [phrase.strip()], "source": "symbolic"}
 
107
  if not _HAS_LETTER_RE.search(phrase or ""):
108
  return {"label": "unknown", "confidence": 1.0,
109
  "probs": {"yes": 0.0, "no": 0.0, "unknown": 1.0},
110
- "tokens": [], "source": "no-letter shortcut"}
111
 
112
- enc = TOKENIZER.encode(_normalize(phrase))
113
  feeds = {"input_ids": np.array([enc.ids], dtype=np.int64),
114
  "attention_mask": np.array([enc.attention_mask], dtype=np.int64)}
115
- if "token_type_ids" in INPUT_NAMES:
116
  feeds["token_type_ids"] = np.array([enc.type_ids], dtype=np.int64)
117
- feeds = {k: v for k, v in feeds.items() if k in INPUT_NAMES}
118
- logits = SESSION.run(None, feeds)[0][0]
119
- probs_arr = _softmax(logits / TEMPERATURE)
120
- probs = {c: float(p) for c, p in zip(CLASSES, probs_arr)}
121
  idx = int(probs_arr.argmax())
122
- label = CLASSES[idx]
123
  conf = probs[label]
124
  if conf < threshold and label != "unknown":
125
  label = "unknown"
126
  conf = probs["unknown"]
127
  return {"label": label, "confidence": conf, "probs": probs,
128
  "tokens": [t for t in enc.tokens if t not in {"<s>", "</s>", "[CLS]", "[SEP]", "[PAD]"}],
129
- "source": "model"}
130
 
131
 
132
- # --- Rendering ---------------------------------------------------------------
133
  EMPTY_HTML = '<div class="fsl-result fsl-result-empty">Tape une phrase pour la classifier.</div>'
134
 
135
  RESULT_TEMPLATE = """
@@ -143,7 +188,7 @@ RESULT_TEMPLATE = """
143
  <div class="fsl-tokens-label">{token_label}</div>
144
  <div class="fsl-tokens-list">{tokens_html}</div>
145
  </div>
146
- <div class="fsl-meta">{source} · {elapsed_ms:.1f} ms</div>
147
  </div>
148
  """
149
 
@@ -156,11 +201,11 @@ BAR_TEMPLATE = """
156
  """
157
 
158
 
159
- def render_result(phrase: str, threshold: float) -> str:
160
  if not phrase or not phrase.strip():
161
  return EMPTY_HTML
162
  t0 = time.perf_counter()
163
- res = classify_full(phrase, threshold)
164
  elapsed_ms = (time.perf_counter() - t0) * 1000
165
  bars = "".join(
166
  BAR_TEMPLATE.format(name=cls, pct=res["probs"][cls] * 100, value=res["probs"][cls])
@@ -181,6 +226,7 @@ def render_result(phrase: str, threshold: float) -> str:
181
  tokens_html=tokens_html,
182
  token_label=token_label,
183
  source=res["source"],
 
184
  elapsed_ms=elapsed_ms,
185
  )
186
 
@@ -231,6 +277,9 @@ CSS = """
231
  .fsl-threshold .wrap { background: transparent !important; }
232
  .fsl-threshold input[type="range"] { accent-color: var(--fsl-accent) !important; }
233
 
 
 
 
234
  .fsl-result {
235
  background: var(--fsl-surface); border: 1px solid var(--fsl-border); border-radius: 12px;
236
  padding: 20px; min-height: 120px; color: var(--fsl-text);
@@ -300,8 +349,17 @@ with gr.Blocks(title="ForSureLLM", css=CSS, theme=gr.themes.Base()) as demo:
300
  autofocus=True,
301
  show_label=False,
302
  )
303
- thr = gr.Slider(0, 1, value=0, step=0.01, label="threshold (force unknown si conf < seuil)",
304
- elem_classes="fsl-threshold")
 
 
 
 
 
 
 
 
 
305
 
306
  gr.HTML('<div id="fsl-presets-label">exemples</div>')
307
  with gr.Row(elem_classes="fsl-presets-row"):
@@ -309,8 +367,9 @@ with gr.Blocks(title="ForSureLLM", css=CSS, theme=gr.themes.Base()) as demo:
309
 
310
  out = gr.HTML(value=EMPTY_HTML)
311
 
312
- inp.change(render_result, inputs=[inp, thr], outputs=[out], show_progress="hidden")
313
- thr.change(render_result, inputs=[inp, thr], outputs=[out], show_progress="hidden")
 
314
  for btn, p in zip(preset_btns, PRESETS):
315
  btn.click(lambda v=p: v, outputs=[inp])
316
 
 
1
  """Gradio demo for ForSureLLM hosted on HuggingFace Spaces.
2
 
3
+ Loads BOTH variants (full multilingual + pruned FR+EN) at startup and exposes
4
+ a dropdown so visitors can compare predictions / sizes / speed in real time.
5
+
6
+ ONNX files are downloaded from the jcfossati/ForSureLLM Model repo at boot.
7
+ Tokenizer + config files are bundled in this Space (small).
8
  """
9
  from __future__ import annotations
10
 
 
13
  import re
14
  import time
15
  import unicodedata
16
+ from dataclasses import dataclass
17
  from pathlib import Path
18
 
19
  import gradio as gr
 
23
  from tokenizers import Tokenizer
24
 
25
  MODEL_REPO = "jcfossati/ForSureLLM"
 
26
 
27
+ # --- Variant registry ------------------------------------------------------
28
  ROOT = Path(__file__).parent
29
+
30
+
31
+ @dataclass
32
+ class Variant:
33
+ label: str # display name in the dropdown
34
+ onnx_remote: str # filename in the Model repo to download
35
+ tokenizer_local: str # bundled tokenizer file in space/
36
+ config_local: str # bundled config file in space/
37
+ tokenizer: Tokenizer = None
38
+ session: ort.InferenceSession = None
39
+ classes: list = None
40
+ input_names: set = None
41
+ temperature: float = 1.0
42
+ size_mb: float = 0.0
43
+ vocab_size: int = 0
44
+
45
+
46
+ VARIANTS: dict[str, Variant] = {
47
+ "Full multilingual (113 MB)": Variant(
48
+ label="Full multilingual (113 MB)",
49
+ onnx_remote="forsurellm-int8.onnx",
50
+ tokenizer_local="tokenizer.json",
51
+ config_local="config.json",
52
+ ),
53
+ "Pruned FR+EN (24 MB)": Variant(
54
+ label="Pruned FR+EN (24 MB)",
55
+ onnx_remote="forsurellm-int8_fr-en.onnx",
56
+ tokenizer_local="tokenizer_fr-en.json",
57
+ config_local="config_fr-en.json",
58
+ ),
59
+ }
60
+ DEFAULT_VARIANT = "Pruned FR+EN (24 MB)"
61
+
62
+
63
+ def _load_variant(v: Variant) -> None:
64
+ print(f"[boot] loading {v.label}: downloading {v.onnx_remote}...")
65
+ onnx_path = hf_hub_download(MODEL_REPO, v.onnx_remote)
66
+ v.size_mb = round(Path(onnx_path).stat().st_size / (1024 * 1024), 1)
67
+ v.tokenizer = Tokenizer.from_file(str(ROOT / v.tokenizer_local))
68
+ with (ROOT / v.config_local).open(encoding="utf-8") as f:
69
+ cfg = json.load(f)
70
+ v.tokenizer.enable_truncation(max_length=cfg["max_length"])
71
+ v.classes = cfg["classes"]
72
+ v.temperature = float(cfg.get("temperature", 1.0))
73
+ v.session = ort.InferenceSession(onnx_path, providers=["CPUExecutionProvider"])
74
+ v.input_names = {i.name for i in v.session.get_inputs()}
75
+ v.vocab_size = v.tokenizer.get_vocab_size()
76
+ print(f"[boot] -> {v.label} ready ({v.size_mb} MB ONNX, {v.vocab_size} tokens)")
77
+
78
+
79
+ for v in VARIANTS.values():
80
+ _load_variant(v)
81
+
82
 
83
  # --- Preprocessing (mirror forsurellm/classifier.py) ------------------------
84
  _HAS_LETTER_RE = re.compile(r"[^\W\d_]", re.UNICODE)
 
139
  return e / e.sum(axis=-1, keepdims=True)
140
 
141
 
142
+ def classify_full(phrase: str, variant_label: str, threshold: float = 0.0) -> dict:
143
+ """Returns {label, confidence, probs, tokens, source, variant}."""
144
+ v = VARIANTS[variant_label]
145
  sym = _classify_symbolic(phrase or "")
146
  if sym is not None:
147
  label, conf = sym
148
+ probs = {c: 0.0 for c in v.classes}
149
  probs[label] = conf
150
+ return {"label": label, "confidence": conf, "probs": probs,
151
+ "tokens": [phrase.strip()], "source": "symbolic", "variant": variant_label}
152
  if not _HAS_LETTER_RE.search(phrase or ""):
153
  return {"label": "unknown", "confidence": 1.0,
154
  "probs": {"yes": 0.0, "no": 0.0, "unknown": 1.0},
155
+ "tokens": [], "source": "no-letter shortcut", "variant": variant_label}
156
 
157
+ enc = v.tokenizer.encode(_normalize(phrase))
158
  feeds = {"input_ids": np.array([enc.ids], dtype=np.int64),
159
  "attention_mask": np.array([enc.attention_mask], dtype=np.int64)}
160
+ if "token_type_ids" in v.input_names:
161
  feeds["token_type_ids"] = np.array([enc.type_ids], dtype=np.int64)
162
+ feeds = {k: x for k, x in feeds.items() if k in v.input_names}
163
+ logits = v.session.run(None, feeds)[0][0]
164
+ probs_arr = _softmax(logits / v.temperature)
165
+ probs = {c: float(p) for c, p in zip(v.classes, probs_arr)}
166
  idx = int(probs_arr.argmax())
167
+ label = v.classes[idx]
168
  conf = probs[label]
169
  if conf < threshold and label != "unknown":
170
  label = "unknown"
171
  conf = probs["unknown"]
172
  return {"label": label, "confidence": conf, "probs": probs,
173
  "tokens": [t for t in enc.tokens if t not in {"<s>", "</s>", "[CLS]", "[SEP]", "[PAD]"}],
174
+ "source": "model", "variant": variant_label}
175
 
176
 
177
+ # --- Rendering --------------------------------------------------------------
178
  EMPTY_HTML = '<div class="fsl-result fsl-result-empty">Tape une phrase pour la classifier.</div>'
179
 
180
  RESULT_TEMPLATE = """
 
188
  <div class="fsl-tokens-label">{token_label}</div>
189
  <div class="fsl-tokens-list">{tokens_html}</div>
190
  </div>
191
+ <div class="fsl-meta">{source} · {variant} · {elapsed_ms:.1f} ms</div>
192
  </div>
193
  """
194
 
 
201
  """
202
 
203
 
204
+ def render_result(phrase: str, variant_label: str, threshold: float) -> str:
205
  if not phrase or not phrase.strip():
206
  return EMPTY_HTML
207
  t0 = time.perf_counter()
208
+ res = classify_full(phrase, variant_label, threshold)
209
  elapsed_ms = (time.perf_counter() - t0) * 1000
210
  bars = "".join(
211
  BAR_TEMPLATE.format(name=cls, pct=res["probs"][cls] * 100, value=res["probs"][cls])
 
226
  tokens_html=tokens_html,
227
  token_label=token_label,
228
  source=res["source"],
229
+ variant=res["variant"].split(" (")[0], # short label
230
  elapsed_ms=elapsed_ms,
231
  )
232
 
 
277
  .fsl-threshold .wrap { background: transparent !important; }
278
  .fsl-threshold input[type="range"] { accent-color: var(--fsl-accent) !important; }
279
 
280
+ .fsl-variant select, .fsl-variant .wrap-inner { background: var(--fsl-surface-2) !important; color: var(--fsl-text) !important; border-color: var(--fsl-border) !important; }
281
+ .fsl-variant label span { color: var(--fsl-muted) !important; font-size: 13px !important; }
282
+
283
  .fsl-result {
284
  background: var(--fsl-surface); border: 1px solid var(--fsl-border); border-radius: 12px;
285
  padding: 20px; min-height: 120px; color: var(--fsl-text);
 
349
  autofocus=True,
350
  show_label=False,
351
  )
352
+ with gr.Row():
353
+ variant_dd = gr.Dropdown(
354
+ choices=list(VARIANTS.keys()),
355
+ value=DEFAULT_VARIANT,
356
+ label="model variant",
357
+ elem_classes="fsl-variant",
358
+ scale=2,
359
+ )
360
+ thr = gr.Slider(0, 1, value=0, step=0.01,
361
+ label="threshold (force unknown si conf < seuil)",
362
+ elem_classes="fsl-threshold", scale=3)
363
 
364
  gr.HTML('<div id="fsl-presets-label">exemples</div>')
365
  with gr.Row(elem_classes="fsl-presets-row"):
 
367
 
368
  out = gr.HTML(value=EMPTY_HTML)
369
 
370
+ inp.change(render_result, inputs=[inp, variant_dd, thr], outputs=[out], show_progress="hidden")
371
+ thr.change(render_result, inputs=[inp, variant_dd, thr], outputs=[out], show_progress="hidden")
372
+ variant_dd.change(render_result, inputs=[inp, variant_dd, thr], outputs=[out], show_progress="hidden")
373
  for btn, p in zip(preset_btns, PRESETS):
374
  btn.click(lambda v=p: v, outputs=[inp])
375
 
config_fr-en.json ADDED
@@ -0,0 +1,10 @@
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "classes": [
3
+ "yes",
4
+ "no",
5
+ "unknown"
6
+ ],
7
+ "max_length": 64,
8
+ "temperature": 0.6799102425575256,
9
+ "tokenizer_file": "tokenizer_fr-en.json"
10
+ }
tokenizer_fr-en.json ADDED
The diff for this file is too large to render. See raw diff