Spaces:
Running on Zero
Running on Zero
Paste box goes live: fine-tuned spec parser fills the form from messy text
Browse files- .gitattributes +1 -0
- app.py +8 -0
- model/metrics.json +11 -1
- model/spec_lora/README.md +62 -0
- model/spec_lora/adapter_config.json +52 -0
- model/spec_lora/adapter_model.safetensors +3 -0
- model/spec_lora/chat_template.jinja +98 -0
- model/spec_lora/tokenizer.json +3 -0
- model/spec_lora/tokenizer_config.json +233 -0
- model/speed_model.skops +1 -1
- requirements.txt +1 -0
- spec_brick.py +96 -0
- static/app.js +56 -0
- static/index.html +2 -0
- static/style.css +9 -0
.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:
|
| 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);
|