| <!DOCTYPE html> |
| <html lang="en"> |
| <head> |
| <meta charset="UTF-8"> |
| <meta name="viewport" content="width=device-width, initial-scale=1.0"> |
|
|
| <title>SnapChef Edibility Checker</title> |
|
|
| <script src="https://cdn.jsdelivr.net/npm/@tensorflow/tfjs@latest"></script> |
|
|
| <style> |
| * { |
| box-sizing: border-box; |
| } |
| |
| body { |
| margin: 0; |
| min-height: 100vh; |
| display: flex; |
| justify-content: center; |
| align-items: center; |
| padding: 30px; |
| font-family: Arial, sans-serif; |
| background: linear-gradient(135deg, #effaf3, #d9f2e3); |
| color: #18392b; |
| } |
| |
| .container { |
| width: 100%; |
| max-width: 520px; |
| padding: 36px; |
| text-align: center; |
| background: white; |
| border-radius: 24px; |
| box-shadow: 0 14px 40px rgba(0, 0, 0, 0.12); |
| } |
| |
| h1 { |
| margin-top: 0; |
| margin-bottom: 8px; |
| font-size: 32px; |
| } |
| |
| .subtitle { |
| margin-bottom: 26px; |
| color: #5f746a; |
| line-height: 1.5; |
| } |
| |
| .upload-box { |
| display: block; |
| padding: 25px; |
| border: 2px dashed #4ba870; |
| border-radius: 16px; |
| cursor: pointer; |
| background: #f4fbf6; |
| transition: 0.2s; |
| } |
| |
| .upload-box:hover { |
| background: #eaf7ee; |
| } |
| |
| input[type="file"] { |
| display: none; |
| } |
| |
| #preview { |
| display: none; |
| width: 100%; |
| max-height: 320px; |
| margin-top: 22px; |
| object-fit: contain; |
| border-radius: 16px; |
| background: #f2f2f2; |
| } |
| |
| button { |
| width: 100%; |
| margin-top: 22px; |
| padding: 14px; |
| border: none; |
| border-radius: 12px; |
| background: #218c52; |
| color: white; |
| font-size: 17px; |
| font-weight: bold; |
| cursor: pointer; |
| } |
| |
| button:hover { |
| background: #187441; |
| } |
| |
| button:disabled { |
| background: #9bad9f; |
| cursor: not-allowed; |
| } |
| |
| #status { |
| margin-top: 20px; |
| color: #5f746a; |
| } |
| |
| #result { |
| display: none; |
| margin-top: 22px; |
| padding: 20px; |
| border-radius: 16px; |
| font-size: 20px; |
| font-weight: bold; |
| } |
| |
| .edible { |
| color: #176b3a; |
| background: #e4f8eb; |
| border: 1px solid #78ca95; |
| } |
| |
| .inedible { |
| color: #a32929; |
| background: #fdeaea; |
| border: 1px solid #e89898; |
| } |
| |
| .confidence { |
| display: block; |
| margin-top: 8px; |
| font-size: 15px; |
| font-weight: normal; |
| } |
| |
| .warning { |
| margin-top: 24px; |
| font-size: 12px; |
| line-height: 1.5; |
| color: #78877f; |
| } |
| </style> |
| </head> |
|
|
| <body> |
| <main class="container"> |
| <h1>SnapChef</h1> |
|
|
| <p class="subtitle"> |
| Upload a food image to check whether it appears edible or inedible |
| </p> |
|
|
| <label class="upload-box" for="imageInput"> |
| Click here to choose a food image |
| </label> |
|
|
| <input |
| id="imageInput" |
| type="file" |
| accept="image/png, image/jpeg, image/webp" |
| > |
|
|
| <img id="preview" alt="selected food image"> |
|
|
| <button id="predictButton" disabled> |
| Check Edibility |
| </button> |
|
|
| <p id="status">Loading model...</p> |
|
|
| <div id="result"></div> |
|
|
| <p class="warning"> |
| This prediction is produced by an AI model and should not replace proper |
| food safety checks |
| </p> |
| </main> |
|
|
| <script> |
| let model = null |
| |
| const imageInput = document.getElementById("imageInput") |
| const preview = document.getElementById("preview") |
| const predictButton = document.getElementById("predictButton") |
| const statusText = document.getElementById("status") |
| const resultBox = document.getElementById("result") |
| |
| async function loadModel() { |
| try { |
| model = await tf.loadGraphModel("./model.json") |
| |
| statusText.textContent = "Model ready" |
| |
| if (imageInput.files.length > 0) { |
| predictButton.disabled = false |
| } |
| } catch (error) { |
| console.error(error) |
| statusText.textContent = "Failed to load the model" |
| } |
| } |
| |
| imageInput.addEventListener("change", function () { |
| const file = imageInput.files[0] |
| |
| if (!file) { |
| return |
| } |
| |
| preview.src = URL.createObjectURL(file) |
| preview.style.display = "block" |
| |
| resultBox.style.display = "none" |
| |
| if (model) { |
| predictButton.disabled = false |
| } |
| }) |
| |
| predictButton.addEventListener("click", async function () { |
| if (!model || !preview.src) { |
| return |
| } |
| |
| predictButton.disabled = true |
| statusText.textContent = "Analysing image..." |
| resultBox.style.display = "none" |
| |
| let inputTensor |
| let predictionTensor |
| |
| try { |
| inputTensor = tf.tidy(function () { |
| const imageTensor = tf.browser.fromPixels(preview) |
| |
| const resizedTensor = tf.image.resizeBilinear( |
| imageTensor, |
| [224, 224] |
| ) |
| |
| return resizedTensor |
| .toFloat() |
| .expandDims(0) |
| }) |
| |
| predictionTensor = model.predict(inputTensor) |
| |
| const predictionData = await predictionTensor.data() |
| const inedibleProbability = predictionData[0] |
| |
| let label |
| let confidence |
| |
| if (inedibleProbability >= 0.5) { |
| label = "Inedible" |
| confidence = inedibleProbability |
| resultBox.className = "inedible" |
| } else { |
| label = "Edible" |
| confidence = 1 - inedibleProbability |
| resultBox.className = "edible" |
| } |
| |
| resultBox.innerHTML = ` |
| ${label} |
| <span class="confidence"> |
| Confidence: ${(confidence * 100).toFixed(2)}% |
| </span> |
| ` |
| |
| resultBox.style.display = "block" |
| statusText.textContent = "Analysis complete" |
| } catch (error) { |
| console.error(error) |
| statusText.textContent = "Prediction failed" |
| } finally { |
| if (inputTensor) { |
| inputTensor.dispose() |
| } |
| |
| if (predictionTensor) { |
| predictionTensor.dispose() |
| } |
| |
| predictButton.disabled = false |
| } |
| }) |
| |
| loadModel() |
| </script> |
| </body> |
| </html> |