Download index.html from icekun/tinymodel: direct link, hf CLI and curl.
- Browser
- Download file 17.3 kB
-
https://huggingface.co/spaces/icekun/tinymodel/resolve/main/index.html
- Command line
-
hf download hf://spaces/icekun/tinymodel/index.html
-
curl -L -o index.html https://huggingface.co/spaces/icekun/tinymodel/resolve/main/index.html
17.3 kB
| <html lang="ja"> | |
| <head> | |
| <meta charset="UTF-8"> | |
| <meta name="viewport" content="width=device-width, initial-scale=1.0"> | |
| <title>その画像、必要?</title> | |
| <!-- ONNX Runtime Web --> | |
| <script src="https://cdn.jsdelivr.net/npm/onnxruntime-web/dist/ort.min.js"></script> | |
| <style> | |
| :root { | |
| --orange-primary: #ff7c00; | |
| --orange-hover: #e56f00; | |
| --bg-main: #f9fafb; | |
| --block-bg: #ffffff; | |
| --border-color: #e5e7eb; | |
| --text-title: #111827; | |
| --text-body: #374151; | |
| --text-muted: #6b7280; | |
| --radius-block: 10px; | |
| --radius-input: 6px; | |
| } | |
| * { | |
| box-sizing: border-box; | |
| margin: 0; | |
| padding: 0; | |
| } | |
| body { | |
| font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif; | |
| background-color: var(--bg-main); | |
| color: var(--text-body); | |
| padding: 2rem 1rem; | |
| display: flex; | |
| justify-content: center; | |
| } | |
| .gradio-container { | |
| width: 100%; | |
| max-width: 960px; | |
| background: var(--bg-main); | |
| } | |
| .gradio-header { | |
| margin-bottom: 1.5rem; | |
| text-align: left; | |
| } | |
| .gradio-header h1 { | |
| font-size: 1.75rem; | |
| font-weight: 700; | |
| color: var(--text-title); | |
| margin-bottom: 0.5rem; | |
| } | |
| .gradio-header p { | |
| color: var(--text-muted); | |
| font-size: 0.95rem; | |
| } | |
| .gradio-layout { | |
| display: grid; | |
| grid-template-columns: 1fr; | |
| gap: 1.5rem; | |
| } | |
| @media (min-width: 768px) { | |
| .gradio-layout { | |
| grid-template-columns: 1fr 1fr; | |
| } | |
| } | |
| .gradio-column { | |
| display: flex; | |
| flex-direction: column; | |
| gap: 1.25rem; | |
| } | |
| .gr-block { | |
| background: var(--block-bg); | |
| border: 1px solid var(--border-color); | |
| border-radius: var(--radius-block); | |
| padding: 1rem; | |
| box-shadow: 0 1px 2px 0 rgba(0, 0, 0, 0.05); | |
| } | |
| .gr-label { | |
| font-size: 0.85rem; | |
| font-weight: 600; | |
| color: var(--text-muted); | |
| margin-bottom: 0.5rem; | |
| display: block; | |
| text-transform: uppercase; | |
| letter-spacing: 0.05em; | |
| } | |
| .gr-select { | |
| width: 100%; | |
| padding: 0.6rem 0.75rem; | |
| border: 1px solid var(--border-color); | |
| border-radius: var(--radius-input); | |
| background-color: #fff; | |
| font-size: 0.95rem; | |
| color: var(--text-title); | |
| outline: none; | |
| cursor: pointer; | |
| } | |
| .gr-select:focus { | |
| border-color: var(--orange-primary); | |
| } | |
| /* Gradio Image Upload Component */ | |
| .gr-image-upload { | |
| border: 2px dashed var(--border-color); | |
| border-radius: var(--radius-input); | |
| padding: 2rem 1rem; | |
| text-align: center; | |
| background-color: #f9fafb; | |
| cursor: pointer; | |
| position: relative; | |
| transition: all 0.2s; | |
| min-height: 220px; | |
| display: flex; | |
| flex-direction: column; | |
| align-items: center; | |
| justify-content: center; | |
| } | |
| .gr-image-upload:hover { | |
| border-color: var(--orange-primary); | |
| background-color: #fff7ed; | |
| } | |
| .gr-image-upload input[type="file"] { | |
| position: absolute; | |
| width: 100%; | |
| height: 100%; | |
| top: 0; | |
| left: 0; | |
| opacity: 0; | |
| cursor: pointer; | |
| } | |
| .upload-icon { | |
| font-size: 2.5rem; | |
| color: var(--text-muted); | |
| margin-bottom: 0.5rem; | |
| } | |
| .upload-text { | |
| font-size: 0.9rem; | |
| color: var(--text-muted); | |
| } | |
| .preview-img { | |
| max-width: 100%; | |
| max-height: 260px; | |
| border-radius: var(--radius-input); | |
| display: none; | |
| object-fit: contain; | |
| } | |
| /* Gradio Button */ | |
| .gr-button-primary { | |
| width: 100%; | |
| padding: 0.75rem; | |
| background-color: var(--orange-primary); | |
| color: #ffffff; | |
| border: none; | |
| border-radius: var(--radius-input); | |
| font-size: 1rem; | |
| font-weight: 600; | |
| cursor: pointer; | |
| transition: background-color 0.15s; | |
| box-shadow: 0 1px 2px rgba(0, 0, 0, 0.1); | |
| } | |
| .gr-button-primary:hover:not(:disabled) { | |
| background-color: var(--orange-hover); | |
| } | |
| .gr-button-primary:disabled { | |
| background-color: #d1d5db; | |
| cursor: not-allowed; | |
| box-shadow: none; | |
| } | |
| /* Status Banner */ | |
| .status-banner { | |
| font-size: 0.85rem; | |
| padding: 0.5rem 0.75rem; | |
| border-radius: var(--radius-input); | |
| background-color: #f3f4f6; | |
| color: var(--text-muted); | |
| display: flex; | |
| align-items: center; | |
| gap: 0.5rem; | |
| } | |
| /* Gradio Label / Output Result Component */ | |
| .gr-label-output { | |
| display: flex; | |
| flex-direction: column; | |
| gap: 0.75rem; | |
| } | |
| .top-class-box { | |
| text-align: center; | |
| padding: 1rem; | |
| background-color: #fff7ed; | |
| border: 1px solid #ffedd5; | |
| border-radius: var(--radius-input); | |
| margin-bottom: 0.5rem; | |
| } | |
| .top-class-name { | |
| font-size: 1.5rem; | |
| font-weight: 700; | |
| color: var(--orange-primary); | |
| } | |
| .top-class-conf { | |
| font-size: 0.9rem; | |
| color: var(--text-muted); | |
| } | |
| .prob-row { | |
| display: flex; | |
| flex-direction: column; | |
| gap: 0.25rem; | |
| } | |
| .prob-header { | |
| display: flex; | |
| justify-content: space-between; | |
| font-size: 0.85rem; | |
| font-weight: 600; | |
| } | |
| .prob-bar-bg { | |
| height: 20px; | |
| background-color: #f3f4f6; | |
| border-radius: 4px; | |
| overflow: hidden; | |
| } | |
| .prob-bar-fill { | |
| height: 100%; | |
| background-color: var(--orange-primary); | |
| width: 0%; | |
| transition: width 0.4s ease; | |
| } | |
| .prob-bar-fill.secondary { | |
| background-color: #9ca3af; | |
| } | |
| .empty-output { | |
| text-align: center; | |
| color: var(--text-muted); | |
| padding: 3rem 1rem; | |
| font-size: 0.9rem; | |
| } | |
| </style> | |
| </head> | |
| <body> | |
| <div class="gradio-container"> | |
| <div class="gradio-header"> | |
| <h1>必要な画像かどうかを判別</h1> | |
| <p>画像をアップロードし、ドロップダウンで選択した .onnx モデルの判定結果(need / trash)を表示します。</p> | |
| </div> | |
| <div class="gradio-layout"> | |
| <!-- 左カラム:入力 --> | |
| <div class="gradio-column"> | |
| <div class="gr-block"> | |
| <span class="gr-label">使用するモデル重み (.onnx)</span> | |
| <select id="modelSelect" class="gr-select"></select> | |
| </div> | |
| <div class="gr-block"> | |
| <span class="gr-label">入力画像</span> | |
| <div class="gr-image-upload" id="dropzone"> | |
| <input type="file" id="fileInput" accept="image/*"> | |
| <div id="uploadPrompt"> | |
| <div class="upload-icon">📁</div> | |
| <div class="upload-text">クリックまたはドラッグ&ドロップで画像をアップロード</div> | |
| </div> | |
| <img id="previewImg" class="preview-img" alt="Preview"> | |
| </div> | |
| </div> | |
| <button id="submitBtn" class="gr-button-primary" disabled>Submit</button> | |
| <div id="statusBanner" class="status-banner"> | |
| <span id="statusText">モデルをロード中...</span> | |
| </div> | |
| </div> | |
| <!-- 右カラム:出力 --> | |
| <div class="gradio-column"> | |
| <div class="gr-block" style="min-height: 100%;"> | |
| <span class="gr-label">判定結果 (確率)</span> | |
| <div id="emptyOutput" class="empty-output"> | |
| 画像を送信すると判定結果がここに表示されます。 | |
| </div> | |
| <div id="resultOutput" class="gr-label-output" style="display: none;"> | |
| <div class="top-class-box"> | |
| <div id="topClassName" class="top-class-name">need</div> | |
| <div id="topClassConf" class="top-class-conf">信頼度: 100%</div> | |
| </div> | |
| <div class="prob-row"> | |
| <div class="prob-header"> | |
| <span>need</span> | |
| <span id="probNeedText">0%</span> | |
| </div> | |
| <div class="prob-bar-bg"> | |
| <div id="probNeedBar" class="prob-bar-fill"></div> | |
| </div> | |
| </div> | |
| <div class="prob-row"> | |
| <div class="prob-header"> | |
| <span>trash</span> | |
| <span id="probTrashText">0%</span> | |
| </div> | |
| <div class="prob-bar-bg"> | |
| <div id="probTrashBar" class="prob-bar-fill secondary"></div> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| <script> | |
| // モデル定義(JSONの閾値 threshold: 0.19671 を追加) | |
| const models = [ | |
| { file: "tinymodelV9s.onnx", type: "hsc", size: 256, threshold: 0.3 }, // 最適化閾値を適用 | |
| { file: "tinymodelV3.onnx", type: "hsc", size: 224, threshold: 0.5 }, | |
| { file: "tinymodelV2.onnx", type: "softmax", size: 224, threshold: 0.5 } | |
| ]; | |
| let session = null; | |
| let isReady = false; | |
| let currentModelConfig = null; | |
| const modelSelect = document.getElementById('modelSelect'); | |
| const fileInput = document.getElementById('fileInput'); | |
| const previewImg = document.getElementById('previewImg'); | |
| const uploadPrompt = document.getElementById('uploadPrompt'); | |
| const submitBtn = document.getElementById('submitBtn'); | |
| const statusText = document.getElementById('statusText'); | |
| const emptyOutput = document.getElementById('emptyOutput'); | |
| const resultOutput = document.getElementById('resultOutput'); | |
| function initSelect() { | |
| modelSelect.innerHTML = ''; | |
| models.forEach((m, index) => { | |
| const opt = document.createElement('option'); | |
| opt.value = index; | |
| opt.textContent = m.file; | |
| modelSelect.appendChild(opt); | |
| }); | |
| modelSelect.addEventListener('change', (e) => loadModel(models[e.target.value])); | |
| } | |
| async function loadModel(modelConfig) { | |
| isReady = false; | |
| submitBtn.disabled = true; | |
| currentModelConfig = modelConfig; | |
| statusText.innerText = "⏳ " + modelConfig.file + " をロード中..."; | |
| try { | |
| if (session) { | |
| await session.release(); | |
| session = null; | |
| } | |
| session = await ort.InferenceSession.create('./' + modelConfig.file); | |
| isReady = true; | |
| statusText.innerText = "✅ " + modelConfig.file + " の準備完了 (閾値: " + (modelConfig.threshold * 100).toFixed(1) + "%)"; | |
| if (previewImg.src && previewImg.style.display !== 'none') { | |
| submitBtn.disabled = false; | |
| } | |
| } catch (e) { | |
| statusText.innerText = "❌ モデル読込エラー: " + e.message; | |
| } | |
| } | |
| function handleFile(file) { | |
| if (!file || !file.type.startsWith('image/')) return; | |
| const reader = new FileReader(); | |
| reader.onload = (e) => { | |
| previewImg.src = e.target.result; | |
| previewImg.style.display = 'block'; | |
| uploadPrompt.style.display = 'none'; | |
| resultOutput.style.display = 'none'; | |
| emptyOutput.style.display = 'block'; | |
| if (isReady) submitBtn.disabled = false; | |
| }; | |
| reader.readAsDataURL(file); | |
| } | |
| fileInput.addEventListener('change', (e) => handleFile(e.target.files[0])); | |
| async function preprocess(imgElement, targetSize) { | |
| const canvas = document.createElement('canvas'); | |
| canvas.width = targetSize; | |
| canvas.height = targetSize; | |
| const ctx = canvas.getContext('2d'); | |
| ctx.drawImage(imgElement, 0, 0, targetSize, targetSize); | |
| const data = ctx.getImageData(0, 0, targetSize, targetSize).data; | |
| const mean = [0.485, 0.456, 0.406]; | |
| const std = [0.229, 0.224, 0.225]; | |
| const floatData = new Float32Array(3 * targetSize * targetSize); | |
| for (let i = 0; i < targetSize * targetSize; i++) { | |
| const r = data[i * 4] / 255.0; | |
| const g = data[i * 4 + 1] / 255.0; | |
| const b = data[i * 4 + 2] / 255.0; | |
| floatData[i] = (r - mean[0]) / std[0]; | |
| floatData[targetSize * targetSize + i] = (g - mean[1]) / std[1]; | |
| floatData[2 * targetSize * targetSize + i] = (b - mean[2]) / std[2]; | |
| } | |
| return new ort.Tensor('float32', floatData, [1, 3, targetSize, targetSize]); | |
| } | |
| submitBtn.addEventListener('click', async () => { | |
| if (!isReady || !session || !previewImg.src || !currentModelConfig) return; | |
| submitBtn.disabled = true; | |
| statusText.innerText = "⚡ 推論中..."; | |
| try { | |
| const targetSize = currentModelConfig.size || 224; | |
| const origW = previewImg.naturalWidth; | |
| const origH = previewImg.naturalHeight; | |
| let probNeed, probTrash, rawTrashScore; | |
| // 256px 以下の低解像度画像の除外判定 (inf.py 仕様) | |
| if (currentModelConfig.file === "tinymodelV9s.onnx" && (origW <= 256 || origH <= 256)) { | |
| rawTrashScore = 1.0; | |
| probTrash = 100.0; | |
| probNeed = 0.0; | |
| statusText.innerText = "⚠️ 低解像度のため判定をスキップ (Trash)"; | |
| } else { | |
| const inputTensor = await preprocess(previewImg, targetSize); | |
| const feeds = {}; | |
| feeds[session.inputNames[0]] = inputTensor; | |
| const start = performance.now(); | |
| const results = await session.run(feeds); | |
| const duration = (performance.now() - start).toFixed(1); | |
| const output = results[session.outputNames[0]].data; | |
| if (currentModelConfig.type === "hsc") { | |
| rawTrashScore = output[0]; | |
| probTrash = rawTrashScore * 100; | |
| probNeed = (1.0 - rawTrashScore) * 100; | |
| } else if (currentModelConfig.type === "softmax") { | |
| const exp0 = Math.exp(output[0]); | |
| const exp1 = Math.exp(output[1]); | |
| const sum = exp0 + exp1; | |
| probNeed = (exp0 / sum) * 100; | |
| probTrash = (exp1 / sum) * 100; | |
| rawTrashScore = probTrash / 100.0; | |
| } | |
| statusText.innerText = "✅ 完了 (" + duration + "ms)"; | |
| } | |
| // 判定閾値 (threshold) を使用した判定ロジック | |
| const threshold = currentModelConfig.threshold ?? 0.5; | |
| const topClass = rawTrashScore > threshold ? "trash" : "need"; | |
| const topConf = (topClass === "trash" ? probTrash : probNeed).toFixed(1); | |
| // UI描画 | |
| emptyOutput.style.display = 'none'; | |
| resultOutput.style.display = 'flex'; | |
| document.getElementById('topClassName').innerText = topClass; | |
| document.getElementById('topClassConf').innerText = "信頼度: " + topConf + "%"; | |
| document.getElementById('probNeedText').innerText = probNeed.toFixed(1) + "%"; | |
| document.getElementById('probNeedBar').style.width = probNeed + "%"; | |
| document.getElementById('probTrashText').innerText = probTrash.toFixed(1) + "%"; | |
| document.getElementById('probTrashBar').style.width = probTrash + "%"; | |
| } catch (e) { | |
| statusText.innerText = "❌ エラー: " + e.message; | |
| } finally { | |
| submitBtn.disabled = false; | |
| } | |
| }); | |
| window.onload = () => { | |
| initSelect(); | |
| if (models.length > 0) loadModel(models[0]); | |
| }; | |
| </script> | |
| </body> | |
| </html> |