cn0303 commited on
Commit
0e8e243
·
verified ·
1 Parent(s): 51b6fd5

Paste box goes live: fine-tuned spec parser fills the form from messy text

Browse files
.gitattributes CHANGED
@@ -34,3 +34,4 @@ saved_model/**/* filter=lfs diff=lfs merge=lfs -text
34
  *.zst filter=lfs diff=lfs merge=lfs -text
35
  *tfevents* filter=lfs diff=lfs merge=lfs -text
36
  model/speed_model.skops filter=lfs diff=lfs merge=lfs -text
 
 
34
  *.zst filter=lfs diff=lfs merge=lfs -text
35
  *tfevents* filter=lfs diff=lfs merge=lfs -text
36
  model/speed_model.skops filter=lfs diff=lfs merge=lfs -text
37
+ model/spec_lora/tokenizer.json filter=lfs diff=lfs merge=lfs -text
app.py CHANGED
@@ -75,6 +75,14 @@ def api_lookup(payload: LookupIn):
75
  return lookup(p.get("repo", ""), p, spec_from_payload(p))
76
 
77
 
 
 
 
 
 
 
 
 
78
  @app.api(name="ask")
79
  def api_ask(question: str, facts: str = "") -> dict:
80
  """Plain-English follow-up, grounded in the facts /api/advise returned.
 
75
  return lookup(p.get("repo", ""), p, spec_from_payload(p))
76
 
77
 
78
+ @app.api(name="parse")
79
+ def api_parse(text: str = "") -> dict:
80
+ """Messy machine description -> form fields, via the fine-tuned spec
81
+ parser (cn0303/fitcheck-spec-parser). ZeroGPU via the Gradio queue."""
82
+ from spec_brick import parse_specs
83
+ return parse_specs(text)
84
+
85
+
86
  @app.api(name="ask")
87
  def api_ask(question: str, facts: str = "") -> dict:
88
  """Plain-English follow-up, grounded in the facts /api/advise returned.
model/metrics.json CHANGED
@@ -7,5 +7,15 @@
7
  "baseline_mape_pct": 28.1,
8
  "baseline_mae_tps": 11.63,
9
  "model_mape_on_bw_known_pct": 17.5,
10
- "model_mae_on_bw_known_tps": 9.55
 
 
 
 
 
 
 
 
 
 
11
  }
 
7
  "baseline_mape_pct": 28.1,
8
  "baseline_mae_tps": 11.63,
9
  "model_mape_on_bw_known_pct": 17.5,
10
+ "model_mae_on_bw_known_tps": 9.55,
11
+ "envelope": {
12
+ "bytes_gb": [
13
+ 0.84,
14
+ 9.85
15
+ ],
16
+ "eff_bw": [
17
+ 68.0,
18
+ 3350.0
19
+ ]
20
+ }
21
  }
