kylebrodeur commited on
Commit
c6cc161
·
verified ·
1 Parent(s): 958a92e

space: add email signup for Microfactory updates

Browse files
Files changed (4) hide show
  1. FIELD-NOTES.md +17 -0
  2. README.md +11 -2
  3. app.py +17 -1
  4. core/signups.py +114 -0
FIELD-NOTES.md CHANGED
@@ -109,8 +109,25 @@ refusing any prefix that isn't `<username>/`, etc.) so the next adapter is a
109
  ten-minute job, not a half-day. Done means someone you've never met can pull
110
  and run it in one line. Build the publishing in.
111
 
 
 
112
  ---
113
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
114
  *Microfactory Node: 3D Printer runs fully local (Ollama / llama.cpp, Gemma E4B/E2B), falls back
115
  to a deterministic advisor, and publishes both its lesson ledger and its live interaction log as open
116
  datasets. It is the first node of the Microfactory, a network of small machines and the people who
 
109
  ten-minute job, not a half-day. Done means someone you've never met can pull
110
  and run it in one line. Build the publishing in.
111
 
112
+ Got it. To capture interest without pretending the product is finished, I added a simple email signup at the bottom of the Space. It is opt-in only: checkbox + email, clear privacy note, stored as a local JSONL and optionally synced to a private HF dataset when `HF_TOKEN` is set. No print data, no uploaded files, no third-party trackers. The same pattern as the field log, but for people instead of jobs.
113
+
114
  ---
115
 
