Spaces:
Sleeping
Sleeping
| <html lang="en"> | |
| <head> | |
| <meta charset="UTF-8"> | |
| <meta name="viewport" content="width=device-width, initial-scale=1.0"> | |
| <title>Rice Quality AI | Smart Grading</title> | |
| <link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;600;700&display=swap" rel="stylesheet"> | |
| <style> | |
| :root { | |
| --primary: #2e7d32; | |
| --primary-light: #60ad5e; | |
| --accent: #fbc02d; | |
| --bg: #f5f5f5; | |
| --card: #ffffff; | |
| --text: #333333; | |
| } | |
| body { | |
| font-family: 'Inter', sans-serif; | |
| background-color: var(--bg); | |
| color: var(--text); | |
| margin: 0; | |
| padding: 20px; | |
| display: flex; | |
| justify-content: center; | |
| } | |
| .container { | |
| max-width: 900px; | |
| width: 100%; | |
| } | |
| header { | |
| text-align: center; | |
| margin-bottom: 30px; | |
| } | |
| h1 { color: var(--primary); margin-bottom: 5px; } | |
| p.subtitle { color: #666; margin-top: 0; } | |
| .card { | |
| background: var(--card); | |
| padding: 30px; | |
| border-radius: 16px; | |
| box-shadow: 0 4px 20px rgba(0,0,0,0.08); | |
| margin-bottom: 20px; | |
| } | |
| #upload-zone { | |
| border: 2px dashed #ccc; | |
| border-radius: 12px; | |
| padding: 40px 20px; | |
| text-align: center; | |
| cursor: pointer; | |
| transition: all 0.3s; | |
| background: #fafafa; | |
| } | |
| #upload-zone:hover { | |
| border-color: var(--primary); | |
| background: #f1f8e9; | |
| } | |
| #file-input { display: none; } | |
| .btn { | |
| background: var(--primary); | |
| color: white; | |
| border: none; | |
| padding: 12px 24px; | |
| border-radius: 8px; | |
| font-weight: 600; | |
| cursor: pointer; | |
| width: 100%; | |
| font-size: 16px; | |
| margin-top: 20px; | |
| transition: background 0.2s; | |
| } | |
| .btn:disabled { background: #ccc; cursor: not-allowed; } | |
| .btn:hover:not(:disabled) { background: #1b5e20; } | |
| #preview { | |
| max-width: 100%; | |
| border-radius: 8px; | |
| margin-top: 20px; | |
| display: none; | |
| } | |
| #results { | |
| display: none; | |
| animation: fadeIn 0.5s; | |
| } | |
| .visual-container { | |
| display: flex; | |
| gap: 20px; | |
| margin-bottom: 30px; | |
| flex-wrap: wrap; | |
| } | |
| .image-box { | |
| flex: 2; | |
| min-width: 300px; | |
| text-align: center; | |
| } | |
| .legend-box { | |
| flex: 1; | |
| min-width: 200px; | |
| background: #f9f9f9; | |
| padding: 15px; | |
| border-radius: 12px; | |
| border: 1px solid #eee; | |
| align-self: center; | |
| } | |
| .legend-title { | |
| font-weight: 700; | |
| font-size: 14px; | |
| margin-bottom: 10px; | |
| color: #444; | |
| border-bottom: 2px solid var(--primary); | |
| display: inline-block; | |
| } | |
| .legend-item { | |
| display: flex; | |
| align-items: center; | |
| gap: 10px; | |
| margin-bottom: 8px; | |
| font-size: 13px; | |
| } | |
| .color-swatch { | |
| width: 16px; | |
| height: 16px; | |
| border-radius: 4px; | |
| } | |
| .result-item { | |
| display: flex; | |
| justify-content: space-between; | |
| padding: 12px 0; | |
| border-bottom: 1px solid #eee; | |
| } | |
| .result-item:last-child { border-bottom: none; } | |
| .label { font-weight: 600; color: #555; } | |
| .value { color: var(--primary); font-weight: 700; } | |
| .grade-badge { | |
| background: var(--accent); | |
| padding: 4px 12px; | |
| border-radius: 20px; | |
| font-size: 14px; | |
| } | |
| .loader { | |
| display: none; | |
| text-align: center; | |
| margin: 20px 0; | |
| } | |
| .spinner { | |
| width: 30px; | |
| height: 30px; | |
| border: 4px solid #f3f3f3; | |
| border-top: 4px solid var(--primary); | |
| border-radius: 50%; | |
| animation: spin 1s linear infinite; | |
| display: inline-block; | |
| } | |
| @keyframes spin { 0% { transform: rotate(0deg); } 100% { transform: rotate(360deg); } } | |
| @keyframes fadeIn { from { opacity: 0; transform: translateY(10px); } to { opacity: 1; transform: translateY(0); } } | |
| .lang-pills { | |
| display: flex; | |
| gap: 10px; | |
| justify-content: center; | |
| margin-bottom: 20px; | |
| } | |
| .pill { | |
| font-size: 14px; | |
| background: #e0e0e0; | |
| padding: 6px 15px; | |
| border: 2px solid transparent; | |
| border-radius: 20px; | |
| color: #666; | |
| cursor: pointer; | |
| font-weight: 600; | |
| transition: all 0.2s; | |
| } | |
| .pill:hover { background: #d0d0d0; } | |
| .pill.active { | |
| background: var(--primary); | |
| color: white; | |
| border-color: var(--primary-light); | |
| } | |
| #annotated-preview { | |
| max-width: 100%; | |
| border-radius: 8px; | |
| border: 2px solid var(--primary); | |
| } | |
| </style> | |
| </head> | |
| <body> | |
| <div class="container"> | |
| <header> | |
| <h1>🌾 <span data-i18n="title">Rice Quality AI</span></h1> | |
| <p class="subtitle" data-i18n="subtitle">Instant Grading & Market Value</p> | |
| <div class="lang-pills"> | |
| <button class="pill active" onclick="setLanguage('en')">English</button> | |
| <button class="pill" onclick="setLanguage('hi')">हिंदी</button> | |
| <button class="pill" onclick="setLanguage('pa')">ਪੰਜਾਬੀ</button> | |
| </div> | |
| </header> | |
| <div class="card"> | |
| <div id="upload-zone" onclick="document.getElementById('file-input').click()"> | |
| <div id="upload-prompt"> | |
| <p data-i18n="upload_prompt">📸 Tap to Capture or Upload Image</p> | |
| <small data-i18n="upload_hint">For best results, use a white background</small> | |
| </div> | |
| <img id="preview" src="#" alt="Preview"> | |
| </div> | |
| <input type="file" id="file-input" accept="image/*"> | |
| <button id="analyze-btn" class="btn" disabled data-i18n="analyze_btn">Analyze Sample</button> | |
| </div> | |
| <div id="loader" class="loader"> | |
| <div class="spinner"></div> | |
| <p data-i18n="loading">AI Engine Scanning...</p> | |
| </div> | |
| <div id="results" class="card"> | |
| <div class="visual-container"> | |
| <div class="image-box"> | |
| <p style="font-weight: 600; color: #555; margin-bottom: 10px;" data-i18n="visual_proof">Visual Analysis Result</p> | |
| <img id="annotated-preview" src="#" alt="Annotated Result"> | |
| </div> | |
| <div class="legend-box"> | |
| <span class="legend-title" data-i18n="legend">Legend</span> | |
| <div id="legend-items"> | |
| <!-- Legend items will be injected here --> | |
| </div> | |
| </div> | |
| </div> | |
| <div class="result-item"> | |
| <span class="label" data-i18n="res_variety">Detected Variety</span> | |
| <span id="res-variety" class="value">-</span> | |
| </div> | |
| <div class="result-item"> | |
| <span class="label" data-i18n="res_grade">Quality Grade</span> | |
| <span id="res-grade" class="grade-badge">-</span> | |
| </div> | |
| <div class="result-item"> | |
| <span class="label" data-i18n="res_head">Head Rice</span> | |
| <span id="res-head" class="value">-</span> | |
| </div> | |
| <div class="result-item"> | |
| <span class="label" data-i18n="res_count">Head Rice Count</span> | |
| <span id="res-count" class="value" style="color: #2e7d32;">-</span> | |
| </div> | |
| <div class="result-item"> | |
| <span class="label" data-i18n="res_broken">Broken Grains</span> | |
| <span id="res-broken" class="value">-</span> | |
| </div> | |
| <div class="result-item"> | |
| <span class="label" data-i18n="res_foreign">Foreign Matter</span> | |
| <span id="res-foreign" class="value">-</span> | |
| </div> | |
| <div class="result-item"> | |
| <span class="label" data-i18n="res_price">Est. Market Value</span> | |
| <span id="res-price" class="value" style="color: #d32f2f;">-</span> | |
| </div> | |
| <div class="result-item"> | |
| <span class="label" data-i18n="res_ratio">L/W Ratio</span> | |
| <span id="res-ratio" class="value">-</span> | |
| </div> | |
| <div class="result-item" id="res-status-row"> | |
| <span class="label" data-i18n="res_status">Status</span> | |
| <span id="res-status" class="value">-</span> | |
| </div> | |
| </div> | |
| </div> | |
| <script> | |
| const translations = { | |
| en: { | |
| title: "Rice Quality AI", | |
| subtitle: "Instant Grading & Market Value", | |
| upload_prompt: "📸 Tap to Capture or Upload Image", | |
| upload_hint: "For best results, use a white background", | |
| analyze_btn: "Analyze Sample", | |
| loading: "AI Engine Scanning...", | |
| visual_proof: "Visual Analysis Result", | |
| legend: "Category Legend", | |
| res_variety: "Detected Variety", | |
| res_grade: "Quality Grade", | |
| res_head: "Head Rice", | |
| res_count: "Head Rice Count", | |
| res_broken: "Broken Grains", | |
| res_foreign: "Foreign Matter", | |
| res_price: "Est. Market Value", | |
| res_ratio: "L/W Ratio", | |
| res_status: "Status", | |
| classes: ['Lower Admixture', 'Broken', 'Chalky', 'Damaged', 'Dehusked', 'Discoloured', 'Inorganic Foreign', 'Organic Foreign', 'Good Grain', 'Red Grain'] | |
| }, | |
| hi: { | |
| title: "चावल गुणवत्ता AI", | |
| subtitle: "त्वरित ग्रेडिंग और बाजार मूल्य", | |
| upload_prompt: "📸 फोटो लें या इमेज अपलोड करें", | |
| upload_hint: "बेहतर परिणामों के लिए सफेद बैकग्राउंड का उपयोग करें", | |
| analyze_btn: "नमूने का विश्लेषण करें", | |
| loading: "AI इंजन स्कैन कर रहा है...", | |
| visual_proof: "दृश्य विश्लेषण परिणाम", | |
| legend: "श्रेणी स्पष्टीकरण", | |
| res_variety: "चावल की किस्म", | |
| res_grade: "गुणवत्ता श्रेणी", | |
| res_head: "साबुत चावल", | |
| res_count: "साबुत चावल की संख्या", | |
| res_broken: "टूटे हुए चावल", | |
| res_foreign: "बाहरी तत्व", | |
| res_price: "अनुमानित बाजार मूल्य", | |
| res_ratio: "औसत लंबाई/चौड़ाई अनुपात", | |
| res_status: "किस्म की स्थिति", | |
| classes: ['निम्न मिश्रण', 'टूटा हुआ', 'चाक जैसा', 'क्षतिग्रस्त', 'बिना छिलके वाला', 'फीका पड़ा हुआ', 'अकार्बनिक विदेशी', 'जैविक विदेशी', 'अच्छा दाना', 'लाल दाना'] | |
| }, | |
| pa: { | |
| title: "ਚਾਵਲ ਗੁਣਵੱਤਾ AI", | |
| subtitle: "ਤੁਰੰਤ ਗ੍ਰੇਡਿੰਗ ਅਤੇ ਬਾਜ਼ਾਰ ਮੁੱਲ", | |
| upload_prompt: "📸 ਫੋਟੋ ਲਓ ਜਾਂ ਚਿੱਤਰ ਅਪਲੋਡ ਕਰੋ", | |
| upload_hint: "ਵਧੀਆ ਨਤੀਜਿਆਂ ਲਈ ਚਿੱਟੇ ਬੈਕਗ੍ਰਾਊਂਡ ਦੀ ਵਰਤੋਂ ਕਰੋ", | |
| analyze_btn: "ਨਮੂਨੇ ਦਾ ਵਿਸ਼ਲੇਸ਼ਣ ਕਰੋ", | |
| loading: "AI ਇੰਜਣ ਸਕੈਨ ਕਰ ਰਿਹਾ ਹੈ...", | |
| visual_proof: "ਵਿਜ਼ੂਅਲ ਵਿਸ਼ਲੇਸ਼ਣ ਨਤੀਜਾ", | |
| legend: "ਸ਼੍ਰੇਣੀ ਦੀ ਵਿਆਖਿਆ", | |
| res_variety: "ਚਾਵਲ ਦੀ ਕਿਸਮ", | |
| res_grade: "ਗੁਣਵੱਤਾ ਦਰਜਾ", | |
| res_head: "ਸਾਬਤ ਚਾਵਲ", | |
| res_count: "ਸਾਬਤ ਚਾਵਲ ਦੀ ਗਿਣਤੀ", | |
| res_broken: "ਟੁੱਟੇ ਹੋਏ ਚਾਵਲ", | |
| res_foreign: "ਬਾਹਰੀ ਤੱਤ", | |
| res_price: "ਅਨੁਮਾਨਿਤ ਬਾਜ਼ਾਰ ਮੁੱਲ", | |
| res_ratio: "ਔਸਤ ਲੰਬਾਈ/ਚੌੜਾਈ ਅਨੁਪਾਤ", | |
| res_status: "ਕਿਸਮ ਦੀ ਸਥਿਤੀ", | |
| classes: ['ਘੱਟ ਮਿਸ਼ਰਣ', 'ਟੁੱਟਿਆ ਹੋਇਆ', 'ਚਾਕ ਵਰਗਾ', 'ਖਰਾਬ', 'ਬਿਨਾਂ ਛਿਲਕੇ ਵਾਲਾ', 'ਫਿੱਕਾ ਪਿਆ', 'ਅਕਾਰਬਨਿਕ ਬਾਹਰੀ', 'ਜੈਵਿਕ ਬਾਹਰੀ', 'ਵਧੀਆ ਦਾਣਾ', 'ਲਾਲ ਦਾਣਾ'] | |
| } | |
| }; | |
| // Diverse high-contrast palette matching the backend (HEX format) | |
| const colors = [ | |
| "#E6194B", // Red (admixture) | |
| "#FFE119", // Yellow (broken) | |
| "#0082C8", // Blue (chalky) | |
| "#F58230", // Orange (damaged) | |
| "#911EB4", // Purple (dehusked) | |
| "#46F0F0", // Cyan (discoloured) | |
| "#F032E6", // Magenta (inorganic) | |
| "#D2F53C", // Lime (organic) | |
| "#3CB44B", // Green (good grain) | |
| "#FABED4" // Pink (red grain) | |
| ]; | |
| let currentLang = 'en'; | |
| function setLanguage(lang) { | |
| currentLang = lang; | |
| // Update buttons | |
| document.querySelectorAll('.pill').forEach(btn => { | |
| btn.classList.toggle('active', btn.getAttribute('onclick').includes(`'${lang}'`)); | |
| }); | |
| // Update text content | |
| document.querySelectorAll('[data-i18n]').forEach(el => { | |
| const key = el.getAttribute('data-i18n'); | |
| el.innerText = translations[lang][key]; | |
| }); | |
| renderLegend(); | |
| } | |
| function renderLegend() { | |
| const legendContainer = document.getElementById('legend-items'); | |
| legendContainer.innerHTML = ''; | |
| translations[currentLang].classes.forEach((className, index) => { | |
| const item = document.createElement('div'); | |
| item.className = 'legend-item'; | |
| item.innerHTML = ` | |
| <div class="color-swatch" style="background: ${colors[index % colors.length]}"></div> | |
| <span>${className}</span> | |
| `; | |
| legendContainer.appendChild(item); | |
| }); | |
| } | |
| // Initialize legend | |
| renderLegend(); | |
| const fileInput = document.getElementById('file-input'); | |
| const uploadZone = document.getElementById('upload-zone'); | |
| const preview = document.getElementById('preview'); | |
| const prompt = document.getElementById('upload-prompt'); | |
| const analyzeBtn = document.getElementById('analyze-btn'); | |
| const results = document.getElementById('results'); | |
| const loader = document.getElementById('loader'); | |
| fileInput.addEventListener('change', function(e) { | |
| const file = e.target.files[0]; | |
| if (file) { | |
| const reader = new FileReader(); | |
| reader.onload = function(e) { | |
| preview.src = e.target.result; | |
| preview.style.display = 'block'; | |
| prompt.style.display = 'none'; | |
| analyzeBtn.disabled = false; | |
| results.style.display = 'none'; | |
| } | |
| reader.readAsDataURL(file); | |
| } | |
| }); | |
| analyzeBtn.addEventListener('click', async () => { | |
| const file = fileInput.files[0]; | |
| if (!file) return; | |
| const formData = new FormData(); | |
| formData.append('file', file); | |
| analyzeBtn.disabled = true; | |
| loader.style.display = 'block'; | |
| results.style.display = 'none'; | |
| try { | |
| const response = await fetch('/analyze', { | |
| method: 'POST', | |
| body: formData | |
| }); | |
| const data = await response.json(); | |
| document.getElementById('res-variety').innerText = `${data.variety} (${data.confidence})`; | |
| document.getElementById('res-grade').innerText = data.grade; | |
| document.getElementById('res-head').innerText = data.metrics.head_rice; | |
| document.getElementById('res-count').innerText = `${data.metrics.head_count} ${currentLang === 'en' ? 'grains' : currentLang === 'hi' ? 'दाने' : 'ਦਾਣੇ'}`; | |
| document.getElementById('res-broken').innerText = data.metrics.broken_grains; | |
| document.getElementById('res-foreign').innerText = data.metrics.foreign_matter; | |
| document.getElementById('res-price').innerText = data.estimated_value; | |
| document.getElementById('res-ratio').innerText = data.metrics.avg_length_width_ratio; | |
| document.getElementById('res-status').innerText = data.status; | |
| // Show annotated image | |
| const annPreview = document.getElementById('annotated-preview'); | |
| annPreview.src = `data:image/jpeg;base64,${data.annotated_image}`; | |
| annPreview.style.display = 'inline-block'; | |
| loader.style.display = 'none'; | |
| results.style.display = 'block'; | |
| analyzeBtn.disabled = false; | |
| } catch (error) { | |
| alert('Error analyzing image. Please try again.'); | |
| loader.style.display = 'none'; | |
| analyzeBtn.disabled = false; | |
| } | |
| }); | |
| </script> | |
| </body> | |
| </html> | |