Anoderb commited on
Commit
6536045
Β·
verified Β·
1 Parent(s): 2acf4dd

Upload index.html with huggingface_hub

Browse files
Files changed (1) hide show
  1. index.html +40 -46
index.html CHANGED
@@ -4,41 +4,35 @@
4
  <meta charset="UTF-8">
5
  <meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no">
6
  <title>SKU Scanner</title>
7
- <!-- TensorFlow.js TFLite WASM backend -->
8
  <script src="https://cdn.jsdelivr.net/npm/@tensorflow/tfjs-core@4.22.0/dist/tf-core.min.js"></script>
9
- <script src="https://cdn.jsdelivr.net/npm/@tensorflow/tfjs-backend-wasm@4.22.0/dist/tf-backend-wasm.min.js"></script>
10
- <script src="https://cdn.jsdelivr.net/npm/@tensorflow/tfjs-tflite@0.0.1/dist/tf-tflite.min.js"></script>
 
11
  <style>
12
  * { box-sizing: border-box; margin: 0; padding: 0; }
13
  body { font-family: -apple-system, 'Segoe UI', Roboto, sans-serif;
14
  background: #0f172a; color: #e2e8f0; min-height: 100dvh; display: flex; justify-content: center; }
15
  .container { max-width: 420px; width: 100%; padding: 16px; display: flex; flex-direction: column; gap: 12px; }
16
-
17
- /* Header */
18
  h1 { font-size: 20px; text-align: center; color: #38bdf8; font-weight: 700; }
19
  .subtitle { text-align: center; font-size: 13px; color: #94a3b8; margin-top: -8px; }
20
 
21
- /* Status badge */
22
  #status { text-align: center; font-size: 12px; padding: 4px 12px; border-radius: 20px;
23
- background: #1e293b; color: #94a3b8; margin-bottom: 4px;
24
- min-height: 22px; transition: all .3s; }
25
  #status.loading { color: #fbbf24; }
26
  #status.ready { color: #4ade80; background: #14532d; }
27
  #status.error { color: #f87171; background: #451a1a; }
28
 
29
- /* Camera */
30
  .cam-wrap { position: relative; width: 100%; aspect-ratio: 1/1; background: #1e293b;
31
  border-radius: 12px; overflow: hidden; }
32
  .cam-wrap video, .cam-wrap canvas { position: absolute; top: 0; left: 0;
33
  width: 100%; height: 100%; object-fit: cover; }
34
  .cam-wrap canvas { display: none; }
35
-
36
  .overlay { position: absolute; top: 0; left: 0; width: 100%; height: 100%;
37
  border: 3px dashed #38bdf8; border-radius: 12px; pointer-events: none; opacity: 0.5; }
38
  .placeholder { position: absolute; top: 50%; left: 50%; transform: translate(-50%,-50%);
39
  text-align: center; color: #64748b; pointer-events: none; }
40
 
41
- /* Controls */
42
  .controls { display: flex; gap: 8px; flex-wrap: wrap; }
43
  .controls button, #file-label {
44
  flex: 1; min-width: 90px; padding: 12px; border: none; border-radius: 8px;
@@ -46,14 +40,11 @@
46
  }
47
  #btn-cap { background: #2563eb; color: #fff; }
48
  #btn-cap:disabled { background: #334155; color: #64748b; cursor: wait; }
49
- #btn-cap:active { background: #1d4ed8; }
50
  #btn-pred { background: #16a34a; color: #fff; display: none; }
51
- #btn-pred:active { background: #15803d; }
52
  #btn-retake { background: #475569; color: #fff; display: none; }
53
  #file-label { background: #334155; color: #cbd5e1; display: inline-block; }
54
  #file-input { display: none; }
55
 
56
- /* Result */
57
  #result { display: none; background: #1e293b; border-radius: 12px; padding: 16px; }
58
  #result h3 { font-size: 14px; color: #94a3b8; margin-bottom: 8px; }
59
  .pred-row { display: flex; align-items: center; gap: 8px; margin-bottom: 6px; }
@@ -72,7 +63,6 @@
72
  .toast { position: fixed; bottom: 20px; left: 50%; transform: translateX(-50%);
73
  background: #ef4444; color: #fff; padding: 10px 20px; border-radius: 8px;
74
  font-size: 13px; display: none; z-index: 100; white-space: nowrap; }
75
-
76
  .time { text-align: center; font-size: 11px; color: #64748b; margin-top: -4px; }
77
  </style>
78
  </head>
@@ -93,7 +83,7 @@
93
  </div>
94
 
95
  <div class="controls">
96
- <button id="btn-cap" disabled>πŸ“Έ Ambil</button>
97
  <button id="btn-pred" onclick="predict()">πŸ” Prediksi</button>
98
  <button id="btn-retake" onclick="retake()">πŸ”„ Ulang</button>
99
  <label id="file-label" for="file-input">πŸ“ Upload</label>
@@ -115,6 +105,7 @@ let model = null;
115
  let stream = null;
116
  let captured = false;
117
  let classNames = [];
 
118
 
119
  const video = document.getElementById('video');
120
  const canvas = document.getElementById('canvas');
@@ -131,24 +122,29 @@ const fileInput = document.getElementById('file-input');
131
  const placeholder = document.getElementById('placeholder');
132
 
133
  // ── SET WASM PATH ──
134
- async function initTFJS() {
135
- const wasmBase = 'https://cdn.jsdelivr.net/npm/@tensorflow/tfjs-tflite@0.0.1/dist/tflite/';
136
- await tflite.setWasmPath(wasmBase);
137
- await tf.ready();
 
138
  }
139
 
140
  // ── LOAD MODEL ──
141
  async function loadModel() {
 
 
 
142
  try {
143
  model = await tflite.loadTFLiteModel('mobilenetv4_cbam_quantized.tflite');
144
- console.log('Model loaded:', model);
145
  statusEl.textContent = 'βœ“ Model siap';
146
  statusEl.className = 'ready';
147
  btnCap.disabled = false;
 
148
  } catch (e) {
149
- statusEl.textContent = 'βœ— Gagal load model: ' + e.message;
150
  statusEl.className = 'error';
151
- showToast('Gagal load model: ' + e.message);
 
152
  throw e;
153
  }
154
  }
@@ -170,15 +166,14 @@ async function startCam() {
170
  await video.play();
171
  placeholder.style.display = 'none';
172
  } catch (e) {
173
- placeholder.innerHTML = '<div style="color:#ef4444">❌ Kamera gak bisa diakses<br><small style="font-size:12px">Gunakan Upload saja</small></div>';
174
- btnCap.disabled = true;
175
  }
176
  }
177
 
178
  // ── CAPTURE ──
179
  function capture() {
180
- canvas.width = video.videoWidth || 480;
181
- canvas.height = video.videoHeight || 480;
182
  ctx.drawImage(video, 0, 0, canvas.width, canvas.height);
183
  canvas.style.display = 'block';
184
  video.style.display = 'none';
@@ -206,7 +201,7 @@ async function predict() {
206
  if (captured) {
207
  imageData = canvas;
208
  } else if (fileInput.files.length > 0) {
209
- const img = await loadImage(fileInput.files[0]);
210
  canvas.width = img.naturalWidth;
211
  canvas.height = img.naturalHeight;
212
  ctx.drawImage(img, 0, 0);
@@ -221,41 +216,44 @@ async function predict() {
221
  return;
222
  }
223
 
224
- // Show loading
225
  spinner.style.display = 'block';
226
  predsDiv.innerHTML = '';
227
  timeEl.textContent = '';
 
228
 
229
- // Run inference asynchronously (yield to render)
230
  await tf.nextFrame();
231
-
232
  const t0 = performance.now();
 
233
  try {
234
- // Resize to 224x224
235
  const input = tf.tidy(() => {
236
  const img = tf.browser.fromPixels(imageData);
237
  const resized = tf.image.resizeBilinear(img, [224, 224]);
238
- return resized.expandDims(0).toFloat(); // raw [0,255]
239
  });
240
 
241
  const output = model.predict(input);
242
  const scores = output.dataSync();
243
- const time = (performance.now() - t0).toFixed(0);
244
 
245
- // Cleanup tensors
246
  input.dispose();
247
  output.dispose();
248
 
249
  spinner.style.display = 'none';
250
  resultDiv.style.display = 'block';
251
 
 
 
 
 
 
252
  // Top 3
253
  const top3 = Array.from(scores)
254
  .map((s, i) => ({ label: classNames[i] || 'class_' + i, score: s }))
255
  .sort((a, b) => b.score - a.score)
256
  .slice(0, 3);
257
 
258
- timeEl.textContent = `⏱ ${time}ms inferensi (client-side WASM)`;
259
 
260
  const maxScore = top3[0].score;
261
  predsDiv.innerHTML = top3.map((p, i) => {
@@ -275,7 +273,7 @@ async function predict() {
275
  }
276
 
277
  // ── HELPERS ──
278
- function loadImage(file) {
279
  return new Promise((resolve, reject) => {
280
  const r = new FileReader();
281
  r.onload = () => {
@@ -296,10 +294,10 @@ function showToast(msg) {
296
  setTimeout(() => t.style.display = 'none', 3000);
297
  }
298
 
299
- // ── EVENT HANDLERS ──
300
  fileInput.addEventListener('change', async (e) => {
301
  if (e.target.files.length === 0) return;
302
- const img = await loadImage(e.target.files[0]);
303
  canvas.width = img.naturalWidth;
304
  canvas.height = img.naturalHeight;
305
  ctx.drawImage(img, 0, 0);
@@ -313,16 +311,12 @@ fileInput.addEventListener('change', async (e) => {
313
  predict();
314
  });
315
 
316
- // Use capture function from button
317
- btnCap.addEventListener('click', capture);
318
-
319
  // ── INIT ──
320
  (async function init() {
321
- statusEl.textContent = '⏳ Initializing TFJS...';
322
  try {
323
- await initTFJS();
324
- statusEl.textContent = '⏳ Loading model (3.5 MB)...';
325
- await Promise.all([loadModel(), loadClassNames(), startCam()]);
326
  } catch (e) {
327
  console.error('Init error:', e);
328
  }
 
4
  <meta charset="UTF-8">
5
  <meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no">
6
  <title>SKU Scanner</title>
7
+ <!-- TensorFlow.js + TFLite WASM backend (correct CDN paths) -->
8
  <script src="https://cdn.jsdelivr.net/npm/@tensorflow/tfjs-core@4.22.0/dist/tf-core.min.js"></script>
9
+ <script src="https://cdn.jsdelivr.net/npm/@tensorflow/tfjs-backend-cpu@4.22.0/dist/tf-backend-cpu.min.js"></script>
10
+ <script src="https://cdn.jsdelivr.net/npm/@tensorflow/tfjs-tflite/dist/tf-tflite.min.js"></script>
11
+
12
  <style>
13
  * { box-sizing: border-box; margin: 0; padding: 0; }
14
  body { font-family: -apple-system, 'Segoe UI', Roboto, sans-serif;
15
  background: #0f172a; color: #e2e8f0; min-height: 100dvh; display: flex; justify-content: center; }
16
  .container { max-width: 420px; width: 100%; padding: 16px; display: flex; flex-direction: column; gap: 12px; }
 
 
17
  h1 { font-size: 20px; text-align: center; color: #38bdf8; font-weight: 700; }
18
  .subtitle { text-align: center; font-size: 13px; color: #94a3b8; margin-top: -8px; }
19
 
 
20
  #status { text-align: center; font-size: 12px; padding: 4px 12px; border-radius: 20px;
21
+ background: #1e293b; color: #94a3b8; min-height: 22px; transition: all .3s; }
 
22
  #status.loading { color: #fbbf24; }
23
  #status.ready { color: #4ade80; background: #14532d; }
24
  #status.error { color: #f87171; background: #451a1a; }
25
 
 
26
  .cam-wrap { position: relative; width: 100%; aspect-ratio: 1/1; background: #1e293b;
27
  border-radius: 12px; overflow: hidden; }
28
  .cam-wrap video, .cam-wrap canvas { position: absolute; top: 0; left: 0;
29
  width: 100%; height: 100%; object-fit: cover; }
30
  .cam-wrap canvas { display: none; }
 
31
  .overlay { position: absolute; top: 0; left: 0; width: 100%; height: 100%;
32
  border: 3px dashed #38bdf8; border-radius: 12px; pointer-events: none; opacity: 0.5; }
33
  .placeholder { position: absolute; top: 50%; left: 50%; transform: translate(-50%,-50%);
34
  text-align: center; color: #64748b; pointer-events: none; }
35
 
 
36
  .controls { display: flex; gap: 8px; flex-wrap: wrap; }
37
  .controls button, #file-label {
38
  flex: 1; min-width: 90px; padding: 12px; border: none; border-radius: 8px;
 
40
  }
41
  #btn-cap { background: #2563eb; color: #fff; }
42
  #btn-cap:disabled { background: #334155; color: #64748b; cursor: wait; }
 
43
  #btn-pred { background: #16a34a; color: #fff; display: none; }
 
44
  #btn-retake { background: #475569; color: #fff; display: none; }
45
  #file-label { background: #334155; color: #cbd5e1; display: inline-block; }
46
  #file-input { display: none; }
47
 
 
48
  #result { display: none; background: #1e293b; border-radius: 12px; padding: 16px; }
49
  #result h3 { font-size: 14px; color: #94a3b8; margin-bottom: 8px; }
50
  .pred-row { display: flex; align-items: center; gap: 8px; margin-bottom: 6px; }
 
63
  .toast { position: fixed; bottom: 20px; left: 50%; transform: translateX(-50%);
64
  background: #ef4444; color: #fff; padding: 10px 20px; border-radius: 8px;
65
  font-size: 13px; display: none; z-index: 100; white-space: nowrap; }
 
66
  .time { text-align: center; font-size: 11px; color: #64748b; margin-top: -4px; }
67
  </style>
68
  </head>
 
83
  </div>
84
 
85
  <div class="controls">
86
+ <button id="btn-cap" disabled onclick="capture()">πŸ“Έ Ambil</button>
87
  <button id="btn-pred" onclick="predict()">πŸ” Prediksi</button>
88
  <button id="btn-retake" onclick="retake()">πŸ”„ Ulang</button>
89
  <label id="file-label" for="file-input">πŸ“ Upload</label>
 
105
  let stream = null;
106
  let captured = false;
107
  let classNames = [];
108
+ let modelLoading = false;
109
 
110
  const video = document.getElementById('video');
111
  const canvas = document.getElementById('canvas');
 
122
  const placeholder = document.getElementById('placeholder');
123
 
124
  // ── SET WASM PATH ──
125
+ function setWasmPath() {
126
+ // The WASM files are in the same CDN directory as tf-tflite.min.js
127
+ // jsDelivr resolves them automatically, but we set explicitly for safety
128
+ const base = 'https://cdn.jsdelivr.net/npm/@tensorflow/tfjs-tflite/dist/tflite/';
129
+ try { tflite.setWasmPath(base); } catch(e) {}
130
  }
131
 
132
  // ── LOAD MODEL ──
133
  async function loadModel() {
134
+ modelLoading = true;
135
+ statusEl.textContent = '⏳ Loading model (3.5 MB)...';
136
+ statusEl.className = 'loading';
137
  try {
138
  model = await tflite.loadTFLiteModel('mobilenetv4_cbam_quantized.tflite');
 
139
  statusEl.textContent = 'βœ“ Model siap';
140
  statusEl.className = 'ready';
141
  btnCap.disabled = false;
142
+ modelLoading = false;
143
  } catch (e) {
144
+ statusEl.textContent = 'βœ— Gagal: ' + e.message;
145
  statusEl.className = 'error';
146
+ showToast('Model gagal dimuat: ' + e.message);
147
+ modelLoading = false;
148
  throw e;
149
  }
150
  }
 
166
  await video.play();
167
  placeholder.style.display = 'none';
168
  } catch (e) {
169
+ placeholder.innerHTML = '<div style="color:#ef4444">❌ Kamera gak bisa diakses<br><small style="font-size:12px">Upload gambar aja</small></div>';
 
170
  }
171
  }
172
 
173
  // ── CAPTURE ──
174
  function capture() {
175
+ canvas.width = video.videoWidth || video.width || 480;
176
+ canvas.height = video.videoHeight || video.height || 480;
177
  ctx.drawImage(video, 0, 0, canvas.width, canvas.height);
178
  canvas.style.display = 'block';
179
  video.style.display = 'none';
 
201
  if (captured) {
202
  imageData = canvas;
203
  } else if (fileInput.files.length > 0) {
204
+ const img = await loadImageFile(fileInput.files[0]);
205
  canvas.width = img.naturalWidth;
206
  canvas.height = img.naturalHeight;
207
  ctx.drawImage(img, 0, 0);
 
216
  return;
217
  }
218
 
 
219
  spinner.style.display = 'block';
220
  predsDiv.innerHTML = '';
221
  timeEl.textContent = '';
222
+ resultDiv.style.display = 'none';
223
 
 
224
  await tf.nextFrame();
 
225
  const t0 = performance.now();
226
+
227
  try {
228
+ // Preprocess: resize to 224x224, keep [0,255] (model handles normalization)
229
  const input = tf.tidy(() => {
230
  const img = tf.browser.fromPixels(imageData);
231
  const resized = tf.image.resizeBilinear(img, [224, 224]);
232
+ return resized.expandDims(0).toFloat();
233
  });
234
 
235
  const output = model.predict(input);
236
  const scores = output.dataSync();
237
+ const elapsed = (performance.now() - t0).toFixed(0);
238
 
 
239
  input.dispose();
240
  output.dispose();
241
 
242
  spinner.style.display = 'none';
243
  resultDiv.style.display = 'block';
244
 
245
+ if (!classNames.length) {
246
+ // Fallback: generate class names
247
+ for (let i = 0; i < scores.length; i++) classNames.push('class_' + i);
248
+ }
249
+
250
  // Top 3
251
  const top3 = Array.from(scores)
252
  .map((s, i) => ({ label: classNames[i] || 'class_' + i, score: s }))
253
  .sort((a, b) => b.score - a.score)
254
  .slice(0, 3);
255
 
256
+ timeEl.textContent = `⏱ ${elapsed}ms inferensi (browser)`;
257
 
258
  const maxScore = top3[0].score;
259
  predsDiv.innerHTML = top3.map((p, i) => {
 
273
  }
274
 
275
  // ── HELPERS ──
276
+ function loadImageFile(file) {
277
  return new Promise((resolve, reject) => {
278
  const r = new FileReader();
279
  r.onload = () => {
 
294
  setTimeout(() => t.style.display = 'none', 3000);
295
  }
296
 
297
+ // ── FILE UPLOAD ──
298
  fileInput.addEventListener('change', async (e) => {
299
  if (e.target.files.length === 0) return;
300
+ const img = await loadImageFile(e.target.files[0]);
301
  canvas.width = img.naturalWidth;
302
  canvas.height = img.naturalHeight;
303
  ctx.drawImage(img, 0, 0);
 
311
  predict();
312
  });
313
 
 
 
 
314
  // ── INIT ──
315
  (async function init() {
 
316
  try {
317
+ setWasmPath();
318
+ await Promise.all([loadModel(), loadClassNames()]);
319
+ startCam();
320
  } catch (e) {
321
  console.error('Init error:', e);
322
  }