model/spec_lora/README.md ADDED
@@ -0,0 +1,62 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ ---
2
+ license: apache-2.0
3
+ base_model: unsloth/Qwen3-1.7B
4
+ library_name: peft
5
+ pipeline_tag: text-generation
6
+ tags: [lora, sft, structured-extraction, hardware-specs, qwen3, unsloth]
7
+ ---
8
+
9
+ # FitCheck spec parser (Qwen3-1.7B LoRA)
10
+
11
+ Turns messy human descriptions of computers — "my dad's old Dell, i5, 16 gigs,
12
+ some nvidia card" — into the structured spec JSON used by
13
+ [FitCheck](https://huggingface.co/spaces/build-small-hackathon/FitCheck), the
14
+ honest "what AI can your computer run" advisor. This powers its paste box.
15
+
16
+ The one rule that matters: **missing information becomes `null`, never a
17
+ guess.** The model is trained and gated specifically against inventing specs.
18
+
19
+ ## Training data: grounded, not synthetic-echo
20
+
21
+ Labels are never model-generated: every training example starts from a **real
22
+ machine** (GPUs + VRAM from vendor spec tables, 212 cards + Apple chips); only
23
+ the phrasing varies, across ~24 registers mimicking how people actually write
24
+ (casual chat, dxdiag dumps, Task Manager paste, seller listings, consoles,
25
+ comparisons, half-remembered specs, several languages). ~39% of examples have
26
+ no GPU to extract — the don't-invent cases. Trained with Unsloth (bf16 LoRA,
27
+ completion-only loss) on a single RTX 5090 laptop.
28
+
29
+ ## Evaluation: human-written text only
30
+
31
+ Evaluated on a 45-example **human-written dev set** (never generator output;
32
+ multilingual, consoles, buying-intent traps, pure refusals). The builder
33
+ iterated against this set, so these are **dev numbers** — optimistically
34
+ biased by adaptive iteration, and labelled as such:
35
+
36
+ | round | field accuracy | invented-field rate (hallucination) |
37
+ |---|---|---|
38
+ | 1 | 77.3% | 32.5% |
39
+ | 3 (answer-only loss + explicit rules) | 85.8% | 12.0% |
40
+ | 5 (final) | **91.6%** | **1.2%** |
41
+
42
+ A **sealed test set** (written by people who never saw the training data,
43
+ evaluated exactly once, builder-blind) is pending; its result will be added
44
+ here unedited when run. Ship gate: beat the base model zero-shot AND keep the
45
+ invented-field rate under 5% — passed on dev.
46
+
47
+ ## Output schema
48
+
49
+ ```json
50
+ {"computer": "Windows laptop|Windows desktop|Mac|Linux PC|Mini PC / Raspberry Pi|null",
51
+ "ram_gb": "number|null", "provider": "nvidia|amd|apple|intel|none|null",
52
+ "gpu": "string|null", "vram_gb": "number|null"}
53
+ ```
54
+
55
+ Notable learned rules: `"none"` only when the text says there's no graphics
56
+ card (unknown → null); a series alone ("gtx") is a provider, not a GPU; a
57
+ stated VRAM figure beats the model's knowledge of that card; dxdiag's
58
+ "Display Memory" is not system RAM; "8gb dev kit" on a Jetson is unified RAM,
59
+ not VRAM; two machines compared → extract nothing.
60
+
61
+ Part of the FitCheck project (Build Small hackathon): a deterministic engine
62
+ does the math; small models appear only where they earn their place.
model/spec_lora/adapter_config.json ADDED
@@ -0,0 +1,52 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "alora_invocation_tokens": null,
3
+ "alpha_pattern": {},
4
+ "arrow_config": null,
5
+ "auto_mapping": {
6
+ "base_model_class": "Qwen3ForCausalLM",
7
+ "parent_library": "transformers.models.qwen3.modeling_qwen3",
8
+ "unsloth_fixed": true
9
+ },
10
+ "base_model_name_or_path": "unsloth/Qwen3-1.7B",
11
+ "bias": "none",
12
+ "corda_config": null,
13
+ "ensure_weight_tying": false,
14
+ "eva_config": null,
15
+ "exclude_modules": null,
16
+ "fan_in_fan_out": false,
17
+ "inference_mode": true,
18
+ "init_lora_weights": true,
19
+ "layer_replication": null,
20
+ "layers_pattern": null,
21
+ "layers_to_transform": null,
22
+ "loftq_config": {},
23
+ "lora_alpha": 16,
24
+ "lora_bias": false,
25
+ "lora_dropout": 0,
26
+ "lora_ga_config": null,
27
+ "megatron_config": null,
28
+ "megatron_core": "megatron.core",
29
+ "modules_to_save": null,
30
+ "peft_type": "LORA",
31
+ "peft_version": "0.19.1",
32
+ "qalora_group_size": 16,
33
+ "r": 16,
34
+ "rank_pattern": {},
35
+ "revision": null,
36
+ "target_modules": [
37
+ "up_proj",
38
+ "down_proj",
39
+ "q_proj",
40
+ "v_proj",
41
+ "o_proj",
42
+ "k_proj",
43
+ "gate_proj"
44
+ ],
45
+ "target_parameters": null,
46
+ "task_type": "CAUSAL_LM",
47
+ "trainable_token_indices": null,
48
+ "use_bdlora": null,
49
+ "use_dora": false,
50
+ "use_qalora": false,
51
+ "use_rslora": false
52
+ }
model/spec_lora/adapter_model.safetensors ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:7da9d72df630128f602da0724e78f33b98406752ba821b998ea2be521f90e6f4
3
+ size 69782384
model/spec_lora/chat_template.jinja ADDED
@@ -0,0 +1,98 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {%- if tools %}
2
+ {{- '<|im_start|>system\n' }}
3
+ {%- if messages[0].role == 'system' %}
4
+ {{- messages[0].content + '\n\n' }}
5
+ {%- endif %}
6
+ {{- "# Tools\n\nYou may call one or more functions to assist with the user query.\n\nYou are provided with function signatures within <tools></tools> XML tags:\n<tools>" }}
7
+ {%- for tool in tools %}
8
+ {{- "\n" }}
9
+ {{- tool | tojson }}
10
+ {%- endfor %}
11
+ {{- "\n</tools>\n\nFor each function call, return a json object with function name and arguments within <tool_call></tool_call> XML tags:\n<tool_call>\n{\"name\": <function-name>, \"arguments\": <args-json-object>}\n</tool_call><|im_end|>\n" }}
12
+ {%- else %}
13
+ {%- if messages[0].role == 'system' %}
14
+ {{- '<|im_start|>system\n' + messages[0].content + '<|im_end|>\n' }}
15
+ {%- endif %}
16
+ {%- endif %}
17
+ {%- set ns = namespace(multi_step_tool=true, last_query_index=messages|length - 1) %}
18
+ {%- for forward_message in messages %}
19
+ {%- set index = (messages|length - 1) - loop.index0 %}
20
+ {%- set message = messages[index] %}
21
+ {%- set current_content = message.content if message.content is not none else '' %}
22
+ {%- set tool_start = '<tool_response>' %}
23
+ {%- set tool_start_length = tool_start|length %}
24
+ {%- set start_of_message = current_content[:tool_start_length] %}
25
+ {%- set tool_end = '</tool_response>' %}
26
+ {%- set tool_end_length = tool_end|length %}
27
+ {%- set start_pos = (current_content|length) - tool_end_length %}
28
+ {%- if start_pos < 0 %}
29
+ {%- set start_pos = 0 %}
30
+ {%- endif %}
31
+ {%- set end_of_message = current_content[start_pos:] %}
32
+ {%- if ns.multi_step_tool and message.role == "user" and not(start_of_message == tool_start and end_of_message == tool_end) %}
33
+ {%- set ns.multi_step_tool = false %}
34
+ {%- set ns.last_query_index = index %}
35
+ {%- endif %}
36
+ {%- endfor %}
37
+ {%- for message in messages %}
38
+ {%- if (message.role == "user") or (message.role == "system" and not loop.first) %}
39
+ {{- '<|im_start|>' + message.role + '\n' + message.content + '<|im_end|>' + '\n' }}
40
+ {%- elif message.role == "assistant" %}
41
+ {%- set content = message.content %}
42
+ {%- set reasoning_content = '' %}
43
+ {%- if message.reasoning_content is defined and message.reasoning_content is not none %}
44
+ {%- set reasoning_content = message.reasoning_content %}
45
+ {%- else %}
46
+ {%- if '</think>' in message.content %}
47
+ {%- set content = (message.content.split('</think>')|last).lstrip('\n') %}
48
+ {%- set reasoning_content = (message.content.split('</think>')|first).rstrip('\n') %}
49
+ {%- set reasoning_content = (reasoning_content.split('<think>')|last).lstrip('\n') %}
50
+ {%- endif %}
51
+ {%- endif %}
52
+ {%- if loop.index0 > ns.last_query_index %}
53
+ {%- if loop.last or (not loop.last and reasoning_content) %}
54
+ {{- '<|im_start|>' + message.role + '\n<think>\n' + reasoning_content.strip('\n') + '\n</think>\n\n' + content.lstrip('\n') }}
55
+ {%- else %}
56
+ {{- '<|im_start|>' + message.role + '\n' + content }}
57
+ {%- endif %}
58
+ {%- else %}
59
+ {{- '<|im_start|>' + message.role + '\n' + content }}
60
+ {%- endif %}
61
+ {%- if message.tool_calls %}
62
+ {%- for tool_call in message.tool_calls %}
63
+ {%- if (loop.first and content) or (not loop.first) %}
64
+ {{- '\n' }}
65
+ {%- endif %}
66
+ {%- if tool_call.function %}
67
+ {%- set tool_call = tool_call.function %}
68
+ {%- endif %}
69
+ {{- '<tool_call>\n{"name": "' }}
70
+ {{- tool_call.name }}
71
+ {{- '", "arguments": ' }}
72
+ {%- if tool_call.arguments is string %}
73
+ {{- tool_call.arguments }}
74
+ {%- else %}
75
+ {{- tool_call.arguments | tojson }}
76
+ {%- endif %}
77
+ {{- '}\n</tool_call>' }}
78
+ {%- endfor %}
79
+ {%- endif %}
80
+ {{- '<|im_end|>\n' }}
81
+ {%- elif message.role == "tool" %}
82
+ {%- if loop.first or (messages[loop.index0 - 1].role != "tool") %}
83
+ {{- '<|im_start|>user' }}
84
+ {%- endif %}
85
+ {{- '\n<tool_response>\n' }}
86
+ {{- message.content }}
87
+ {{- '\n</tool_response>' }}
88
+ {%- if loop.last or (messages[loop.index0 + 1].role != "tool") %}
89
+ {{- '<|im_end|>\n' }}
90
+ {%- endif %}
91
+ {%- endif %}
92
+ {%- endfor %}
93
+ {%- if add_generation_prompt %}
94
+ {{- '<|im_start|>assistant\n' }}
95
+ {%- if enable_thinking is defined and enable_thinking is false %}
96
+ {{- '<think>\n\n</think>\n\n' }}
97
+ {%- endif %}
98
+ {%- endif %}
model/spec_lora/tokenizer.json ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:d7430e9138b76e93fb6f93462394d236b411111aef53cb421ba97d2691040cca
3
+ size 11423114
model/spec_lora/tokenizer_config.json ADDED
@@ -0,0 +1,233 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "add_prefix_space": false,
3
+ "backend": "tokenizers",
4
+ "bos_token": null,
5
+ "clean_up_tokenization_spaces": false,
6
+ "eos_token": "<|im_end|>",
7
+ "errors": "replace",
8
+ "is_local": false,
9
+ "model_max_length": 40960,
10
+ "pad_token": "<|PAD_TOKEN|>",
11
+ "padding_side": "left",
12
+ "split_special_tokens": false,
13
+ "tokenizer_class": "Qwen2Tokenizer",
14
+ "unk_token": null,
15
+ "added_tokens_decoder": {
16
+ "151643": {
17
+ "content": "<|endoftext|>",
18
+ "single_word": false,
19
+ "lstrip": false,
20
+ "rstrip": false,
21
+ "normalized": false,
22
+ "special": true
23
+ },
24
+ "151644": {
25
+ "content": "<|im_start|>",
26
+ "single_word": false,
27
+ "lstrip": false,
28
+ "rstrip": false,
29
+ "normalized": false,
30
+ "special": true
31
+ },
32
+ "151645": {
33
+ "content": "<|im_end|>",
34
+ "single_word": false,
35
+ "lstrip": false,
36
+ "rstrip": false,
37
+ "normalized": false,
38
+ "special": true
39
+ },
40
+ "151646": {
41
+ "content": "<|object_ref_start|>",
42
+ "single_word": false,
43
+ "lstrip": false,
44
+ "rstrip": false,
45
+ "normalized": false,
46
+ "special": true
47
+ },
48
+ "151647": {
49
+ "content": "<|object_ref_end|>",
50
+ "single_word": false,
51
+ "lstrip": false,
52
+ "rstrip": false,
53
+ "normalized": false,
54
+ "special": true
55
+ },
56
+ "151648": {
57
+ "content": "<|box_start|>",
58
+ "single_word": false,
59
+ "lstrip": false,
60
+ "rstrip": false,
61
+ "normalized": false,
62
+ "special": true
63
+ },
64
+ "151649": {
65
+ "content": "<|box_end|>",
66
+ "single_word": false,
67
+ "lstrip": false,
68
+ "rstrip": false,
69
+ "normalized": false,
70
+ "special": true
71
+ },
72
+ "151650": {
73
+ "content": "<|quad_start|>",
74
+ "single_word": false,
75
+ "lstrip": false,
76
+ "rstrip": false,
77
+ "normalized": false,
78
+ "special": true
79
+ },
80
+ "151651": {
81
+ "content": "<|quad_end|>",
82
+ "single_word": false,
83
+ "lstrip": false,
84
+ "rstrip": false,
85
+ "normalized": false,
86
+ "special": true
87
+ },
88
+ "151652": {
89
+ "content": "<|vision_start|>",
90
+ "single_word": false,
91
+ "lstrip": false,
92
+ "rstrip": false,
93
+ "normalized": false,
94
+ "special": true
95
+ },
96
+ "151653": {
97
+ "content": "<|vision_end|>",
98
+ "single_word": false,
99
+ "lstrip": false,
100
+ "rstrip": false,
101
+ "normalized": false,
102
+ "special": true
103
+ },
104
+ "151654": {
105
+ "content": "<|vision_pad|>",
106
+ "single_word": false,
107
+ "lstrip": false,
108
+ "rstrip": false,
109
+ "normalized": false,
110
+ "special": true
111
+ },
112
+ "151655": {
113
+ "content": "<|image_pad|>",
114
+ "single_word": false,
115
+ "lstrip": false,
116
+ "rstrip": false,
117
+ "normalized": false,
118
+ "special": true
119
+ },
120
+ "151656": {
121
+ "content": "<|video_pad|>",
122
+ "single_word": false,
123
+ "lstrip": false,
124
+ "rstrip": false,
125
+ "normalized": false,
126
+ "special": true
127
+ },
128
+ "151657": {
129
+ "content": "<tool_call>",
130
+ "single_word": false,
131
+ "lstrip": false,
132
+ "rstrip": false,
133
+ "normalized": false,
134
+ "special": false
135
+ },
136
+ "151658": {
137
+ "content": "</tool_call>",
138
+ "single_word": false,
139
+ "lstrip": false,
140
+ "rstrip": false,
141
+ "normalized": false,
142
+ "special": false
143
+ },
144
+ "151659": {
145
+ "content": "<|fim_prefix|>",
146
+ "single_word": false,
147
+ "lstrip": false,
148
+ "rstrip": false,
149
+ "normalized": false,
150
+ "special": false
151
+ },
152
+ "151660": {
153
+ "content": "<|fim_middle|>",
154
+ "single_word": false,
155
+ "lstrip": false,
156
+ "rstrip": false,
157
+ "normalized": false,
158
+ "special": false
159
+ },
160
+ "151661": {
161
+ "content": "<|fim_suffix|>",
162
+ "single_word": false,
163
+ "lstrip": false,
164
+ "rstrip": false,
165
+ "normalized": false,
166
+ "special": false
167
+ },
168
+ "151662": {
169
+ "content": "<|fim_pad|>",
170
+ "single_word": false,
171
+ "lstrip": false,
172
+ "rstrip": false,
173
+ "normalized": false,
174
+ "special": false
175
+ },
176
+ "151663": {
177
+ "content": "<|repo_name|>",
178
+ "single_word": false,
179
+ "lstrip": false,
180
+ "rstrip": false,
181
+ "normalized": false,
182
+ "special": false
183
+ },
184
+ "151664": {
185
+ "content": "<|file_sep|>",
186
+ "single_word": false,
187
+ "lstrip": false,
188
+ "rstrip": false,
189
+ "normalized": false,
190
+ "special": false
191
+ },
192
+ "151665": {
193
+ "content": "<tool_response>",
194
+ "single_word": false,
195
+ "lstrip": false,
196
+ "rstrip": false,
197
+ "normalized": false,
198
+ "special": false
199
+ },
200
+ "151666": {
201
+ "content": "</tool_response>",
202
+ "single_word": false,
203
+ "lstrip": false,
204
+ "rstrip": false,
205
+ "normalized": false,
206
+ "special": false
207
+ },
208
+ "151667": {
209
+ "content": "<think>",
210
+ "single_word": false,
211
+ "lstrip": false,
212
+ "rstrip": false,
213
+ "normalized": false,
214
+ "special": false
215
+ },
216
+ "151668": {
217
+ "content": "</think>",
218
+ "single_word": false,
219
+ "lstrip": false,
220
+ "rstrip": false,
221
+ "normalized": false,
222
+ "special": false
223
+ },
224
+ "151669": {
225
+ "content": "<|PAD_TOKEN|>",
226
+ "single_word": false,
227
+ "lstrip": false,
228
+ "rstrip": false,
229
+ "normalized": false,
230
+ "special": true
231
+ }
232
+ }
233
+ }
model/speed_model.skops CHANGED
@@ -1,3 +1,3 @@
1
  version https://git-lfs.github.com/spec/v1
