suhpau commited on
Commit
9b9f4c0
Β·
verified Β·
1 Parent(s): cfc0d9b

Update index.html

Browse files
Files changed (1) hide show
  1. index.html +69 -44
index.html CHANGED
@@ -3,12 +3,18 @@
3
  <head>
4
  <meta charset="utf-8" />
5
  <meta name="viewport" content="width=device-width,initial-scale=1" />
6
- <title>Hot / Cold / Normal Indicator</title>
7
  <style>
8
  body { font-family: system-ui, -apple-system, Segoe UI, Roboto, sans-serif; margin: 24px; }
9
  .row { display:flex; gap:16px; flex-wrap:wrap; align-items:flex-start; }
10
  .card { border:1px solid #ddd; border-radius:16px; padding:16px; box-shadow: 0 2px 10px rgba(0,0,0,.04); }
11
- video { width: 420px; max-width: 100%; border-radius: 14px; background:#000; }
 
 
 
 
 
 
12
 
13
  #lamp {
14
  width: 240px; height: 240px; border-radius: 999px;
@@ -23,22 +29,16 @@
23
  #lamp.normal { background: #757575; box-shadow: 0 10px 30px rgba(117,117,117,.25); }
24
  #lamp.pulse { transform: scale(1.03); }
25
 
26
- button { padding: 10px 14px; border-radius: 12px; border: 1px solid #ccc; background:#fff; cursor:pointer; }
27
- button:disabled { opacity:.5; cursor:not-allowed; }
28
-
29
- .controls { display:flex; gap:10px; flex-wrap:wrap; align-items:center; }
30
- .small { color:#666; font-size: 14px; line-height:1.4; }
31
- input[type="range"] { width: 220px; }
32
- #preds { font-size: 14px; white-space: pre; }
33
- .badge { display:inline-block; padding:2px 8px; border-radius:999px; border:1px solid #ddd; font-size:12px; color:#444; }
34
  </style>
35
  </head>
36
 
37
  <body>
38
- <h2>Teachable Machine λͺ¨μ…˜ νŒμ • β†’ πŸ”΄/πŸ”΅/βšͺ + μŒμ„± μ•ˆλ‚΄</h2>
39
  <p class="small">
40
- Teachable Machine λͺ¨λΈμ„ λΆˆλŸ¬μ™€ μ›ΉμΊ μ—μ„œ μ‹€μ‹œκ°„ λΆ„λ₯˜ν•˜κ³ ,<br>
41
- κ²°κ³Όλ₯Ό <b>λΉ¨κ°„λΆˆ(λ”μ›Œμš”) / νŒŒλž€λΆˆ(μΆ”μ›Œμš”) / νšŒμƒ‰λΆˆ(ν‰μƒμ‹œ)</b> + μŒμ„±μœΌλ‘œ μ•Œλ €μ€λ‹ˆλ‹€.
42
  </p>
43
 
44
  <div class="row">
@@ -62,9 +62,12 @@
62
  </div>
63
 
64
  <div style="margin-top:12px;">
65
- <video id="webcam" autoplay playsinline muted></video>
 
66
  </div>
67
 
 
 
68
  <p class="small" style="margin-top:10px;">
69
  팁: μž„κ³„κ°’μ„ 올리면 더 β€œν™•μ‹€ν•  λ•Œλ§Œβ€ λ”μ›Œμš”/μΆ”μ›Œμš”κ°€ λœΉλ‹ˆλ‹€.
70
  </p>
@@ -85,15 +88,15 @@
85
  </div>
86
  </div>
87
 
88
- <!-- TFJS + Teachable Machine -->
89
  <script src="https://cdn.jsdelivr.net/npm/@tensorflow/tfjs@4.20.0/dist/tf.min.js"></script>
90
- <script src="https://cdn.jsdelivr.net/npm/@teachablemachine/image@0.8/dist/teachablemachine-image.min.js"></script>
91
 
92
  <script>
93
- // λ„€ Teachable Machine λͺ¨λΈ μ£Όμ†Œ
94
  const MODEL_URL = "https://teachablemachine.withgoogle.com/models/IMOO3vPNb/";
95
 
96
- // 클래슀 이름 (μ •ν™•νžˆ λ§žμΆ°μ•Ό 함)
97
  const CLASS_HOT = "λ”μšΈλ•Œ";
98
  const CLASS_COLD = "μΆ”μšΈλ•Œ";
99
  const CLASS_NORMAL = "ν‰μƒμ‹œ";
@@ -106,6 +109,7 @@
106
  const predsEl = document.getElementById("preds");
107
  const statusEl = document.getElementById("status");
108
  const lastSayEl = document.getElementById("lastSay");
 
109
 
110
  const btnStart = document.getElementById("btnStart");
111
  const btnStop = document.getElementById("btnStop");
@@ -113,7 +117,9 @@
113
  const threshVal = document.getElementById("threshVal");
114
  const voiceOn = document.getElementById("voiceOn");
115
  const speakNormal = document.getElementById("speakNormal");
116
- const videoEl = document.getElementById("webcam");
 
 
117
 
118
  thresh.addEventListener("input", () => threshVal.textContent = (+thresh.value).toFixed(2));
119
 
@@ -154,32 +160,38 @@
154
  const pNormal = getProb(predictions, CLASS_NORMAL);
155
 
156
  const arr = [
157
- {state: "hot", text: "λ”μ›Œμš”", p: pHot},
158
- {state: "cold", text: "μΆ”μ›Œμš”", p: pCold},
159
- {state: "normal", text: "ν‰μƒμ‹œ", p: pNormal},
160
- ].sort((a,b) => b.p - a.p);
161
 
162
  const top = arr[0];
163
- if (top.p < threshold) {
164
- return {state:"normal", text:"ν‰μƒμ‹œ"};
165
- }
166
  return top;
167
  }
168
 
 
 
 
 
 
169
  async function loop() {
170
  webcam.update();
171
- const predictions = await model.predict(webcam.canvas);
 
 
 
 
 
172
 
173
  predsEl.textContent = predictions
174
  .map(p => `${p.className}: ${p.probability.toFixed(2)}`)
175
  .join("\n");
176
 
177
- const th = +thresh.value;
178
- const d = decideState(predictions, th);
179
 
180
  if (d.state !== lastState) {
181
  lastState = d.state;
182
-
183
  if (d.state === "hot") {
184
  setLamp("hot", "λ”μ›Œμš”");
185
  speak("λ”μ›Œμš”");
@@ -196,23 +208,36 @@
196
  }
197
 
198
  async function start() {
 
199
  btnStart.disabled = true;
200
  setLamp("normal", "λ‘œλ”©μ€‘...");
201
 
202
- const modelURL = MODEL_URL + "model.json";
203
- const metadataURL = MODEL_URL + "metadata.json";
204
- model = await tmImage.load(modelURL, metadataURL);
205
-
206
- webcam = new tmImage.Webcam(420, 315, true);
207
- await webcam.setup();
208
- await webcam.play();
209
-
210
- videoEl.srcObject = webcam.webcam;
211
-
212
- btnStop.disabled = false;
213
- setLamp("normal", "뢄석쀑");
214
- lastState = "normal";
215
- rafId = requestAnimationFrame(loop);
 
 
 
 
 
 
 
 
 
 
 
 
216
  }
217
 
218
  function stop() {
 
3
  <head>
4
  <meta charset="utf-8" />
5
  <meta name="viewport" content="width=device-width,initial-scale=1" />
6
+ <title>Hot / Cold / Normal Indicator (Pose)</title>
7
  <style>
8
  body { font-family: system-ui, -apple-system, Segoe UI, Roboto, sans-serif; margin: 24px; }
9
  .row { display:flex; gap:16px; flex-wrap:wrap; align-items:flex-start; }
10
  .card { border:1px solid #ddd; border-radius:16px; padding:16px; box-shadow: 0 2px 10px rgba(0,0,0,.04); }
11
+ .controls { display:flex; gap:10px; flex-wrap:wrap; align-items:center; }
12
+ button { padding: 10px 14px; border-radius: 12px; border: 1px solid #ccc; background:#fff; cursor:pointer; }
13
+ button:disabled { opacity:.5; cursor:not-allowed; }
14
+ .small { color:#666; font-size: 14px; line-height:1.4; }
15
+ input[type="range"] { width: 220px; }
16
+ #preds { font-size: 14px; white-space: pre; }
17
+ .badge { display:inline-block; padding:2px 8px; border-radius:999px; border:1px solid #ddd; font-size:12px; color:#444; }
18
 
19
  #lamp {
20
  width: 240px; height: 240px; border-radius: 999px;
 
29
  #lamp.normal { background: #757575; box-shadow: 0 10px 30px rgba(117,117,117,.25); }
30
  #lamp.pulse { transform: scale(1.03); }
31
 
32
+ canvas { border-radius: 14px; background:#000; max-width: 100%; }
33
+ .error { color:#b00020; font-weight:700; margin-top:10px; white-space:pre-wrap; }
 
 
 
 
 
 
34
  </style>
35
  </head>
36
 
37
  <body>
38
+ <h2>Teachable Machine λͺ¨μ…˜(Pose) νŒμ • β†’ πŸ”΄/πŸ”΅/βšͺ + μŒμ„±</h2>
39
  <p class="small">
40
+ Pose λͺ¨λΈμ„ λΆˆλŸ¬μ™€ μ›ΉμΊ μ—μ„œ μ‹€μ‹œκ°„ λΆ„λ₯˜ν•˜κ³ ,
41
+ <b>λΉ¨κ°„λΆˆ(λ”μ›Œμš”) / νŒŒλž€λΆˆ(μΆ”μ›Œμš”) / νšŒμƒ‰λΆˆ(ν‰μƒμ‹œ)</b> + μŒμ„±μœΌλ‘œ μ•Œλ €μ€λ‹ˆλ‹€.
42
  </p>
43
 
44
  <div class="row">
 
62
  </div>
63
 
64
  <div style="margin-top:12px;">
65
+ <!-- PoseλŠ” video λŒ€μ‹  canvas둜 λ Œλ”λ§ν•˜λŠ”κ²Œ μ•ˆμ •μ  -->
66
+ <canvas id="canvas" width="420" height="315"></canvas>
67
  </div>
68
 
69
+ <div id="err" class="error"></div>
70
+
71
  <p class="small" style="margin-top:10px;">
72
  팁: μž„κ³„κ°’μ„ 올리면 더 β€œν™•μ‹€ν•  λ•Œλ§Œβ€ λ”μ›Œμš”/μΆ”μ›Œμš”κ°€ λœΉλ‹ˆλ‹€.
73
  </p>
 
88
  </div>
89
  </div>
90
 
91
+ <!-- TFJS + Teachable Machine (Pose) -->
92
  <script src="https://cdn.jsdelivr.net/npm/@tensorflow/tfjs@4.20.0/dist/tf.min.js"></script>
93
+ <script src="https://cdn.jsdelivr.net/npm/@teachablemachine/pose@0.8/dist/teachablemachine-pose.min.js"></script>
94
 
95
  <script>
96
+ // λ„€ Teachable Machine λͺ¨λΈ μ£Όμ†Œ (끝에 / 포함)
97
  const MODEL_URL = "https://teachablemachine.withgoogle.com/models/IMOO3vPNb/";
98
 
99
+ // 클래슀 이름 (Teachable Machineμ—μ„œ λ§Œλ“  κ·ΈλŒ€λ‘œ)
100
  const CLASS_HOT = "λ”μšΈλ•Œ";
101
  const CLASS_COLD = "μΆ”μšΈλ•Œ";
102
  const CLASS_NORMAL = "ν‰μƒμ‹œ";
 
109
  const predsEl = document.getElementById("preds");
110
  const statusEl = document.getElementById("status");
111
  const lastSayEl = document.getElementById("lastSay");
112
+ const errEl = document.getElementById("err");
113
 
114
  const btnStart = document.getElementById("btnStart");
115
  const btnStop = document.getElementById("btnStop");
 
117
  const threshVal = document.getElementById("threshVal");
118
  const voiceOn = document.getElementById("voiceOn");
119
  const speakNormal = document.getElementById("speakNormal");
120
+
121
+ const canvas = document.getElementById("canvas");
122
+ const ctx = canvas.getContext("2d");
123
 
124
  thresh.addEventListener("input", () => threshVal.textContent = (+thresh.value).toFixed(2));
125
 
 
160
  const pNormal = getProb(predictions, CLASS_NORMAL);
161
 
162
  const arr = [
163
+ {state:"hot", text:"λ”μ›Œμš”", p:pHot},
164
+ {state:"cold", text:"μΆ”μ›Œμš”", p:pCold},
165
+ {state:"normal", text:"ν‰μƒμ‹œ", p:pNormal},
166
+ ].sort((a,b)=>b.p-a.p);
167
 
168
  const top = arr[0];
169
+ if (top.p < threshold) return {state:"normal", text:"ν‰μƒμ‹œ"};
 
 
170
  return top;
171
  }
172
 
173
+ function drawWebcamToCanvas() {
174
+ // webcam.canvasλŠ” tmPoseκ°€ λ‚΄λΆ€μ μœΌλ‘œ μ—…λ°μ΄νŠΈν•˜λŠ” μΊ”λ²„μŠ€
175
+ ctx.drawImage(webcam.canvas, 0, 0, canvas.width, canvas.height);
176
+ }
177
+
178
  async function loop() {
179
  webcam.update();
180
+
181
+ // Pose μΆ”μ • + λΆ„λ₯˜
182
+ const { posenetOutput } = await model.estimatePose(webcam.canvas);
183
+ const predictions = await model.predict(posenetOutput);
184
+
185
+ drawWebcamToCanvas();
186
 
187
  predsEl.textContent = predictions
188
  .map(p => `${p.className}: ${p.probability.toFixed(2)}`)
189
  .join("\n");
190
 
191
+ const d = decideState(predictions, +thresh.value);
 
192
 
193
  if (d.state !== lastState) {
194
  lastState = d.state;
 
195
  if (d.state === "hot") {
196
  setLamp("hot", "λ”μ›Œμš”");
197
  speak("λ”μ›Œμš”");
 
208
  }
209
 
210
  async function start() {
211
+ errEl.textContent = "";
212
  btnStart.disabled = true;
213
  setLamp("normal", "λ‘œλ”©μ€‘...");
214
 
215
+ try {
216
+ const modelURL = MODEL_URL + "model.json";
217
+ const metadataURL = MODEL_URL + "metadata.json";
218
+ model = await tmPose.load(modelURL, metadataURL);
219
+
220
+ // Webcam (Pose)
221
+ webcam = new tmPose.Webcam(420, 315, true);
222
+ await webcam.setup(); // μ—¬κΈ°μ„œ κΆŒν•œ λ§‰νžˆλ©΄ 멈좀
223
+ await webcam.play();
224
+
225
+ btnStop.disabled = false;
226
+ setLamp("normal", "뢄석쀑");
227
+ lastState = "normal";
228
+ rafId = requestAnimationFrame(loop);
229
+
230
+ } catch (e) {
231
+ // μ‹€νŒ¨ν•˜λ©΄ λ²„νŠΌ λ‹€μ‹œ 살리고 μ—λŸ¬ ν‘œμ‹œ
232
+ btnStart.disabled = false;
233
+ btnStop.disabled = true;
234
+ setLamp("normal", "μ‹€νŒ¨");
235
+ errEl.textContent =
236
+ "❌ μ‹œμž‘ μ‹€νŒ¨\n\n" +
237
+ (e?.message ? e.message : String(e)) +
238
+ "\n\n- μ£Όμ†Œμ°½ μžλ¬Όμ‡ μ—μ„œ 카메라 ν—ˆμš©ν–ˆλŠ”μ§€ 확인\n- λ‹€λ₯Έ νƒ­/앱이 카메라λ₯Ό 점유 쀑이면 μ’…λ£Œ\n- 크둬(PC)μ—μ„œ μž¬μ‹œλ„ ꢌμž₯";
239
+ console.error(e);
240
+ }
241
  }
242
 
243
  function stop() {