icekun commited on
Commit
f4c8dd1
·
verified ·
1 Parent(s): ff5d779

Update index.html

Browse files
Files changed (1) hide show
  1. index.html +134 -507
index.html CHANGED
@@ -3,521 +3,148 @@
3
  <head>
4
  <meta charset="UTF-8">
5
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
- <title>TinyModel MobileNetV4 Predictor</title>
7
- <link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&family=Noto+Sans+JP:wght@400;500;700&display=swap" rel="stylesheet">
8
- <script src="https://cdn.jsdelivr.net/npm/onnxruntime-web/dist/ort.min.js"></script>
9
- <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css">
10
-
11
- <style>
12
- :root {
13
- --primary: #3b82f6;
14
- --primary-hover: #2563eb;
15
- --bg-color: #f8fafc;
16
- --card-bg: #ffffff;
17
- --text-main: #1e293b;
18
- --text-muted: #64748b;
19
- --border-color: #e2e8f0;
20
- --success: #10b981;
21
- --danger: #ef4444;
22
- --shadow-sm: 0 1px 2px 0 rgb(0 0 0 / 0.05);
23
- --shadow-md: 0 4px 6px -1px rgb(0 0 0 / 0.1), 0 2px 4px -2px rgb(0 0 0 / 0.1);
24
- --shadow-lg: 0 10px 15px -3px rgb(0 0 0 / 0.1), 0 4px 6px -4px rgb(0 0 0 / 0.1);
25
- --radius-md: 0.5rem;
26
- --radius-lg: 0.75rem;
27
- }
28
-
29
- * {
30
- box-sizing: border-box;
31
- margin: 0;
32
- padding: 0;
33
- }
34
-
35
- body {
36
- font-family: 'Inter', 'Noto Sans JP', sans-serif;
37
- background-color: var(--bg-color);
38
- color: var(--text-main);
39
- line-height: 1.5;
40
- display: flex;
41
- justify-content: center;
42
- padding: 2rem 1rem;
43
- min-height: 100vh;
44
- }
45
-
46
- .container {
47
- width: 100%;
48
- max-width: 640px;
49
- }
50
-
51
- .header {
52
- text-align: center;
53
- margin-bottom: 2rem;
54
- }
55
-
56
- .header h1 {
57
- font-size: 1.875rem;
58
- font-weight: 700;
59
- color: var(--text-main);
60
- display: flex;
61
- align-items: center;
62
- justify-content: center;
63
- gap: 0.75rem;
64
- }
65
-
66
- .header p {
67
- color: var(--text-muted);
68
- margin-top: 0.5rem;
69
- font-size: 0.95rem;
70
- }
71
-
72
- .card {
73
- background: var(--card-bg);
74
- border-radius: var(--radius-lg);
75
- box-shadow: var(--shadow-md);
76
- padding: 2rem;
77
- margin-bottom: 1.5rem;
78
- }
79
-
80
- .form-group {
81
- margin-bottom: 1.5rem;
82
- }
83
-
84
- .form-label {
85
- display: block;
86
- font-weight: 600;
87
- margin-bottom: 0.5rem;
88
- font-size: 0.9rem;
89
- color: var(--text-main);
90
- }
91
-
92
- .custom-select {
93
- width: 100%;
94
- padding: 0.75rem 1rem;
95
- border: 1px solid var(--border-color);
96
- border-radius: var(--radius-md);
97
- background-color: var(--bg-color);
98
- font-family: inherit;
99
- font-size: 0.95rem;
100
- color: var(--text-main);
101
- appearance: none;
102
- cursor: pointer;
103
- transition: all 0.2s;
104
- background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' fill='none' viewBox='0 0 24 24' stroke='%2364748b'%3E%3Cpath stroke-linecap='round' stroke-linejoin='round' stroke-width='2' d='M19 9l-7 7-7-7'%3E%3C/path%3E%3C/svg%3E");
105
- background-repeat: no-repeat;
106
- background-position: right 1rem center;
107
- background-size: 1.2em;
108
- }
109
-
110
- .custom-select:focus {
111
- outline: none;
112
- border-color: var(--primary);
113
- box-shadow: 0 0 0 3px rgba(59, 130, 246, 0.2);
114
- }
115
-
116
- .dropzone {
117
- border: 2px dashed var(--border-color);
118
- border-radius: var(--radius-lg);
119
- padding: 3rem 2rem;
120
- text-align: center;
121
- cursor: pointer;
122
- transition: all 0.2s ease;
123
- background-color: var(--bg-color);
124
- position: relative;
125
- overflow: hidden;
126
- }
127
-
128
- .dropzone:hover, .dropzone.dragover {
129
- border-color: var(--primary);
130
- background-color: #eff6ff;
131
- }
132
-
133
- .dropzone-content {
134
- pointer-events: none;
135
- }
136
-
137
- .dropzone i {
138
- font-size: 3rem;
139
- color: var(--primary);
140
- margin-bottom: 1rem;
141
- }
142
-
143
- .dropzone h3 {
144
- font-size: 1.1rem;
145
- font-weight: 600;
146
- margin-bottom: 0.25rem;
147
- }
148
-
149
- .dropzone p {
150
- font-size: 0.85rem;
151
- color: var(--text-muted);
152
- }
153
-
154
- #fileInput {
155
- position: absolute;
156
- top: 0;
157
- left: 0;
158
- width: 100%;
159
- height: 100%;
160
- opacity: 0;
161
- cursor: pointer;
162
- }
163
-
164
- .preview-container {
165
- display: none;
166
- margin-top: 1.5rem;
167
- text-align: center;
168
- }
169
-
170
- .preview-container img {
171
- max-width: 100%;
172
- max-height: 300px;
173
- border-radius: var(--radius-md);
174
- box-shadow: var(--shadow-sm);
175
- object-fit: contain;
176
- background-color: #000;
177
- }
178
-
179
- .btn {
180
- display: block;
181
- width: 100%;
182
- padding: 0.875rem;
183
- background-color: var(--primary);
184
- color: white;
185
- border: none;
186
- border-radius: var(--radius-md);
187
- font-size: 1rem;
188
- font-weight: 600;
189
- cursor: pointer;
190
- transition: background-color 0.2s, transform 0.1s;
191
- display: flex;
192
- align-items: center;
193
- justify-content: center;
194
- gap: 0.5rem;
195
- }
196
-
197
- .btn:hover:not(:disabled) {
198
- background-color: var(--primary-hover);
199
- }
200
-
201
- .btn:active:not(:disabled) {
202
- transform: scale(0.98);
203
- }
204
-
205
- .btn:disabled {
206
- background-color: #94a3b8;
207
- cursor: not-allowed;
208
- }
209
-
210
- .status-bar {
211
- margin-top: 1.5rem;
212
- padding: 1rem;
213
- border-radius: var(--radius-md);
214
- background-color: var(--bg-color);
215
- display: flex;
216
- align-items: center;
217
- gap: 0.75rem;
218
- font-size: 0.95rem;
219
- font-weight: 500;
220
- }
221
-
222
- .status-icon {
223
- font-size: 1.25rem;
224
- }
225
- .status-loading { color: #f59e0b; }
226
- .status-ready { color: var(--success); }
227
- .status-error { color: var(--danger); }
228
-
229
- .results-container {
230
- display: none;
231
- margin-top: 1.5rem;
232
- padding-top: 1.5rem;
233
- border-top: 1px solid var(--border-color);
234
- }
235
-
236
- .results-title {
237
- font-weight: 700;
238
- font-size: 1.1rem;
239
- margin-bottom: 1rem;
240
- }
241
-
242
- .result-bar-group {
243
- margin-bottom: 1rem;
244
- }
245
-
246
- .result-label {
247
- display: flex;
248
- justify-content: space-between;
249
- font-size: 0.9rem;
250
- font-weight: 600;
251
- margin-bottom: 0.4rem;
252
- }
253
 
254
- .progress-bg {
255
- height: 0.75rem;
256
- background-color: var(--border-color);
257
- border-radius: 999px;
258
- overflow: hidden;
259
- }
260
 
261
- .progress-fill {
262
- height: 100%;
263
- border-radius: 999px;
264
- transition: width 0.6s cubic-bezier(0.4, 0, 0.2, 1);
265
- width: 0%;
266
- }
267
-
268
- .fill-need { background-color: var(--primary); }
269
- .fill-trash { background-color: var(--danger); }
270
 
271
- .fa-spinner {
272
- animation: spin 1s linear infinite;
273
- }
274
- @keyframes spin {
275
- 0% { transform: rotate(0deg); }
276
- 100% { transform: rotate(360deg); }
277
  }
278
  </style>
279
  </head>
280
  <body>
281
 
282
- <div class="container">
283
- <div class="header">
284
- <h1><i class="fa-solid fa-microchip" style="color: var(--primary);"></i> TinyModel Predictor</h1>
285
- <p>MobileNetV4 ブラウザ完結型・画像分類AI</p>
286
- </div>
287
-
288
- <div class="card">
289
- <!-- モデル選択 -->
290
- <div class="form-group">
291
- <label class="form-label" for="modelSelect"><i class="fa-solid fa-cube"></i> 使用するモデル (.onnx)</label>
292
- <select id="modelSelect" class="custom-select"></select>
293
- </div>
294
-
295
- <!-- 画像アップロード -->
296
- <div class="form-group">
297
- <label class="form-label"><i class="fa-regular fa-image"></i> 判定する画像</label>
298
- <div class="dropzone" id="dropzone">
299
- <input type="file" id="fileInput" accept="image/*">
300
- <div class="dropzone-content">
301
- <i class="fa-solid fa-cloud-arrow-up"></i>
302
- <h3>クリックまたはドラッグ&ドロップ</h3>
303
- <p>PNG, JPG, JPEG (Max 10MB)</p>
304
- </div>
305
- </div>
306
-
307
- <div class="preview-container" id="previewContainer">
308
- <img id="preview" src="" alt="Image Preview">
309
- </div>
310
- </div>
311
-
312
- <!-- 実行ボタン -->
313
- <button id="runBtn" class="btn" disabled>
314
- <i class="fa-solid fa-bolt"></i> 判定を実行する
315
- </button>
316
-
317
- <!-- ステータス表示 -->
318
- <div class="status-bar" id="statusBar">
319
- <i class="fa-solid fa-spinner status-icon status-loading" id="statusIcon"></i>
320
- <span id="statusText">システムを初期化中...</span>
321
- </div>
322
-
323
- <!-- 結果表示 -->
324
- <div class="results-container" id="resultsContainer">
325
- <h3 class="results-title"><i class="fa-solid fa-chart-simple"></i> 判定結果</h3>
326
-
327
- <div class="result-bar-group">
328
- <div class="result-label">
329
- <span><i class="fa-solid fa-box-open" style="color: var(--primary);"></i> need (必要なもの)</span>
330
- <span id="prob-need">0.0%</span>
331
- </div>
332
- <div class="progress-bg">
333
- <div class="progress-fill fill-need" id="bar-need"></div>
334
- </div>
335
- </div>
336
-
337
- <div class="result-bar-group">
338
- <div class="result-label">
339
- <span><i class="fa-solid fa-trash-can" style="color: var(--danger);"></i> trash (ゴミ)</span>
340
- <span id="prob-trash">0.0%</span>
341
- </div>
342
- <div class="progress-bg">
343
- <div class="progress-fill fill-trash" id="bar-trash"></div>
344
- </div>
345
- </div>
346
- </div>
347
- </div>
348
- </div>
349
-
350
- <script>
351
- const CLASS_NAMES = ["need", "trash"];
352
- // Python変数展開
353
- const models = ["best_model.onnx", "model_epoch_1_acc_0.9849.onnx", "model_epoch_2_acc_0.9885.onnx", "model_epoch_3_acc_0.9865.onnx", "model_epoch_4_acc_0.9874.onnx", "model_epoch_5_acc_0.9877.onnx", "model_epoch_6_acc_0.9901.onnx", "model_epoch_7_acc_0.9908.onnx", "model_epoch_8_acc_0.9912.onnx", "model_epoch_9_acc_0.9909.onnx", "model_epoch_10_acc_0.9411.onnx", "model_epoch_11_acc_0.9929.onnx", "model_epoch_12_acc_0.9922.onnx", "model_epoch_13_acc_0.9918.onnx", "model_epoch_14_acc_0.9922.onnx", "model_epoch_15_acc_0.9925.onnx", "model_epoch_16_acc_0.9906.onnx", "model_epoch_17_acc_0.9908.onnx", "model_epoch_18_acc_0.9926.onnx", "model_epoch_19_acc_0.9916.onnx", "model_epoch_20_acc_0.9915.onnx"];
354
- let session = null;
355
- let isModelReady = false;
356
-
357
- const modelSelect = document.getElementById('modelSelect');
358
- const fileInput = document.getElementById('fileInput');
359
- const dropzone = document.getElementById('dropzone');
360
- const previewContainer = document.getElementById('previewContainer');
361
- const previewImg = document.getElementById('preview');
362
- const runBtn = document.getElementById('runBtn');
363
- const statusIcon = document.getElementById('statusIcon');
364
- const statusText = document.getElementById('statusText');
365
- const resultsContainer = document.getElementById('resultsContainer');
366
-
367
- function setStatus(type, msg) {
368
- statusText.innerText = msg;
369
- statusIcon.className = 'fa-solid status-icon';
370
- if (type === 'loading') statusIcon.classList.add('fa-spinner', 'status-loading');
371
- if (type === 'ready') statusIcon.classList.add('fa-circle-check', 'status-ready');
372
- if (type === 'error') statusIcon.classList.add('fa-triangle-exclamation', 'status-error');
373
- }
374
-
375
- function initSelect() {
376
- models.forEach(model => {
377
- const option = document.createElement('option');
378
- option.value = model;
379
- option.textContent = model;
380
- modelSelect.appendChild(option);
381
- });
382
-
383
- modelSelect.addEventListener('change', (e) => {
384
- loadModel(e.target.value);
385
- });
386
- }
387
-
388
- async function loadModel(modelName) {
389
- isModelReady = false;
390
- runBtn.disabled = true;
391
- setStatus('loading', modelName + ' を読み込み中...');
392
-
393
- try {
394
- if (session) {
395
- await session.release();
396
- session = null;
397
- }
398
-
399
- session = await ort.InferenceSession.create('./' + modelName);
400
- isModelReady = true;
401
-
402
- setStatus('ready', modelName + ' のロードが完了しました。');
403
-
404
- if (previewImg.src && previewImg.src !== window.location.href) {
405
- runBtn.disabled = false;
406
- }
407
- } catch (e) {
408
- setStatus('error', 'モデル読込エラー: ' + e.message);
409
- console.error(e);
410
- }
411
- }
412
-
413
- function handleFile(file) {
414
- if (!file || !file.type.startsWith('image/')) return;
415
-
416
- const reader = new FileReader();
417
- reader.onload = (e) => {
418
- previewImg.src = e.target.result;
419
- previewContainer.style.display = 'block';
420
- resultsContainer.style.display = 'none';
421
-
422
- if (isModelReady) {
423
- runBtn.disabled = false;
424
- }
425
- };
426
- reader.readAsDataURL(file);
427
- }
428
-
429
- fileInput.addEventListener('change', (e) => handleFile(e.target.files[0]));
430
-
431
- dropzone.addEventListener('dragover', (e) => {
432
- e.preventDefault();
433
- dropzone.classList.add('dragover');
434
- });
435
-
436
- dropzone.addEventListener('dragleave', (e) => {
437
- e.preventDefault();
438
- dropzone.classList.remove('dragover');
439
- });
440
-
441
- dropzone.addEventListener('drop', (e) => {
442
- e.preventDefault();
443
- dropzone.classList.remove('dragover');
444
- handleFile(e.dataTransfer.files[0]);
445
- });
446
-
447
- async function preprocess(imgElement) {
448
- const canvas = document.createElement('canvas');
449
- canvas.width = 224;
450
- canvas.height = 224;
451
- const ctx = canvas.getContext('2d');
452
-
453
- ctx.drawImage(imgElement, 0, 0, 224, 224);
454
- const imgData = ctx.getImageData(0, 0, 224, 224);
455
- const data = imgData.data;
456
-
457
- const mean = [0.485, 0.456, 0.406];
458
- const std = [0.229, 0.224, 0.225];
459
- const floatData = new Float32Array(3 * 224 * 224);
460
-
461
- for (let i = 0; i < 224 * 224; i++) {
462
- const r = data[i * 4] / 255.0;
463
- const g = data[i * 4 + 1] / 255.0;
464
- const b = data[i * 4 + 2] / 255.0;
465
-
466
- floatData[i] = (r - mean[0]) / std[0];
467
- floatData[224 * 224 + i] = (g - mean[1]) / std[1];
468
- floatData[2 * 224 * 224 + i] = (b - mean[2]) / std[2];
469
- }
470
-
471
- return new ort.Tensor('float32', floatData, [1, 3, 224, 224]);
472
- }
473
-
474
- runBtn.addEventListener('click', async () => {
475
- if (!isModelReady || !session || !previewImg.src) return;
476
-
477
- runBtn.disabled = true;
478
- setStatus('loading', '推論を実行中...');
479
-
480
- try {
481
- const inputTensor = await preprocess(previewImg);
482
- const feeds = {};
483
- feeds[session.inputNames[0]] = inputTensor;
484
-
485
- const start = performance.now();
486
- const results = await session.run(feeds);
487
- const end = performance.now();
488
-
489
- const output = results[session.outputNames[0]].data;
490
-
491
- const exp0 = Math.exp(output[0]);
492
- const exp1 = Math.exp(output[1]);
493
- const sum = exp0 + exp1;
494
- const probNeed = (exp0 / sum * 100);
495
- const probTrash = (exp1 / sum * 100);
496
-
497
- document.getElementById('prob-need').innerText = probNeed.toFixed(2) + '%';
498
- document.getElementById('bar-need').style.width = probNeed + '%';
499
-
500
- document.getElementById('prob-trash').innerText = probTrash.toFixed(2) + '%';
501
- document.getElementById('bar-trash').style.width = probTrash + '%';
502
-
503
- resultsContainer.style.display = 'block';
504
- setStatus('ready', '判定完了 (' + (end - start).toFixed(1) + 'ms)');
505
-
506
- } catch (e) {
507
- setStatus('error', '推論エラー: ' + e.message);
508
- console.error(e);
509
- } finally {
510
- runBtn.disabled = false;
511
- }
512
- });
513
-
514
- window.onload = () => {
515
- initSelect();
516
- if (models.length > 0) {
517
- loadModel(models[0]);
518
- }
519
- };
520
- </script>
521
 
522
  </body>
523
  </html>
 
3
  <head>
4
  <meta charset="UTF-8">
5
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
+ <title>TinyModel MobileNetV4 Predictor (Gradio Lite)</title>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
7
 
8
+ <!-- Gradio Lite (WebAssembly版Gradio) -->
9
+ <script type="module" src="https://cdn.jsdelivr.net/npm/@gradio/lite/dist/lite.js"></script>
10
+ <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@gradio/lite/dist/lite.css" />
 
 
 
11
 
12
+ <!-- ONNX Runtime Web -->
13
+ <script src="https://cdn.jsdelivr.net/npm/onnxruntime-web/dist/ort.min.js"></script>
 
 
 
 
 
 
 
14
 
15
+ <style>
16
+ body {
17
+ margin: 0;
18
+ padding: 20px;
19
+ background-color: #f8fafc;
20
+ font-family: sans-serif;
21
  }
22
  </style>
23
  </head>
24
  <body>
25
 
26
+ <!-- Pyodideで動くGradioアプリ本体 -->
27
+ <gradio-lite py-packages="pillow, numpy">
28
+ <gradio-file name="app.py">
29
+ import gradio as gr
30
+ import numpy as np
31
+ from PIL import Image
32
+ import js
33
+
34
+ # 21個のONNXモデルリスト
35
+ MODEL_FILES = [
36
+ "best_model.onnx",
37
+ "model_epoch_1_acc_0.9849.onnx",
38
+ "model_epoch_2_acc_0.9885.onnx",
39
+ "model_epoch_3_acc_0.9865.onnx",
40
+ "model_epoch_4_acc_0.9874.onnx",
41
+ "model_epoch_5_acc_0.9877.onnx",
42
+ "model_epoch_6_acc_0.9901.onnx",
43
+ "model_epoch_7_acc_0.9908.onnx",
44
+ "model_epoch_8_acc_0.9912.onnx",
45
+ "model_epoch_9_acc_0.9909.onnx",
46
+ "model_epoch_10_acc_0.9411.onnx",
47
+ "model_epoch_11_acc_0.9929.onnx",
48
+ "model_epoch_12_acc_0.9922.onnx",
49
+ "model_epoch_13_acc_0.9918.onnx",
50
+ "model_epoch_14_acc_0.9922.onnx",
51
+ "model_epoch_15_acc_0.9925.onnx",
52
+ "model_epoch_16_acc_0.9906.onnx",
53
+ "model_epoch_17_acc_0.9908.onnx",
54
+ "model_epoch_18_acc_0.9926.onnx",
55
+ "model_epoch_19_acc_0.9916.onnx",
56
+ "model_epoch_20_acc_0.9915.onnx"
57
+ ]
58
+
59
+ CLASS_NAMES = ["need", "trash"]
60
+
61
+ # ONNXセッションのキャッシュ管理
62
+ current_session = None
63
+ current_model_name = None
64
+
65
+ async def get_session(model_name):
66
+ global current_session, current_model_name
67
+ if current_session is None or current_model_name != model_name:
68
+ # ブラウザの ort.InferenceSession をPythonから呼び出し
69
+ promise = js.ort.InferenceSession.create(f"./{model_name}")
70
+ current_session = await promise
71
+ current_model_name = model_name
72
+ return current_session
73
+
74
+ async def predict(image, model_name):
75
+ if image is None:
76
+ return "画像をアップロードしてください。"
77
+
78
+ try:
79
+ # 1. 画像の前処理 (224x224 リサイズ & ImageNet正規化)
80
+ img = Image.fromarray(image).convert("RGB").resize((224, 224))
81
+ img_np = np.array(img, dtype=np.float32) / 255.0
82
+
83
+ mean = np.array([0.485, 0.456, 0.406], dtype=np.float32)
84
+ std = np.array([0.229, 0.224, 0.225], dtype=np.float32)
85
+ img_np = (img_np - mean) / std
86
+
87
+ # HWC -> NCHW (1, 3, 224, 224)
88
+ img_np = img_np.transpose(2, 0, 1)
89
+ img_np = np.expand_dims(img_np, axis=0)
90
+
91
+ # 2. PythonのNumPy配列をJSのTensorへ変換
92
+ flat_data = img_np.flatten().tolist()
93
+ js_data = js.Float32Array.new(flat_data)
94
+ js_shape = js.Array.new(1, 3, 224, 224)
95
+ input_tensor = js.ort.Tensor.new('float32', js_data, js_shape)
96
+
97
+ # 3. 推論実行
98
+ session = await get_session(model_name)
99
+ input_name = session.inputNames[0]
100
+ output_name = session.outputNames[0]
101
+
102
+ feeds = js.Object.new()
103
+ js.Reflect.set(feeds, input_name, input_tensor)
104
+
105
+ results = await session.run(feeds)
106
+ output_data = js.Reflect.get(results, output_name).data
107
+
108
+ out_list = [float(output_data[0]), float(output_data[1])]
109
+
110
+ # 4. Softmax で確率計算
111
+ exp_vals = np.exp(out_list - np.max(out_list))
112
+ probs = exp_vals / np.sum(exp_vals)
113
+
114
+ return {
115
+ CLASS_NAMES[0]: float(probs[0]),
116
+ CLASS_NAMES[1]: float(probs[1])
117
+ }
118
+ except Exception as e:
119
+ return f"エラー: {str(e)}"
120
+
121
+ # Gradio Blocks UI 構築
122
+ with gr.Blocks(title="TinyModel Predictor") as demo:
123
+ gr.Markdown("## 🗑️ TinyModel (MobileNetV4) 判定 (Gradio Lite)")
124
+ gr.Markdown("Wasm(Pyodide)技術を使用し、サーバーなし・ブラウザのみで判定を行うGradioアプリです。")
125
+
126
+ with gr.Row():
127
+ with gr.Column():
128
+ input_image = gr.Image(label="入力画像")
129
+ model_selector = gr.Dropdown(
130
+ choices=MODEL_FILES,
131
+ value=MODEL_FILES[0],
132
+ label="使用するモデル重み (.onnx)"
133
+ )
134
+ submit_btn = gr.Button("判定実行", variant="primary")
135
+
136
+ with gr.Column():
137
+ output_label = gr.Label(num_top_classes=2, label="判定結果 (確率)")
138
+
139
+ submit_btn.click(
140
+ fn=predict,
141
+ inputs=[input_image, model_selector],
142
+ outputs=output_label
143
+ )
144
+
145
+ demo.launch()
146
+ </gradio-file>
147
+ </gradio-lite>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
148
 
149
  </body>
150
  </html>