116
+ ### Build paper trail
117
+
118
+ Everything that is part of the judged artifact is linked from the Space README and mirrored in the
119
+ GitHub repo:
120
+
121
+ - **Model cards:** [`learn/finetune/MODEL_CARD.md`](https://github.com/kylebrodeur/microfactory-lab/blob/main/chief-engineer/learn/finetune/MODEL_CARD.md) · [`MODEL_CARD_QAT.md`](https://github.com/kylebrodeur/microfactory-lab/blob/main/chief-engineer/learn/finetune/MODEL_CARD_QAT.md)
122
+ - **Local run / publish:** [`SERVING.md`](https://github.com/kylebrodeur/microfactory-lab/blob/main/chief-engineer/learn/finetune/SERVING.md) · [`OLLAMA_PUBLISHING.md`](https://github.com/kylebrodeur/microfactory-lab/blob/main/chief-engineer/learn/finetune/OLLAMA_PUBLISHING.md)
123
+ - **Fine-tune pipeline:** [`learn/finetune/README.md`](https://github.com/kylebrodeur/microfactory-lab/blob/main/chief-engineer/learn/finetune/README.md) · [`PIPELINE.md`](https://github.com/kylebrodeur/microfactory-lab/blob/main/chief-engineer/learn/finetune/PIPELINE.md) · [`RUNBOOK.md`](https://github.com/kylebrodeur/microfactory-lab/blob/main/chief-engineer/learn/finetune/RUNBOOK.md) · [`BUDGET.md`](https://github.com/kylebrodeur/microfactory-lab/blob/main/chief-engineer/learn/finetune/BUDGET.md)
124
+ - **Session report + activity trace:** [`SESSION_REPORT.md`](https://github.com/kylebrodeur/microfactory-lab/blob/main/chief-engineer/learn/finetune/SESSION_REPORT.md) · [`activity.jsonl`](https://github.com/kylebrodeur/microfactory-lab/blob/main/chief-engineer/learn/finetune/activity.jsonl)
125
+ - **Calibration:** [`sim/calibration/CALIBRATION-REPORT.md`](https://github.com/kylebrodeur/microfactory-lab/blob/main/chief-engineer/sim/calibration/CALIBRATION-REPORT.md) · [`sim/calibration/README.md`](https://github.com/kylebrodeur/microfactory-lab/blob/main/chief-engineer/sim/calibration/README.md)
126
+ - **Ingestion + assets + outputs:** [`ingest/README.md`](https://github.com/kylebrodeur/microfactory-lab/blob/main/chief-engineer/ingest/README.md) · [`assets/screenshots/README.md`](https://github.com/kylebrodeur/microfactory-lab/blob/main/chief-engineer/assets/screenshots/README.md) · [`dist/README.md`](https://github.com/kylebrodeur/microfactory-lab/blob/main/chief-engineer/dist/README.md) · [`dist/deliberation/README.md`](https://github.com/kylebrodeur/microfactory-lab/blob/main/chief-engineer/dist/deliberation/README.md)
127
+
128
+ Internal planning docs, raw workorders, and archived iterations stay in the GitHub repo but are
129
+ kept out of the Space so the public-facing card stays curated.
130
+
131
  *Microfactory Node: 3D Printer runs fully local (Ollama / llama.cpp, Gemma E4B/E2B), falls back
132
  to a deterministic advisor, and publishes both its lesson ledger and its live interaction log as open
133
  datasets. It is the first node of the Microfactory, a network of small machines and the people who
README.md CHANGED
@@ -141,9 +141,9 @@ Local dev uses [uv](https://docs.astral.sh/uv); the HF Space installs via `requi
141
  **Layout:** `app.py` (Space entrypoint) + `test_core.py` at the root; the core library lives in
142
  `core/`, runnable helpers in `scripts/` (run via `make <target>` or `uv run python -m scripts.<name>`),
143
  and the simulator / learning-loop / ingestion packages in `sim/`, `learn/`, `ingest/`. See
144
- `docs/RUNBOOK.md` for the full run-and-test order of operations.
145
 
146
- **HF Space deployment** (source repo): `docs/reference/DEPLOYMENT.md` + `docs/RUNBOOK.md`
147
  ("Deploy to the Space") cover Space variables, the dual local/Space config, build strategy,
148
  the `@spaces.GPU`-on-inference-only pattern, and field-log setup.
149
 
@@ -239,7 +239,16 @@ Storytelling is a judging principle, not a badge.
239
  - **Field log (live):** [build-small-hackathon/chief-engineer-field-log](https://huggingface.co/datasets/build-small-hackathon/chief-engineer-field-log)
240
  - **Demo video:** [<!-- TODO: add video URL after recording -->]()
241
  - **Social post:** [<!-- TODO: add social post URL after publishing -->]()
 
242
  - **How to use it (guided tour):** [`docs/RUNBOOK.md` §2](https://github.com/kylebrodeur/microfactory-lab/blob/main/chief-engineer/docs/RUNBOOK.md#2--use-the-tool-the-guided-tour--also-the-judges-tour)
 
 
 
 
 
 
 
 
243
  - **Source:** [kylebrodeur/microfactory-lab](https://github.com/kylebrodeur/microfactory-lab)
244
 
245
  ## License
 
141
  **Layout:** `app.py` (Space entrypoint) + `test_core.py` at the root; the core library lives in
142
  `core/`, runnable helpers in `scripts/` (run via `make <target>` or `uv run python -m scripts.<name>`),
143
  and the simulator / learning-loop / ingestion packages in `sim/`, `learn/`, `ingest/`. See
144
+ [`docs/RUNBOOK.md`](https://github.com/kylebrodeur/microfactory-lab/blob/main/chief-engineer/docs/RUNBOOK.md) for the full run-and-test order of operations.
145
 
146
+ **HF Space deployment** (source repo): [`docs/reference/DEPLOYMENT.md`](https://github.com/kylebrodeur/microfactory-lab/blob/main/chief-engineer/docs/reference/DEPLOYMENT.md) + [`docs/RUNBOOK.md`](https://github.com/kylebrodeur/microfactory-lab/blob/main/chief-engineer/docs/RUNBOOK.md)
147
  ("Deploy to the Space") cover Space variables, the dual local/Space config, build strategy,
148
  the `@spaces.GPU`-on-inference-only pattern, and field-log setup.
149
 
 
239
  - **Field log (live):** [build-small-hackathon/chief-engineer-field-log](https://huggingface.co/datasets/build-small-hackathon/chief-engineer-field-log)
240
  - **Demo video:** [<!-- TODO: add video URL after recording -->]()
241
  - **Social post:** [<!-- TODO: add social post URL after publishing -->]()
242
+ - **Stay in the loop:** an email signup lives at the bottom of the live Space. Opt-in only, clear privacy note, no third-party trackers.
243
  - **How to use it (guided tour):** [`docs/RUNBOOK.md` §2](https://github.com/kylebrodeur/microfactory-lab/blob/main/chief-engineer/docs/RUNBOOK.md#2--use-the-tool-the-guided-tour--also-the-judges-tour)
244
+ - **Build story / field notes:** [`docs/writeup/06-FIELD-NOTES.md`](https://github.com/kylebrodeur/microfactory-lab/blob/main/chief-engineer/docs/writeup/06-FIELD-NOTES.md)
245
+ - **Build paper trail (on this Space):**
246
+ - **Model cards:** [`LoRA v2`](learn/finetune/MODEL_CARD.md) · [`LoRA v3 QAT`](learn/finetune/MODEL_CARD_QAT.md)
247
+ - **Local run / publish:** [`SERVING.md`](learn/finetune/SERVING.md) · [`OLLAMA_PUBLISHING.md`](learn/finetune/OLLAMA_PUBLISHING.md)
248
+ - **Fine-tune pipeline:** [`learn/finetune/README.md`](learn/finetune/README.md) · [`PIPELINE.md`](learn/finetune/PIPELINE.md) · [`RUNBOOK.md`](learn/finetune/RUNBOOK.md) · [`BUDGET.md`](learn/finetune/BUDGET.md)
249
+ - **Session report + activity trace:** [`SESSION_REPORT.md`](learn/finetune/SESSION_REPORT.md) · [`activity.jsonl`](learn/finetune/activity.jsonl)
250
+ - **Calibration:** [`sim/calibration/CALIBRATION-REPORT.md`](sim/calibration/CALIBRATION-REPORT.md) · [`sim/calibration/README.md`](sim/calibration/README.md)
251
+ - **Ingestion + assets:** [`ingest/README.md`](ingest/README.md) · [`assets/screenshots/README.md`](assets/screenshots/README.md) · [`dist/README.md`](dist/README.md) · [`dist/deliberation/README.md`](dist/deliberation/README.md)
252
  - **Source:** [kylebrodeur/microfactory-lab](https://github.com/kylebrodeur/microfactory-lab)
253
 
254
  ## License
app.py CHANGED
@@ -39,6 +39,7 @@ from core import field_log
39
  from core import inspector
40
  from core import llm
41
  from core import seed_lessons
 
42
  from core.theme import (
43
  THEME, CSS, rule, command_bar, footer_bar, inspector_panel, icon, loader, tab_intro,
44
  CLOCK_JS,
@@ -783,7 +784,7 @@ def run_print(state, iterations, nozzle, bed, fan, retract, progress=gr.Progress
783
  + (f"first clean print at **iteration {first}**, now **{traj[-1]:.2f}**."
784
  if first else f"still improving — best **{max(traj):.2f}** after {len(traj)} runs.")
785
  + " The Engineer proposed; a separate simulated world reported the outcome; the **Inspector** "
786
- "graded each run; the policy and ledger learned. *(Simulated — see SIMULATION.md.)*"
787
  )
788
  policy_html = (f"{before_html}<div style='text-align:center;color:var(--ao-orange);font-size:11px;"
789
  f"letter-spacing:2px;'>{icon('arrow')} LEARNED</div>{policy_cell_html(after, key)}")
@@ -1144,6 +1145,18 @@ def build() -> gr.Blocks:
1144
  footer = gr.HTML(footer_bar())
1145
  privacy_line = gr.HTML(visible=field_log.is_active())
1146
 
 
 
 
 
 
 
 
 
 
 
 
 
1147
  # ── wiring ──
1148
  reset_outs = _reset_outputs(ledger_panel, node_cards, review_summary, p_curve, p_policy,
1149
  p_log, p_headline, outcome_panel, results_group, real_log_msg,
@@ -1167,6 +1180,9 @@ def build() -> gr.Blocks:
1167
  demo.load(None, None, None, js=CLOCK_JS)
1168
  demo.load(lambda: field_log.privacy_notice() if field_log.is_active() else "",
1169
  None, [privacy_line])
 
 
 
1170
  roll_btn.click(randomize_sensors, None, sensor_outs).then(status_footer, foot_in, [footer])
1171
  for c in (temp, humidity, bed_position, material):
1172
  c.change(sync_readout, [temp, humidity, bed_position], [sensors_readout]).then(
 
39
  from core import inspector
40
  from core import llm
41
  from core import seed_lessons
42
+ from core import signups
43
  from core.theme import (
44
  THEME, CSS, rule, command_bar, footer_bar, inspector_panel, icon, loader, tab_intro,
45
  CLOCK_JS,
 
784
  + (f"first clean print at **iteration {first}**, now **{traj[-1]:.2f}**."
785
  if first else f"still improving — best **{max(traj):.2f}** after {len(traj)} runs.")
786
  + " The Engineer proposed; a separate simulated world reported the outcome; the **Inspector** "
787
+ "graded each run; the policy and ledger learned. *(Simulated — see [SIMULATION.md](https://github.com/kylebrodeur/microfactory-lab/blob/main/chief-engineer/docs/reference/SIMULATION.md).)*"
788
  )
789
  policy_html = (f"{before_html}<div style='text-align:center;color:var(--ao-orange);font-size:11px;"
790
  f"letter-spacing:2px;'>{icon('arrow')} LEARNED</div>{policy_cell_html(after, key)}")
 
1145
  footer = gr.HTML(footer_bar())
1146
  privacy_line = gr.HTML(visible=field_log.is_active())
1147
 
1148
+ # Email signup for Microfactory updates (opt-in, privacy-first)
1149
+ with gr.Group(elem_classes=["ce-card"]):
1150
+ gr.HTML(rule("STAY IN THE LOOP"))
1151
+ gr.HTML("<div class='ce-sub'>Get Microfactory updates: new nodes, build notes, and when this thing learns to stream g-code straight to the printer. One email. No spam. Unsub any time.</div>")
1152
+ with gr.Row():
1153
+ signup_email = gr.Textbox(placeholder="you@example.com", label="EMAIL", show_label=False,
1154
+ elem_classes=["ce-notes-inline"], scale=3)
1155
+ signup_consent = gr.Checkbox(label="Yes, email me Microfactory updates", scale=2)
1156
+ signup_btn = gr.Button("SUBSCRIBE", elem_classes=["ce-pillbtn"])
1157
+ signup_status = gr.HTML()
1158
+ signup_privacy = gr.HTML(signups.privacy_notice())
1159
+
1160
  # ── wiring ──
1161
  reset_outs = _reset_outputs(ledger_panel, node_cards, review_summary, p_curve, p_policy,
1162
  p_log, p_headline, outcome_panel, results_group, real_log_msg,
 
1180
  demo.load(None, None, None, js=CLOCK_JS)
1181
  demo.load(lambda: field_log.privacy_notice() if field_log.is_active() else "",
1182
  None, [privacy_line])
1183
+ signup_btn.click(signups.record_signup,
1184
+ [signup_email, signup_consent, gr.State("space" if signups.is_active() else "local")],
1185
+ [signup_status])
1186
  roll_btn.click(randomize_sensors, None, sensor_outs).then(status_footer, foot_in, [footer])
1187
  for c in (temp, humidity, bed_position, material):
1188
  c.change(sync_readout, [temp, humidity, bed_position], [sensors_readout]).then(
core/signups.py ADDED
@@ -0,0 +1,114 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Email signup for Microfactory updates.
2
+
3
+ Lightweight, privacy-first opt-in collection. Stores one JSONL row per signup:
4
+ {ts, email, consent, source, app_version}. Local runs write to data/signups.jsonl.
5
+ Space runs can additionally push to a private HF dataset via CommitScheduler if
6
+ SIGNUPS_DATASET and HF_TOKEN are set. Never writes without explicit consent.
7
+ """
8
+
9
+ from __future__ import annotations
10
+
11
+ import json
12
+ import os
13
+ import re
14
+ import threading
15
+ from datetime import datetime, timezone
16
+ from pathlib import Path
17
+ from typing import Any
18
+
19
+ SIGNUPS_FILE = Path(__file__).resolve().parent.parent / "data" / "signups.jsonl"
20
+ SIGNUPS_DATASET = os.environ.get("SIGNUPS_DATASET", "kylebrodeur/microfactory-signups").strip()
21
+ FLUSH_MINUTES = 5
22
+
23
+ _scheduler: Any = None
24
+ _lock = threading.Lock()
25
+
26
+ _EMAIL_RE = re.compile(r"^[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,}$")
27
+
28
+
29
+ def _get_scheduler():
30
+ """Lazy-init CommitScheduler for the private signup dataset."""
31
+ global _scheduler
32
+ token = os.environ.get("HF_TOKEN", "").strip()
33
+ if not token:
34
+ return None
35
+ if not SIGNUPS_DATASET or "/" not in SIGNUPS_DATASET:
36
+ return None
37
+ if _scheduler is None:
38
+ with _lock:
39
+ if _scheduler is None:
40
+ try:
41
+ from huggingface_hub import CommitScheduler
42
+ except ImportError:
43
+ return None
44
+ SIGNUPS_FILE.parent.mkdir(parents=True, exist_ok=True)
45
+ if not SIGNUPS_FILE.exists():
46
+ SIGNUPS_FILE.write_text("", encoding="utf-8")
47
+ _scheduler = CommitScheduler(
48
+ repo_id=SIGNUPS_DATASET,
49
+ repo_type="dataset",
50
+ folder_path=str(SIGNUPS_FILE.parent),
51
+ every=FLUSH_MINUTES,
52
+ token=token,
53
+ allow_patterns=["signups.jsonl"],
54
+ )
55
+ return _scheduler
56
+
57
+
58
+ def is_active() -> bool:
59
+ """True if signup syncing to HF is configured and active."""
60
+ return _get_scheduler() is not None
61
+
62
+
63
+ def validate_email(email: str) -> str | None:
64
+ """Return normalized email or None if invalid."""
65
+ if not email:
66
+ return None
67
+ email = email.strip().lower()
68
+ if not _EMAIL_RE.match(email):
69
+ return None
70
+ return email
71
+
72
+
73
+ def record_signup(email: str, consent: bool, source: str = "local") -> tuple[bool, str]:
74
+ """Record one signup. Returns (ok, message).
75
+
76
+ Requires explicit consent. Email is validated. Writes locally always;
77
+ pushes to HF only when HF_TOKEN + SIGNUPS_DATASET are present.
78
+ """
79
+ if not consent:
80
+ return False, "Please check the box to opt in before submitting."
81
+ normalized = validate_email(email)
82
+ if normalized is None:
83
+ return False, "Enter a valid email address."
84
+
85
+ row = {
86
+ "ts": datetime.now(timezone.utc).isoformat(),
87
+ "email": normalized,
88
+ "consent": True,
89
+ "source": source,
90
+ "app_version": os.environ.get("CHIEF_ENGINEER_VERSION", "0.1.0"),
91
+ }
92
+
93
+ try:
94
+ SIGNUPS_FILE.parent.mkdir(parents=True, exist_ok=True)
95
+ with _lock:
96
+ with SIGNUPS_FILE.open("a", encoding="utf-8") as f:
97
+ f.write(json.dumps(row, ensure_ascii=False) + "\n")
98
+ sched = _get_scheduler()
99
+ if sched is not None:
100
+ try:
101
+ sched.trigger()
102
+ except Exception:
103
+ pass
104
+ return True, "You're on the list. Microfactory updates will hit that inbox."
105
+ except Exception as e:
106
+ return False, f"Could not save signup: {e}"
107
+
108
+
109
+ def privacy_notice() -> str:
110
+ return (
111
+ "<div class='ce-sub' style='font-size:10px;opacity:0.7;margin-top:4px;'>"
112
+ "📬 Microfactory updates: one email, no spam, unsub any time. "
113
+ "We store only your email and this timestamp. No print data, no uploaded files.</div>"
114
+ )