File size: 6,514 Bytes
8ce7d0c
 
 
 
 
 
 
 
 
5180e9f
8ce7d0c
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
37bdc2b
8ce7d0c
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
bd126e1
8ce7d0c
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
bd126e1
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
import spaces  # MUST be first — before any torch/CUDA import
import torch
import json
import re
import gradio as gr

from threading import Thread
from transformers import AutoModelForCausalLM, AutoTokenizer, TextIteratorStreamer

MODEL_ID = "caid-technologies/parti-base"

tokenizer = AutoTokenizer.from_pretrained(MODEL_ID)
model = AutoModelForCausalLM.from_pretrained(
    MODEL_ID,
    torch_dtype=torch.bfloat16,
).to("cuda")
model.eval()

SYSTEM_PROMPT = (
    "You design hobbyist electronics projects. Given a request, reply with a single "
    "JSON object describing the full project. Output only the JSON."
)

MODE_PROMPTS = {
    "Full project plan": "",
    "Parts list only": " Respond with ONLY the parts list (a JSON array of components).",
    "Wiring map only": " Respond with ONLY the wiring/connection map (a JSON object of connections).",
    "Build steps only": " Respond with ONLY the ordered build steps (a JSON array of steps).",
}


def _build_messages(user_prompt: str, mode: str) -> list:
    suffix = MODE_PROMPTS.get(mode, "")
    return [
        {"role": "system", "content": SYSTEM_PROMPT},
        {"role": "user", "content": user_prompt + suffix},
    ]


def _try_format_json(text: str) -> str:
    """Attempt to extract and pretty-print a JSON block from the model output."""
    text = text.strip()
    # Try direct parse
    try:
        parsed = json.loads(text)
        return json.dumps(parsed, indent=2)
    except (json.JSONDecodeError, ValueError):
        pass
    # Try to find a JSON object or array in the text
    for pattern in [r"\{.*\}", r"\[.*\]"]:
        match = re.search(pattern, text, re.DOTALL)
        if match:
            try:
                parsed = json.loads(match.group())
                return json.dumps(parsed, indent=2)
            except (json.JSONDecodeError, ValueError):
                continue
    return text


@spaces.GPU(duration=75)
def generate(
    project_idea: str,
    mode: str,
    max_tokens: int,
    repetition_penalty: float,
):
    """Generate a structured hardware project plan from a plain-English description.

    Args:
        project_idea: A description of the hardware project you want to build.
        mode: What to generate — full plan, parts list, wiring map, or build steps.
        max_tokens: Maximum number of new tokens to generate.
        repetition_penalty: Penalty for repeated tokens (1.1 recommended for wiring lists).
    """
    messages = _build_messages(project_idea, mode)
    inputs = tokenizer.apply_chat_template(
        messages,
        add_generation_prompt=True,
        return_tensors="pt",
        return_dict=True,
    ).to("cuda")

    text_streamer = TextIteratorStreamer(
        tokenizer,
        skip_prompt=True,
        skip_special_tokens=True,
    )

    generation_kwargs = dict(
        **inputs,
        max_new_tokens=max_tokens,
        do_sample=False,
        repetition_penalty=repetition_penalty,
        pad_token_id=tokenizer.eos_token_id,
        streamer=text_streamer,
    )

    thread = Thread(target=model.generate, kwargs=generation_kwargs)
    thread.start()

    collected = ""
    for text in text_streamer:
        collected += text
        yield collected

    thread.join()

    # Try to pretty-print the JSON if valid
    yield _try_format_json(collected)


CSS = """
#col-container { max-width: 900px; margin: 0 auto; }
.dark .gradio-container { color: var(--body-text-color); }
"""

with gr.Blocks(title="Hardware Blueprint Generator") as demo:
    with gr.Column(elem_id="col-container"):
        gr.Markdown(
            "# Hardware Blueprint Generator\n"
            "Describe a hardware project and get a structured JSON blueprint — "
            "parts list, wiring map, build steps, and design check.\n\n"
            "Powered by [caid-technologies/blueprint-base](https://huggingface.co/caid-technologies/blueprint-base), "
            "a Qwen2.5-3B fine-tune for hobbyist electronics project planning."
        )

        with gr.Row():
            project_input = gr.Textbox(
                label="Project Description",
                placeholder="e.g. A compact desk clock with an e-ink display and an IR remote.",
                lines=3,
                scale=4,
            )
            mode_selector = gr.Dropdown(
                label="Output Mode",
                choices=list(MODE_PROMPTS.keys()),
                value="Full project plan",
                scale=1,
            )

        generate_btn = gr.Button("Generate Blueprint", variant="primary")

        output = gr.Textbox(
            label="Generated Blueprint (JSON)",
            lines=25,
        )

        with gr.Accordion("Advanced Settings", open=False):
            max_tokens = gr.Slider(
                label="Max New Tokens",
                minimum=1024,
                maximum=8192,
                value=6144,
                step=512,
                info="Higher values allow longer plans but take more time.",
            )
            rep_penalty = gr.Slider(
                label="Repetition Penalty",
                minimum=1.0,
                maximum=2.0,
                value=1.1,
                step=0.05,
                info="1.1 recommended — prevents wiring lists from repeating.",
            )

        gr.Examples(
            examples=[
                ["A compact desk clock with an e-ink display and an IR remote.", "Full project plan"],
                ["A weather station with temperature, humidity, and pressure sensors that uploads data to a web dashboard.", "Full project plan"],
                ["A pet feeder that dispenses food on a schedule and sends phone notifications.", "Parts list only"],
                ["A LoRa-based air quality monitor with a solar panel for power.", "Build steps only"],
            ],
            inputs=[project_input, mode_selector],
            outputs=output,
            fn=generate,
            cache_examples=True,
            cache_mode="lazy",
        )

        gr.Markdown(
            "---\n"
            "**Note:** This is an early research preview. Output is a *first draft* to review — "
            "not a finished engineering design. Always sanity-check before building."
        )

    generate_btn.click(
        fn=generate,
        inputs=[project_input, mode_selector, max_tokens, rep_penalty],
        outputs=output,
        api_name="generate",
    )

if __name__ == "__main__":
    demo.launch(mcp_server=True, theme=gr.themes.Citrus(), css=CSS)