2
- oid sha256:70249e8241e24de5202af7ba82e911b9dbfaed06c19be920d6b6fd2873bef58b
3
  size 1658817
 
1
  version https://git-lfs.github.com/spec/v1
2
+ oid sha256:43f0f404f6cd13cfad072c764bcfebb9775cd56b994bbd65669185ce04228319
3
  size 1658817
requirements.txt CHANGED
@@ -14,3 +14,4 @@ accelerate # device placement / efficient loading
14
  einops # required by the kernels-community mamba-ssm kernel
15
  skops # safe loading of the trained speed predictor
16
  xgboost # the speed predictor's runtime (engine/speed.py)
 
 
14
  einops # required by the kernels-community mamba-ssm kernel
15
  skops # safe loading of the trained speed predictor
16
  xgboost # the speed predictor's runtime (engine/speed.py)
17
+ peft # spec-parser LoRA adapter loading
spec_brick.py ADDED
@@ -0,0 +1,96 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ The spec-parser brick: messy human text -> the advisor's form fields.
3
+
4
+ Serves cn0303/fitcheck-spec-parser (Qwen3-1.7B + LoRA, trained in this repo —
5
+ see scripts/train_spec_lora.py and the model card for the honest eval). Same
6
+ serving pattern as the narrator: lazy load inside @spaces.GPU, loud errors,
7
+ no fake fallbacks. Missing info comes back null — the model is specifically
8
+ gated against inventing specs.
9
+ """
10
+
11
+ import json
12
+ import re
13
+ import sys
14
+
15
+ from model_brick import _should_load
16
+
17
+ ADAPTER_ID = "cn0303/fitcheck-spec-parser"
18
+ BASE_ID = "unsloth/Qwen3-1.7B"
19
+
20
+ # MUST stay in sync with scripts/build_spec_dataset.py (the training prompt).
21
+ SYSTEM_PROMPT = """\
22
+ You turn a person's description of their computer into JSON for a hardware checker.
23
+ Output ONLY a JSON object with exactly these fields:
24
+ {"computer": "Windows laptop"|"Windows desktop"|"Mac"|"Linux PC"|"Mini PC / Raspberry Pi"|null,
25
+ "ram_gb": number|null, "provider": "nvidia"|"amd"|"apple"|"intel"|"none"|null,
26
+ "gpu": string|null, "vram_gb": number|null}
27
+ Rules:
28
+ - Extract ONLY what the text states or directly implies. Anything not stated is null. Never guess or invent a spec.
29
+ - "provider": "none" ONLY when the text says there is no separate graphics card (e.g. "no GPU", "integrated only"). Graphics simply not mentioned or unknown -> null.
30
+ - "gpu" must be a specific model (e.g. "RTX 3060"). A brand or series alone ("geforce", "gtx", "radeon") is NOT a gpu -> set provider, leave gpu null.
31
+ - If the text describes two or more different machines or a choice between them, every field is null."""
32
+
33
+ FIELDS = ("computer", "ram_gb", "provider", "gpu", "vram_gb")
34
+
35
+ _GENERATE = None
36
+ _state = {"tok": None, "model": None}
37
+
38
+ if _should_load():
39
+ try:
40
+ import spaces
41
+ import torch
42
+ from transformers import AutoModelForCausalLM, AutoTokenizer
43
+
44
+ def _load():
45
+ from peft import PeftModel
46
+ tok = AutoTokenizer.from_pretrained(ADAPTER_ID)
47
+ model = AutoModelForCausalLM.from_pretrained(BASE_ID, dtype=torch.bfloat16)
48
+ model = PeftModel.from_pretrained(model, ADAPTER_ID)
49
+ _state["tok"] = tok
50
+ _state["model"] = model.to("cuda").eval()
51
+
52
+ @spaces.GPU(duration=60)
53
+ def _generate(text: str) -> str:
54
+ if _state["model"] is None:
55
+ _load()
56
+ tok, model = _state["tok"], _state["model"]
57
+ msgs = [{"role": "system", "content": SYSTEM_PROMPT},
58
+ {"role": "user", "content": text}]
59
+ kw = dict(add_generation_prompt=True, return_tensors="pt", return_dict=True)
60
+ try:
61
+ inputs = tok.apply_chat_template(msgs, enable_thinking=False, **kw)
62
+ except TypeError:
63
+ inputs = tok.apply_chat_template(msgs, **kw)
64
+ inputs = inputs.to("cuda")
65
+ n = inputs["input_ids"].shape[1]
66
+ with torch.no_grad():
67
+ out = model.generate(**inputs, max_new_tokens=96, do_sample=False,
68
+ pad_token_id=tok.eos_token_id)
69
+ return tok.decode(out[0][n:], skip_special_tokens=True).strip()
70
+
71
+ _GENERATE = _generate
72
+ except Exception as e: # noqa: BLE001
73
+ print(f"[FitCheck] spec parser unavailable: {e!r}", file=sys.stderr, flush=True)
74
+
75
+
76
+ def parse_specs(text: str) -> dict:
77
+ """Returns the parsed fields, or {error} — never invented content."""
78
+ text = (text or "").strip()
79
+ if not text:
80
+ return {"error": "Nothing to parse — paste or type a description first."}
81
+ if len(text) > 4000:
82
+ text = text[:4000]
83
+ if _GENERATE is None:
84
+ return {"error": "The spec parser model isn't loaded in this environment."}
85
+ try:
86
+ raw = _GENERATE(text)
87
+ except Exception as e: # noqa: BLE001
88
+ return {"error": f"Spec parser failed: {e}"}
89
+ m = re.search(r"\{.*\}", raw, re.DOTALL)
90
+ if not m:
91
+ return {"error": f"The parser didn't return JSON. Raw output: {raw[:200]}"}
92
+ try:
93
+ obj = json.loads(m.group(0))
94
+ except json.JSONDecodeError:
95
+ return {"error": f"The parser returned malformed JSON: {m.group(0)[:200]}"}
96
+ return {f: obj.get(f) for f in FIELDS}
static/app.js CHANGED
@@ -567,6 +567,61 @@ async function drawRoofline(speed) {
567
  host.innerHTML = s;
568
  }
569
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
570
  // ---- Follow-up: the model brick (grounded explainer) ---------------------
571
  function wireAsk() {
572
  const input = $("#ask-input"), send = $("#ask-send");
@@ -656,6 +711,7 @@ function init() {
656
  ["#ram","#gpu","#vram","#custom-uc","#repo-check"].forEach(s => { const el = $(s); if (el) el.addEventListener("change", maybeLiveUpdate); });
657
  $("#paste").addEventListener("input", maybeLiveUpdate);
658
  $("#check-btn").addEventListener("click", check);
 
659
  fillGpu();
660
  $("#find-specs-body").innerHTML = findSpecsText();
661
  detectHardware();
 
567
  host.innerHTML = s;
568
  }
569
 
570
+ // ---- Paste box: the fine-tuned spec parser fills the form -----------------
571
+ async function parsePaste() {
572
+ const text = $("#paste").value.trim();
573
+ const hint = $("#parse-hint");
574
+ if (!text) { hint.textContent = "Paste or type something first."; return; }
575
+ hint.textContent = "Reading it… (first use after a quiet spell wakes the model, up to a minute)";
576
+ try {
577
+ const client = await getClient();
578
+ const r = await client.predict("/parse", { text });
579
+ const d = Array.isArray(r.data) ? r.data[0] : r.data;
580
+ if (d.error) { hint.textContent = d.error; return; }
581
+ applyParsed(d, hint);
582
+ } catch (e) {
583
+ hint.textContent = `Parser unavailable: ${e && e.message ? e.message : e}`;
584
+ }
585
+ }
586
+
587
+ function applyParsed(d, hint) {
588
+ const got = [];
589
+ if (d.computer) {
590
+ state.computer = d.computer;
591
+ setActive("#computer-seg", d.computer);
592
+ syncProviderForComputer();
593
+ got.push(d.computer);
594
+ }
595
+ if (d.provider) {
596
+ state.provider = d.provider;
597
+ setActive("#provider-seg", d.provider);
598
+ fillGpu();
599
+ got.push(d.provider.toUpperCase());
600
+ }
601
+ if (d.gpu) {
602
+ const sel = $("#gpu");
603
+ const want = String(d.gpu).toLowerCase().replace(/\b(nvidia|geforce|amd|radeon|intel)\b/g, "").trim();
604
+ const match = [...sel.options].find(o => o.value.toLowerCase().includes(want));
605
+ if (match) { sel.value = match.value; got.push(match.value); }
606
+ else if (d.vram_gb) { $("#vram").value = d.vram_gb; got.push(`${d.gpu} (${d.vram_gb} GB)`); }
607
+ else got.push(d.gpu);
608
+ } else if (d.vram_gb) {
609
+ $("#vram").value = d.vram_gb;
610
+ got.push(`${d.vram_gb} GB VRAM`);
611
+ }
612
+ if (d.ram_gb) {
613
+ const ram = $("#ram");
614
+ const opt = [...ram.options].map(o => parseFloat(o.value))
615
+ .reduce((a, b) => Math.abs(b - d.ram_gb) < Math.abs(a - d.ram_gb) ? b : a);
616
+ ram.value = `${opt} GB`;
617
+ got.push(`${d.ram_gb} GB RAM`);
618
+ }
619
+ hint.textContent = got.length
620
+ ? `Understood: ${got.join(", ")}. Anything you didn't mention stayed blank — confirm and press Check.`
621
+ : "Couldn't find any specs in that text — nothing was filled in (the parser doesn't guess).";
622
+ maybeLiveUpdate();
623
+ }
624
+
625
  // ---- Follow-up: the model brick (grounded explainer) ---------------------
626
  function wireAsk() {
627
  const input = $("#ask-input"), send = $("#ask-send");
 
711
  ["#ram","#gpu","#vram","#custom-uc","#repo-check"].forEach(s => { const el = $(s); if (el) el.addEventListener("change", maybeLiveUpdate); });
712
  $("#paste").addEventListener("input", maybeLiveUpdate);
713
  $("#check-btn").addEventListener("click", check);
714
+ const pb = $("#parse-btn"); if (pb) pb.addEventListener("click", parsePaste);
715
  fillGpu();
716
  $("#find-specs-body").innerHTML = findSpecsText();
717
  detectHardware();
static/index.html CHANGED
@@ -90,6 +90,8 @@
90
  <div class="field" style="margin-bottom:0">
91
  <span class="label">Or paste / describe your specs</span>
92
  <textarea id="paste" placeholder="Paste output from 'dxdiag' or 'Task Manager → Performance', or just describe it: 'Dell XPS, RTX 3050, 16GB'…"></textarea>
 
 
93
  </div>
94
  </div>
95
  </details>
 
90
  <div class="field" style="margin-bottom:0">
91
  <span class="label">Or paste / describe your specs</span>
92
  <textarea id="paste" placeholder="Paste output from 'dxdiag' or 'Task Manager → Performance', or just describe it: 'Dell XPS, RTX 3050, 16GB'…"></textarea>
93
+ <button class="parse-btn" id="parse-btn">Fill the form from this</button>
94
+ <div class="hint" id="parse-hint">A small model fine-tuned for this reads your text and fills the form. It never guesses: anything you didn't say stays blank.</div>
95
  </div>
96
  </div>
97
  </details>
static/style.css CHANGED
@@ -240,6 +240,15 @@ details.disc > summary:hover { color: var(--text-primary); }
240
  .disc-body dt { font-weight: 700; color: var(--text-primary); font-family: var(--font-head); }
241
  .disc-body dd { color: var(--text-muted); }
242
 
 
 
 
 
 
 
 
 
 
243
  /* Primary CTA */
244
  .cta {
245
  width: 100%; border: none; border-radius: var(--r-md);
 
240
  .disc-body dt { font-weight: 700; color: var(--text-primary); font-family: var(--font-head); }
241
  .disc-body dd { color: var(--text-muted); }
242
 
243
+ .parse-btn {
244
+ margin-top: var(--s-2); width: 100%;
245
+ background: var(--accent-soft); border: 1px solid var(--accent);
246
+ color: var(--text-primary); border-radius: var(--r-sm);
247
+ padding: 9px 12px; font-size: 13.5px; font-weight: 600;
248
+ transition: filter .15s, transform .15s;
249
+ }
250
+ .parse-btn:hover { filter: brightness(1.15); transform: translateY(-1px); }
251
+
252
  /* Primary CTA */
253
  .cta {
254
  width: 100%; border: none; border-radius: var(--r-md);