File size: 23,063 Bytes
cba4874
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
0e69d49
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
cba4874
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
{
 "cells": [
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "# App Creation β€” PDF Injection Detector (MiMo-7B)\n",
    "\n",
    "The build log for the Hugging Face Space at\n",
    "[`BentoUniAcc/Mimo_Injection_detector`](https://huggingface.co/spaces/BentoUniAcc/Mimo_Injection_detector):\n",
    "what it does, which decisions were forced rather than chosen, what broke, and the checks that say\n",
    "it is still faithful to the notebooks it quotes.\n",
    "\n",
    "The Space is four Python files and this notebook is not one of them β€” it does not define the app,\n",
    "it records how the app came to be what it is. Every cell below runs against the *deployed* files,\n",
    "so if one fails, the Space is wrong and not the notebook.\n"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## 1 β€” What it is\n",
    "\n",
    "Upload a PDF. It is rendered to text with the extractor that built the project corpus, the regions\n",
    "carrying structural markers are ranked, the whole document is cut into **batches that fit one run\n",
    "of the model**, and **MiMo-7B-RL** reads the batch you choose β€” reporting whether a payload is\n",
    "hidden there and quoting the substring that convinced it.\n",
    "\n",
    "| File | Role |\n",
    "|---|---|\n",
    "| `app.py` | Gradio UI, batching, report aggregation. No detection logic. |\n",
    "| `corpus_text.py` | PDF bytes to skeleton to candidate regions. |\n",
    "| `mimo.py` | Prompt, prefill, parser, and both model runtimes. |\n",
    "| `neighbours.py` | Part A embedding index and nearest-neighbour lookup. |\n",
    "| `test_fidelity.py` | Proves the extraction reproduces the published corpus exactly. |\n"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## 2 β€” The provenance rule\n",
    "\n",
    "The published numbers only describe this app if the app reads a PDF and asks the question exactly\n",
    "as the notebooks did. So the code was lifted from the **dataset repos themselves**, not rewritten\n",
    "and not copied from the earlier archived app:\n",
    "\n",
    "| Borrowed | From |\n",
    "|---|---|\n",
    "| `build_skeleton`, `mask_leaks`, `detect_markers`, `payload_window`, `ANY_MARKER_RE`, `INJECTION_MARKERS`, `LEAK_STRINGS` | EDA notebook, cells 88 / 89 / 107 |\n",
    "| `SYSTEM`, `PREFILL`, `build_messages`, `scan_objects`, `parse_response`, `MAX_NEW`, `BATCH` | Part B notebook, cell 48 |\n",
    "| embedder repo, prefix, dims, normalisation, input column | `Part_A_Outputs/part_a_results.json` |\n",
    "\n",
    "One decision worth stating: **the parser is the narrow original**, not the widened version that\n",
    "appears in the archived app. A wider salvage recovers more verdicts and would also mean the F1\n",
    "quoted in the interface describes a parser that is not the one running.\n"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### 2.1 β€” The prompt and the prefill, as deployed\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "import mimo\n",
    "\n",
    "print(f'system message : {len(mimo.SYSTEM)} chars')\n",
    "print(f'families       : {len(mimo.FAMILIES)} (closed set, fixed order)')\n",
    "print(f'max new tokens : {mimo.MAX_NEW}')\n",
    "print(f'batch          : {mimo.BATCH}')\n",
    "print()\n",
    "print('prefill:', repr(mimo.PREFILL))\n"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "MiMo is the only model in Part B that carries a prefill. It is reasoning-trained and opens every\n",
    "answer with `<think>`; at a 200-token budget it never closed the block, so not one of its 1,100\n",
    "answers reached the JSON. An empty, already-closed think-block says the deliberation is finished\n",
    "before it begins, and the opening brace puts the model inside the answer.\n"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### 2.2 β€” The parser survives the bug that mattered\n",
    "\n",
    "Part B's first parser used a regex to find the JSON object. Every injected file in this corpus\n",
    "carries an EICAR-style marker containing a closing brace, so the moment a model quoted its\n",
    "evidence the match truncated and the verdict was thrown away β€” **the bug fired exactly when the\n",
    "model was right**. The replacement counts braces and tracks string literals.\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "import json\n",
    "\n",
    "evidence = r'X5O!P%@AP[4\\PZX54(P^)7CC)7}$EICAR-STANDARD-ANTIVIRUS-TEST-FILE'\n",
    "body = json.dumps({'injected': True, 'injection_type': 'javascript_injection',\n",
    "                   'evidence': evidence, 'reasoning': 'marker present'})\n",
    "\n",
    "# what MiMo actually emits: the prefill, then the remainder of that object\n",
    "raw = mimo.PREFILL + body[body.index(':') + 1:]\n",
    "r = mimo.parse_response(raw)\n",
    "\n",
    "print('route                    :', r['parsed_by'])\n",
    "print('verdict / family         :', r['pred_injected'], r['pred_family'])\n",
    "print('brace inside evidence ok :', '}' in r['evidence'])\n",
    "\n",
    "# reasoning-trained models restate the schema while thinking: the LAST object must win\n",
    "two = '{\"injected\": false, \"injection_type\": \"none\"} ... then really ' + raw\n",
    "print('last-object-wins         :', mimo.parse_response(two)['pred_injected'] == 1)\n"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## 3 β€” Fidelity: does this read a PDF the way the corpus was read?\n",
    "\n",
    "This is the check the whole design rests on. It pulls real PDFs out of the generation repo, runs\n",
    "them through the deployed `corpus_text.py`, and compares against the published parquet **character\n",
    "by character** β€” not 'close enough', identical. Injected and clean files both, because masking and\n",
    "binary-stream handling differ between them and a test that saw only one would pass on a broken\n",
    "extractor.\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "!python test_fidelity.py 8\n"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## 4 β€” The embedding lookup, and the silent failure it avoids\n",
    "\n",
    "A query embedded with the wrong model, or without Part A's `search_document: ` prefix, lands in a\n",
    "different vector space and returns meaningless neighbours β€” silently, with no error anywhere. So\n",
    "`check_provenance()` asserts the embedder repo, prefix, dimension, normalisation and input column\n",
    "against Part A's own results file before any lookup runs.\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "import neighbours\n",
    "\n",
    "print('Part A winner, asserted against part_a_results.json:')\n",
    "for k, v in neighbours.check_provenance().items():\n",
    "    print(f'  {k:15s}: {v}')\n",
    "\n",
    "idx = neighbours.load_index()\n",
    "print()\n",
    "print('index:', idx['matrix'].shape, idx['matrix'].dtype)\n"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### 4.1 β€” Proof the query lands in the index's own space\n",
    "\n",
    "Embed a corpus row's `payload_window` and dot it against that same row's stored vector. Anything\n",
    "below 1.0 means the app and the index disagree about what an embedding is.\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "import pandas as pd\n",
    "\n",
    "CORPUS = ('https://huggingface.co/datasets/Cyber-security-final-project/'\n",
    "          'HARMLESS_Synthetic_Injected_PDFs_EDA/resolve/main/Datasets/'\n",
    "          'synthetic_corpus_part2_clustered.parquet')\n",
    "\n",
    "corpus = pd.read_parquet(CORPUS)\n",
    "pos = {str(f): i for i, f in enumerate(idx['ids'])}\n",
    "\n",
    "sample = corpus[['file_id', 'payload_window', 'injection_type']].sample(5, random_state=0)\n",
    "for fid, window, family in sample.itertuples(index=False):\n",
    "    self_sim = float(neighbours.embed(window) @ idx['matrix'][pos[fid]])\n",
    "    print(f'{family[:24]:24s} self-cosine = {self_sim:.4f}')\n"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Measured: **1.0000 on every file**. The query vectors are literally the index vectors.\n",
    "\n",
    "What the lookup is worth is a separate question, and a smaller number: Part A's winning embedder\n",
    "reaches **precision@5 of 35.6%** against a 6.8% random baseline. Fewer than 2 of the 5 files\n",
    "returned are the same kind of attack. Far better than chance, and not good β€” which is why the\n",
    "interface says *nearest files in the corpus* and never *the same attack*.\n"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## 5 β€” Fitting the free tier: what was forced, not chosen\n",
    "\n",
    "### 5.1 β€” MiMo cannot be called remotely\n",
    "\n",
    "The first plan was to run the app on a free CPU Space and call MiMo through an inference provider.\n",
    "That is not available at any price short of a dedicated endpoint:\n"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### 5.0 β€” Why MiMo, when Gemma scored higher\n",
    "\n",
    "Part B's actual winner is **Gemma-2-9B at F1 0.969**; this app runs **MiMo-7B at F1 0.945**. That\n",
    "is a hosting decision, not a disagreement with the evaluation. Three constraints a free Space\n",
    "cannot absorb:\n",
    "\n",
    "- **Gemma is gated.** It needs an account with Google's licence accepted plus a read token, so on\n",
    "  a public Space the first thing a new visitor meets is a 403 β€” or the app has to ask strangers to\n",
    "  paste a token. MiMo downloads for anyone, with no account at all.\n",
    "- **Gemma is 2.6x slower** β€” 10.95 s per window against MiMo's 4.18 s, measured in Part B on the\n",
    "  same T4. A ZeroGPU grant is capped at 300 s and the scan *plus* the model load must fit inside\n",
    "  it, so the slower model means roughly a third as many regions per run.\n",
    "- **Free ZeroGPU is ~5 minutes per day.** At Gemma's rate that is a couple of batches for a whole\n",
    "  day. Gemma is also ~6 GB in 4-bit against MiMo's ~5 GB β€” not decisive alone, but it points the\n",
    "  same way.\n",
    "\n",
    "The price is **0.024 F1**, and family-naming falling from 63% to 43%. Both are stated in the\n",
    "interface rather than rounded away. Moving to dedicated paid hardware would make Gemma a change to\n",
    "two repo constants in `mimo.py` plus a token secret β€” the prompt, prefill and parser are shared\n",
    "and would not need touching.\n",
    "\n",
    "The cell in section 7 prints all four models' Part B scores, so the trade is visible as numbers.\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "import requests\n",
    "\n",
    "for repo in ['XiaomiMiMo/MiMo-7B-RL', 'Qwen/Qwen2.5-7B-Instruct']:\n",
    "    r = requests.get(f'https://huggingface.co/api/models/{repo}',\n",
    "                     params={'expand[]': 'inferenceProviderMapping'}).json()\n",
    "    providers = r.get('inferenceProviderMapping') or {}\n",
    "    print(f'{repo:32s} providers:', list(providers) or 'NONE')\n"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "So the weights run locally, and the only question left was on what.\n",
    "\n",
    "### 5.2 β€” Two runtimes, chosen at startup\n",
    "\n",
    "A Space's hardware is not the code's decision, so `mimo.py` carries both paths and picks one at\n",
    "import time. The prompt, prefill, decoding parameters and parser are byte-identical on both.\n",
    "\n",
    "| | `gpu` | `cpu` |\n",
    "|---|---|---|\n",
    "| weights | BF16 checkpoint, 4-bit NF4 via `bitsandbytes` | `quantflex/MiMo-7B-RL-nomtp-Q4_K_M.gguf` |\n",
    "| runs on | ZeroGPU, inside one `@spaces.GPU` call | `llama.cpp`, plain CPU |\n",
    "| speed | ~4.2 s / region | ~130 s / region |\n",
    "| limit | ~5 min of ZeroGPU per day | none |\n",
    "| Part B's configuration? | **yes, exactly** | no β€” see below |\n",
    "\n",
    "The CPU path is a genuine deviation and is labelled as one in the interface. MiMo carries\n",
    "multi-token-prediction layers that `llama.cpp` cannot load, so the only GGUF that runs at all is\n",
    "one with those layers deleted. MTP is a speculative-decoding accelerator that the ordinary forward\n",
    "pass does not use, so greedy output *should* be unaffected β€” but 'should be' is doing real work in\n",
    "that sentence.\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "print('backend selected here :', mimo.BACKEND)\n",
    "print('seconds per region    :', mimo.SECONDS)\n",
    "print('GPU grant requested   :', mimo.GPU_DURATION, 's ->',\n",
    "      int(mimo.GPU_DURATION * 1.5), 's reserved by the scheduler')\n",
    "print('regions per GPU batch :', mimo.MAX_WINDOWS_GPU)\n",
    "print()\n",
    "print(mimo.CAVEATS[mimo.BACKEND])\n"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### 5.3 β€” Batching, and why the user picks the batch\n",
    "\n",
    "Part B scored one window per document, because it already knew where the payload was. An uploaded\n",
    "file offers no such promise, and neither runtime can read every window of a real PDF inside its\n",
    "limit. So:\n",
    "\n",
    "1. the marker alternation that located the corpus payload is run over the **whole** skeleton, not\n",
    "   stopped at the first hit; every match becomes a candidate with the identical +/-1,500-character\n",
    "   shape, and overlapping ones are merged;\n",
    "2. the rest of the document is tiled into windows of the same size;\n",
    "3. the list is cut into batches sized to fit one run, and **you choose which batch to spend a run\n",
    "   on**. The report always states how much is still unread.\n",
    "\n",
    "Ranking decides reading order, never the verdict.\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "import corpus_text\n",
    "from huggingface_hub import hf_hub_download\n",
    "\n",
    "path = hf_hub_download('Cyber-security-final-project/Generated_Injected_PDFs_HARMLESS',\n",
    "                       'Output_PDFs/javascript_injection_WICAR_0001.pdf', repo_type='dataset')\n",
    "skeleton, truncated, dropped = corpus_text.build_skeleton(open(path, 'rb').read())\n",
    "\n",
    "for cover_all in (False, True):\n",
    "    regions = corpus_text.candidate_windows(skeleton, cover_all=cover_all)\n",
    "    print(f'cover_all={str(cover_all):5s} -> {len(regions)} region(s):',\n",
    "          [w['source'] for w in regions])\n"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### 5.4 β€” The sweep regions are honestly worse, and the app says so\n",
    "\n",
    "Marker-only triage leaves most of a file unread, and the marker set only knows the twelve families\n",
    "this project generated β€” so a payload shaped like none of them would sit in text the model never\n",
    "saw while the report said *clean*. The sweep closes that hole. It also introduces a real problem.\n",
    "\n",
    "Part B only ever showed MiMo marker-centred windows or the head of a document. Handed an arbitrary\n",
    "mid-file content stream β€” a page of font-positioning operators β€” **MiMo does not answer**: it\n",
    "carries on copying the input after the prefill, and the response parses as unrecoverable, which\n",
    "scores as *not injected*.\n",
    "\n",
    "Observed live on one injected corpus PDF:\n",
    "\n",
    "| region | kind | verdict | evidence |\n",
    "|---|---|---|---|\n",
    "| 1 | marker | **PAYLOAD**, `javascript_injection` | `/JS (var payload = 'eicar-standard-...')` |\n",
    "| 2-6 | sweep | clean *(unreadable answer)* | β€” |\n",
    "\n",
    "So: sweep regions buy coverage of text that would otherwise never be looked at, and a *clean*\n",
    "verdict on one is close to no evidence at all. Rows are labelled `marker` / `sweep`, the report\n",
    "counts sweep parse failures separately and explains them, and the sweep can be switched off to\n",
    "keep the app strictly inside the shape Part B measured.\n",
    "\n",
    "The tempting fixes β€” widening the parser, or editing the prompt to insist harder on JSON β€” are\n",
    "both refused on purpose. Either one would break the provenance argument that justifies quoting\n",
    "F1 0.945 anywhere in the interface.\n"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## 6 β€” What broke on the way, and what it cost\n",
    "\n",
    "| # | Symptom | Cause | Resolution |\n",
    "|---|---|---|---|\n",
    "| 1 | no provider serves MiMo | `inferenceProviderMapping` empty for every variant | run the weights locally |\n",
    "| 2 | `Quota exceeded for flavor cpu-basic: limit=0` | the **org** has no compute allowance; its old Space only ran because it was `sdk: static` | moved to the personal Space |\n",
    "| 3 | `Textbox.__init__() got an unexpected keyword 'show_copy_button'` | no `sdk_version` pinned, so HF installed gradio 6, which removed the argument | pinned `sdk_version: 5.50.0` **and** dropped the argument |\n",
    "| 4 | `No @spaces.GPU function detected during startup` | the Space is ZeroGPU, and the CPU-only app had no decorated function | added the GPU path; the decorator is applied at import, because ZeroGPU scans at startup |\n",
    "| 5 | `requested GPU duration (450s) is larger than the maximum allowed` | the `spaces` client asks for **1.5x** the declared duration, against a 300 s cap | lowered the duration; also moved the 15.7 GB weight download *outside* the grant |\n",
    "| 6 | `Value: 0 is not in the list of choices: []` | the batch dropdown validates against choices that are empty until a PDF is uploaded | `allow_custom_value=True` plus a tolerant `resolve_batch()` |\n",
    "| 7 | `exceeded your free ZeroGPU quota (270s requested vs 264s left)` | ~5 min/day, and the scheduler **reserves** the full requested duration up front | shorter grant (110 s), smaller batch (8), and a user-facing runtime picker so the CPU path can take over |\n",
    "| 8 | `libc.musl-x86_64.so.1: cannot open shared object file` | the prebuilt `llama-cpp-python` wheels are tagged `linux_x86_64` but linked against **musl**; a Space is glibc | build from the PyPI sdist, the only binary-free route |\n",
    "\n",
    "Two of these are worth carrying into any future Space in this project: **pin `sdk_version`**, and\n",
    "**never pin `torch`** β€” the image supplies one matching its own driver.\n"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## 7 β€” The numbers, and what they do not cover\n",
    "\n",
    "On the 1,100-document corpus Part B measured, MiMo-7B-RL scored **F1 0.945**, precision 0.988,\n",
    "recall 0.906, naming the family correctly 43.3% of the time, with 10 false alarms on 200 clean\n",
    "files and 155 unparsable answers.\n",
    "\n",
    "**A detector that calls every file malicious scores F1 0.900 on this corpus**, because 82% of it\n",
    "is injected. Read 0.945 against 0.900, not against zero β€” it is a 5% relative improvement over\n",
    "doing no work at all. Gemma-2-9B scored 0.969 and is the real Part B winner; MiMo is used here\n",
    "because it is ungated, needs no token, and is 2.6x faster.\n",
    "\n",
    "And the corpus is synthetic: harmless EICAR/AMTSO/WICAR/RANSIM test markers injected into ordinary\n",
    "PDFs. Real malware does not announce itself the same way. **This is not a general malware\n",
    "scanner.**\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "import pandas as pd\n",
    "\n",
    "PART_B = ('https://huggingface.co/datasets/Cyber-security-final-project/'\n",
    "          'Evaluation_of_OpenSource_Models_for_PDF_Injection_Recognition/'\n",
    "          'resolve/main/Part_B_Outputs/part_b_results.json')\n",
    "print(pd.read_json(PART_B).set_index('detector').to_string())\n"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## 8 β€” Deploying\n",
    "\n",
    "`VCURR/HF_SPACE/` is the source of truth; the Space is a deployment target, not a working copy.\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "# run from VCURR/HF_SPACE/\n",
    "from huggingface_hub import HfApi, CommitOperationAdd\n",
    "\n",
    "FILES = ['README.md', 'requirements.txt', 'app.py', 'corpus_text.py',\n",
    "         'mimo.py', 'neighbours.py', 'test_fidelity.py', 'App_Creation.ipynb']\n",
    "\n",
    "api = HfApi()\n",
    "commit = api.create_commit(\n",
    "    repo_id='BentoUniAcc/Mimo_Injection_detector', repo_type='space',\n",
    "    operations=[CommitOperationAdd(f, f) for f in FILES],\n",
    "    commit_message='describe the change here')\n",
    "print(commit.commit_url)\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "# build status\n",
    "import requests\n",
    "\n",
    "runtime = requests.get(\n",
    "    'https://huggingface.co/api/spaces/BentoUniAcc/Mimo_Injection_detector').json()['runtime']\n",
    "print(runtime['stage'], '|', runtime.get('errorMessage'))\n"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "---\n",
    "\n",
    "*Space: [BentoUniAcc/Mimo_Injection_detector](https://huggingface.co/spaces/BentoUniAcc/Mimo_Injection_detector)*\n"
   ]
  }
 ],
 "metadata": {
  "kernelspec": {
   "display_name": "Python 3",
   "language": "python",
   "name": "python3"
  },
  "language_info": {
   "name": "python",
   "version": "3.10"
  }
 },
 "nbformat": 4,
 "nbformat_minor": 5
}