| <!DOCTYPE html> |
| <html lang="en"> |
| <head> |
| <meta charset="UTF-8"> |
| <meta name="viewport" content="width=device-width, initial-scale=1.0"> |
| <title>AFIP β Acoustic Flatulence Intelligence Platform</title> |
| <style> |
| :root { --bg: #0a0a0f; --surface: #14141a; --border: #2a2a35; --text: #e2e2e8; --text-muted: #888898; --accent: #3b82f6; --success: #22c55e; --warn: #eab308; } |
| * { box-sizing: border-box; margin: 0; } |
| body { background: var(--bg); color: var(--text); font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif; padding: 40px 24px; } |
| .container { max-width: 720px; margin: 0 auto; } |
| header { text-align: center; margin-bottom: 40px; } |
| h1 { font-size: 2rem; margin-bottom: 8px; } |
| p { color: var(--text-muted); } |
| .actions { display: flex; gap: 12px; justify-content: center; margin-bottom: 32px; flex-wrap: wrap; } |
| button { |
| background: linear-gradient(135deg, var(--accent), #8b5cf6); color: #fff; border: none; |
| padding: 12px 28px; border-radius: 10px; font-size: 1rem; cursor: pointer; transition: transform 0.15s; |
| } |
| button:hover { transform: translateY(-2px); } |
| button.secondary { background: var(--surface); border: 1px solid var(--border); color: var(--text); } |
| .dropzone { |
| border: 2px dashed var(--border); border-radius: 12px; padding: 48px 24px; |
| text-align: center; background: var(--surface); cursor: pointer; transition: border-color 0.2s; |
| } |
| .dropzone:hover, .dropzone.dragover { border-color: var(--accent); } |
| .dropzone input { display: none; } |
| .result { background: var(--surface); border: 1px solid var(--border); border-radius: 12px; padding: 20px; margin-top: 24px; display: none; } |
| .result.show { display: block; } |
| .result h3 { margin-bottom: 12px; } |
| .grid { display: grid; grid-template-columns: 1fr 1fr; gap: 12px; } |
| .kv { display: flex; justify-content: space-between; padding: 8px 0; border-bottom: 1px solid var(--border); font-size: 0.9rem; } |
| .kv .key { color: var(--text-muted); } |
| .badge { background: rgba(34,197,94,0.15); color: var(--success); padding: 2px 10px; border-radius: 999px; font-size: 0.75rem; } |
| .notes { max-height: 200px; overflow-y: auto; font-family: ui-monospace, monospace; font-size: 0.82rem; margin-top: 12px; } |
| .note-row { padding: 4px 0; border-bottom: 1px solid var(--border); display: flex; gap: 12px; } |
| .note-row span { color: var(--text-muted); } |
| .loading { text-align: center; color: var(--text-muted); margin-top: 20px; display: none; } |
| .leaderboard { margin-top: 32px; } |
| .leaderboard h3 { margin-bottom: 12px; } |
| .lb-row { display: flex; justify-content: space-between; padding: 10px 0; border-bottom: 1px solid var(--border); font-size: 0.9rem; } |
| .lb-row .score { color: var(--success); font-weight: 600; } |
| </style> |
| </head> |
| <body> |
| <div class="container"> |
| <header> |
| <h1>π¨ AFIP</h1> |
| <p>Acoustic Flatulence Intelligence Platform v3.0<br>Upload a .wav β get a FartScore, MIDI, and Solana wallet.</p> |
| </header> |
|
|
| <div class="actions"> |
| <button onclick="document.getElementById('fileInput').click()">π Upload WAV</button> |
| <button class="secondary" onclick="runMock()">π² Generate Mock</button> |
| <button class="secondary" onclick="loadLeaderboard()">π Leaderboard</button> |
| </div> |
|
|
| <div class="dropzone" id="dropzone" onclick="document.getElementById('fileInput').click()"> |
| <input type="file" id="fileInput" accept=".wav" onchange="handleFile(this.files[0])"> |
| <div style="font-size: 3rem; margin-bottom: 8px;">π΅</div> |
| <h3>Drop a WAV file</h3> |
| <p style="color: var(--text-muted);">or click to browse</p> |
| </div> |
|
|
| <div class="loading" id="loading">Analyzing...</div> |
|
|
| <div class="result" id="result"> |
| <h3>Analysis Result <span class="badge" id="scoreBadge">FartScore: 0</span></h3> |
| <div class="grid" id="grid"></div> |
| <div class="notes" id="notes"></div> |
| <div style="margin-top: 12px;"> |
| <a id="midiLink" href="#" style="color: var(--accent); text-decoration: none;">β¬ Download MIDI</a> |
| </div> |
| </div> |
|
|
| <div class="leaderboard" id="leaderboard" style="display:none;"> |
| <h3>π Leaderboard</h3> |
| <div id="lbList"></div> |
| </div> |
| </div> |
|
|
| <script> |
| const $ = id => document.getElementById(id); |
| |
| async function handleFile(file) { |
| if (!file) return; |
| $('loading').style.display = 'block'; |
| $('result').classList.remove('show'); |
| const fd = new FormData(); |
| fd.append('file', file); |
| try { |
| const r = await fetch('/api/analyze', { method: 'POST', body: fd }); |
| const data = await r.json(); |
| showResult(data); |
| } catch (e) { alert('Error: ' + e.message); } |
| $('loading').style.display = 'none'; |
| } |
| |
| async function runMock() { |
| $('loading').style.display = 'block'; |
| $('result').classList.remove('show'); |
| try { |
| const r = await fetch('/api/mock', { method: 'POST' }); |
| const data = await r.json(); |
| showResult(data); |
| } catch (e) { alert('Error: ' + e.message); } |
| $('loading').style.display = 'none'; |
| } |
| |
| function showResult(data) { |
| $('scoreBadge').textContent = 'FartScore: ' + data.fartscore; |
| $('grid').innerHTML = ` |
| <div class="kv"><span class="key">Duration</span><span>${data.duration_sec}s</span></div> |
| <div class="kv"><span class="key">Sample Rate</span><span>${data.sample_rate} Hz</span></div> |
| <div class="kv"><span class="key">Frames</span><span>${data.frame_count}</span></div> |
| <div class="kv"><span class="key">Notes</span><span>${data.note_count}</span></div> |
| <div class="kv"><span class="key">Solana Pub</span><span style="font-size:0.78rem;">${data.solana_public.substring(0,24)}...</span></div> |
| <div class="kv"><span class="key">Audio Hash</span><span style="font-size:0.78rem;">${data.audio_hash.substring(0,24)}...</span></div> |
| `; |
| $('notes').innerHTML = (data.notes || []).map(n => |
| `<div class="note-row"><span>${n.start}s</span><span>${n.dur}s</span><span>MIDI ${n.note}</span><span>vel ${n.vel}</span></div>` |
| ).join(''); |
| $('midiLink').href = '/api/midi/' + data.audio_hash.substring(0,16); |
| $('result').classList.add('show'); |
| } |
| |
| async function loadLeaderboard() { |
| try { |
| const r = await fetch('/api/leaderboard'); |
| const data = await r.json(); |
| $('lbList').innerHTML = data.map((row, i) => |
| `<div class="lb-row"><span>#${i+1} ${row.id}</span><span class="score">${row.fartscore}</span></div>` |
| ).join(''); |
| $('leaderboard').style.display = 'block'; |
| } catch (e) { console.error(e); } |
| } |
| |
| const dz = $('dropzone'); |
| dz.addEventListener('dragover', e => { e.preventDefault(); dz.classList.add('dragover'); }); |
| dz.addEventListener('dragleave', () => dz.classList.remove('dragover')); |
| dz.addEventListener('drop', e => { |
| e.preventDefault(); dz.classList.remove('dragover'); |
| const f = e.dataTransfer.files[0]; |
| if (f && f.name.endsWith('.wav')) handleFile(f); |
| }); |
| </script> |
| </body> |
| </html> |
|
|