Spaces:
Running on Zero
Running on Zero
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) |