Upload static/index.html with huggingface_hub
Browse files- static/index.html +121 -223
static/index.html
CHANGED
|
@@ -3,248 +3,146 @@
|
|
| 3 |
<head>
|
| 4 |
<meta charset="UTF-8">
|
| 5 |
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
| 6 |
-
<title>AFIP
|
| 7 |
<style>
|
| 8 |
-
|
| 9 |
-
|
| 10 |
-
|
| 11 |
-
|
| 12 |
-
header
|
| 13 |
-
|
| 14 |
-
|
| 15 |
-
.
|
| 16 |
-
|
| 17 |
-
|
| 18 |
-
|
| 19 |
-
|
| 20 |
-
|
| 21 |
-
|
| 22 |
-
|
| 23 |
-
|
| 24 |
-
|
| 25 |
-
|
| 26 |
-
.
|
| 27 |
-
.
|
| 28 |
-
.
|
| 29 |
-
.
|
| 30 |
-
.
|
| 31 |
-
.
|
| 32 |
-
.
|
| 33 |
-
.
|
| 34 |
-
.
|
| 35 |
-
.
|
| 36 |
-
|
| 37 |
-
|
| 38 |
-
|
| 39 |
-
|
| 40 |
-
|
| 41 |
-
|
| 42 |
-
|
| 43 |
</style>
|
| 44 |
</head>
|
| 45 |
<body>
|
| 46 |
-
<header>
|
| 47 |
-
<h1>AFIP</h1>
|
| 48 |
-
<span class="badge">v3.0.0</span>
|
| 49 |
-
<span style="color:#888;font-size:.75rem;margin-left:auto">Acoustic Flatulence Intelligence Platform</span>
|
| 50 |
-
</header>
|
| 51 |
<div class="container">
|
| 52 |
-
|
| 53 |
-
<
|
| 54 |
-
<
|
| 55 |
-
<
|
| 56 |
-
|
| 57 |
-
<
|
| 58 |
-
<button
|
| 59 |
-
<
|
| 60 |
-
<
|
| 61 |
-
</div>
|
| 62 |
-
|
| 63 |
-
|
| 64 |
-
<
|
| 65 |
-
<
|
| 66 |
-
<
|
| 67 |
-
<
|
| 68 |
-
<
|
| 69 |
-
|
| 70 |
-
<div class="
|
| 71 |
-
|
| 72 |
-
|
| 73 |
-
<
|
| 74 |
-
<div
|
| 75 |
-
<div class="
|
| 76 |
-
<div
|
| 77 |
-
<
|
| 78 |
-
<
|
| 79 |
-
<
|
| 80 |
-
|
| 81 |
-
<div style="
|
| 82 |
-
<
|
| 83 |
-
<
|
| 84 |
-
</div>
|
| 85 |
-
</div>
|
| 86 |
-
<div style="flex:1;min-width:280px;text-align:center">
|
| 87 |
-
<div class="score-ring">
|
| 88 |
-
<svg width="120" height="120"><circle cx="60" cy="60" r="50" fill="none" stroke="#222" stroke-width="8"/><circle id="scoreCircle" cx="60" cy="60" r="50" fill="none" stroke="#00ff88" stroke-width="8" stroke-dasharray="314" stroke-dashoffset="314"/></svg>
|
| 89 |
-
<div style="position:absolute;top:50%;left:50%;transform:translate(-50%,-50%);font-size:1.8rem;color:#00ff88" id="scoreText">0</div>
|
| 90 |
-
</div>
|
| 91 |
-
</div>
|
| 92 |
-
</div>
|
| 93 |
-
|
| 94 |
-
<h2 style="margin-top:1.5rem">Piano Roll</h2>
|
| 95 |
-
<canvas id="pianoRoll" class="piano-roll"></canvas>
|
| 96 |
-
</div>
|
| 97 |
-
|
| 98 |
-
<div class="card">
|
| 99 |
-
<h2>Leaderboard</h2>
|
| 100 |
-
<table id="leaderboard"><thead><tr><th>Rank</th><th>Time</th><th>FartScore</th><th>Solana Address</th></tr></thead><tbody></tbody></table>
|
| 101 |
-
</div>
|
| 102 |
-
|
| 103 |
-
<div class="card">
|
| 104 |
-
<h2>Recent Sessions</h2>
|
| 105 |
-
<table id="history"><thead><tr><th>ID</th><th>Time</th><th>Score</th><th>Notes</th><th>Address</th></tr></thead><tbody></tbody></table>
|
| 106 |
-
</div>
|
| 107 |
-
|
| 108 |
</div>
|
| 109 |
|
| 110 |
<script>
|
| 111 |
const $ = id => document.getElementById(id);
|
| 112 |
-
let audioCtx, recorderChunks = [], mediaRecorder, analyser;
|
| 113 |
|
| 114 |
-
async function
|
| 115 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 116 |
const data = await r.json();
|
| 117 |
-
|
| 118 |
-
|
| 119 |
-
|
| 120 |
-
}
|
| 121 |
-
async function loadHistory(){
|
| 122 |
-
const r = await fetch('/api/history?limit=20');
|
| 123 |
-
const data = await r.json();
|
| 124 |
-
$('history').querySelector('tbody').innerHTML = data.map(row =>
|
| 125 |
-
`<tr><td>${row.id}</td><td>${row.created?.slice(0,19)||''}</td><td style="color:#00ff88">${row.fartscore}</td><td>${row.note_count}</td><td style="font-size:.7rem">${row.solana_pub?.slice(0,20)}...</td></tr>`
|
| 126 |
-
).join('');
|
| 127 |
-
}
|
| 128 |
-
|
| 129 |
-
$('recordBtn').onclick = async () => {
|
| 130 |
-
const stream = await navigator.mediaDevices.getUserMedia({audio:true});
|
| 131 |
-
audioCtx = new AudioContext({sampleRate:16000});
|
| 132 |
-
const src = audioCtx.createMediaStreamSource(stream);
|
| 133 |
-
analyser = audioCtx.createAnalyser();
|
| 134 |
-
src.connect(analyser);
|
| 135 |
-
recorderChunks = [];
|
| 136 |
-
mediaRecorder = new MediaRecorder(stream);
|
| 137 |
-
mediaRecorder.ondataavailable = e => { if(e.data.size) recorderChunks.push(e.data); };
|
| 138 |
-
mediaRecorder.onstop = () => processRecording(new Blob(recorderChunks, {type:'audio/webm'}));
|
| 139 |
-
mediaRecorder.start(100);
|
| 140 |
-
$('recordBtn').disabled = true;
|
| 141 |
-
$('stopBtn').disabled = false;
|
| 142 |
-
$('status').textContent = 'Recording...'; $('status').className = 'rec';
|
| 143 |
-
};
|
| 144 |
-
$('stopBtn').onclick = () => { mediaRecorder?.stop(); $('recordBtn').disabled = false; $('stopBtn').disabled = true; $('status').textContent = 'Processing...'; };
|
| 145 |
-
$('uploadBtn').onclick = () => $('uploadInput').click();
|
| 146 |
-
$('uploadInput').onchange = e => { const f = e.target.files[0]; if(f) uploadFile(f); };
|
| 147 |
-
|
| 148 |
-
async function processRecording(blob){
|
| 149 |
-
const ab = await blob.arrayBuffer();
|
| 150 |
-
const buf = await audioCtx.decodeAudioData(ab);
|
| 151 |
-
const mono = buf.numberOfChannels > 1 ? mixToMono(buf) : buf.getChannelData(0);
|
| 152 |
-
const wav = encodeWav(mono, 16000);
|
| 153 |
-
const file = new File([wav], 'fart.wav', {type:'audio/wav'});
|
| 154 |
-
uploadFile(file);
|
| 155 |
-
}
|
| 156 |
-
|
| 157 |
-
function mixToMono(buf){
|
| 158 |
-
const L = buf.getChannelData(0), R = buf.getChannelData(1), out = new Float32Array(L.length);
|
| 159 |
-
for(let i=0;i<L.length;i++) out[i] = (L[i]+R[i])/2;
|
| 160 |
-
return out;
|
| 161 |
-
}
|
| 162 |
-
|
| 163 |
-
function encodeWav(samples, sr){
|
| 164 |
-
const n = samples.length;
|
| 165 |
-
const buffer = new ArrayBuffer(44 + n * 2);
|
| 166 |
-
const v = new DataView(buffer);
|
| 167 |
-
const writeStr = (off, s) => { for(let i=0;i<s.length;i++) v.setUint8(off+i, s.charCodeAt(i)); };
|
| 168 |
-
writeStr(0,'RIFF'); v.setUint32(4,36+n*2,true); writeStr(8,'WAVE');
|
| 169 |
-
writeStr(12,'fmt '); v.setUint32(16,16,true); v.setUint16(20,1,true); v.setUint16(22,1,true);
|
| 170 |
-
v.setUint32(24,sr,true); v.setUint32(28,sr*2,true); v.setUint16(32,2,true); v.setUint16(34,16,true);
|
| 171 |
-
writeStr(36,'data'); v.setUint32(40,n*2,true);
|
| 172 |
-
for(let i=0;i<n;i++){ let s = Math.max(-1,Math.min(1,samples[i])); v.setInt16(44+i*2,s<0?s*0x8000:s*0x7FFF,true); }
|
| 173 |
-
return new Blob([buffer],{type:'audio/wav'});
|
| 174 |
}
|
| 175 |
|
| 176 |
-
async function
|
| 177 |
-
|
| 178 |
-
|
| 179 |
-
|
| 180 |
-
|
| 181 |
-
|
| 182 |
-
|
| 183 |
-
|
| 184 |
-
|
| 185 |
-
|
| 186 |
-
function showResults(d){
|
| 187 |
-
$('resultsCard').style.display = 'block';
|
| 188 |
-
$('mDuration').textContent = d.duration_sec + 's';
|
| 189 |
-
$('mNotes').textContent = d.note_count;
|
| 190 |
-
$('mScore').textContent = d.fartscore;
|
| 191 |
-
$('mSR').textContent = d.sample_rate + ' Hz';
|
| 192 |
-
$('walletPub').textContent = d.solana_public;
|
| 193 |
-
$('walletPriv').textContent = d.solana_private;
|
| 194 |
-
$('status').textContent = 'Done'; $('status').className = 'ok';
|
| 195 |
-
|
| 196 |
-
// Score ring
|
| 197 |
-
const pct = d.fartscore / 100;
|
| 198 |
-
const circ = 2 * Math.PI * 50;
|
| 199 |
-
$('scoreCircle').style.strokeDashoffset = circ * (1 - pct);
|
| 200 |
-
$('scoreText').textContent = d.fartscore;
|
| 201 |
-
|
| 202 |
-
// Piano roll
|
| 203 |
-
drawPianoRoll(d.notes || []);
|
| 204 |
-
|
| 205 |
-
// MIDI download
|
| 206 |
-
$('dlMidi').onclick = () => { window.location = `/api/midi/${d.audio_hash.slice(0,16)}`; };
|
| 207 |
-
$('playMidi').onclick = () => playNotes(d.notes || []);
|
| 208 |
-
|
| 209 |
-
loadLeaderboard(); loadHistory();
|
| 210 |
}
|
| 211 |
|
| 212 |
-
function
|
| 213 |
-
|
| 214 |
-
|
| 215 |
-
|
| 216 |
-
|
| 217 |
-
|
| 218 |
-
|
| 219 |
-
|
| 220 |
-
|
| 221 |
-
|
| 222 |
-
|
| 223 |
-
|
| 224 |
-
|
| 225 |
-
|
| 226 |
-
|
| 227 |
-
ctx.fillRect(x, y, Math.max(w, 2), h);
|
| 228 |
-
ctx.globalAlpha = 1;
|
| 229 |
-
});
|
| 230 |
}
|
| 231 |
|
| 232 |
-
function
|
| 233 |
-
|
| 234 |
-
|
| 235 |
-
|
| 236 |
-
|
| 237 |
-
|
| 238 |
-
|
| 239 |
-
|
| 240 |
-
|
| 241 |
-
o.start(ctx.currentTime + n.start);
|
| 242 |
-
g.gain.exponentialRampToValueAtTime(0.001, ctx.currentTime + n.start + n.dur);
|
| 243 |
-
o.stop(ctx.currentTime + n.start + n.dur);
|
| 244 |
-
});
|
| 245 |
}
|
| 246 |
|
| 247 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 248 |
</script>
|
| 249 |
</body>
|
| 250 |
</html>
|
|
|
|
| 3 |
<head>
|
| 4 |
<meta charset="UTF-8">
|
| 5 |
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
| 6 |
+
<title>AFIP — Acoustic Flatulence Intelligence Platform</title>
|
| 7 |
<style>
|
| 8 |
+
:root { --bg: #0a0a0f; --surface: #14141a; --border: #2a2a35; --text: #e2e2e8; --text-muted: #888898; --accent: #3b82f6; --success: #22c55e; --warn: #eab308; }
|
| 9 |
+
* { box-sizing: border-box; margin: 0; }
|
| 10 |
+
body { background: var(--bg); color: var(--text); font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif; padding: 40px 24px; }
|
| 11 |
+
.container { max-width: 720px; margin: 0 auto; }
|
| 12 |
+
header { text-align: center; margin-bottom: 40px; }
|
| 13 |
+
h1 { font-size: 2rem; margin-bottom: 8px; }
|
| 14 |
+
p { color: var(--text-muted); }
|
| 15 |
+
.actions { display: flex; gap: 12px; justify-content: center; margin-bottom: 32px; flex-wrap: wrap; }
|
| 16 |
+
button {
|
| 17 |
+
background: linear-gradient(135deg, var(--accent), #8b5cf6); color: #fff; border: none;
|
| 18 |
+
padding: 12px 28px; border-radius: 10px; font-size: 1rem; cursor: pointer; transition: transform 0.15s;
|
| 19 |
+
}
|
| 20 |
+
button:hover { transform: translateY(-2px); }
|
| 21 |
+
button.secondary { background: var(--surface); border: 1px solid var(--border); color: var(--text); }
|
| 22 |
+
.dropzone {
|
| 23 |
+
border: 2px dashed var(--border); border-radius: 12px; padding: 48px 24px;
|
| 24 |
+
text-align: center; background: var(--surface); cursor: pointer; transition: border-color 0.2s;
|
| 25 |
+
}
|
| 26 |
+
.dropzone:hover, .dropzone.dragover { border-color: var(--accent); }
|
| 27 |
+
.dropzone input { display: none; }
|
| 28 |
+
.result { background: var(--surface); border: 1px solid var(--border); border-radius: 12px; padding: 20px; margin-top: 24px; display: none; }
|
| 29 |
+
.result.show { display: block; }
|
| 30 |
+
.result h3 { margin-bottom: 12px; }
|
| 31 |
+
.grid { display: grid; grid-template-columns: 1fr 1fr; gap: 12px; }
|
| 32 |
+
.kv { display: flex; justify-content: space-between; padding: 8px 0; border-bottom: 1px solid var(--border); font-size: 0.9rem; }
|
| 33 |
+
.kv .key { color: var(--text-muted); }
|
| 34 |
+
.badge { background: rgba(34,197,94,0.15); color: var(--success); padding: 2px 10px; border-radius: 999px; font-size: 0.75rem; }
|
| 35 |
+
.notes { max-height: 200px; overflow-y: auto; font-family: ui-monospace, monospace; font-size: 0.82rem; margin-top: 12px; }
|
| 36 |
+
.note-row { padding: 4px 0; border-bottom: 1px solid var(--border); display: flex; gap: 12px; }
|
| 37 |
+
.note-row span { color: var(--text-muted); }
|
| 38 |
+
.loading { text-align: center; color: var(--text-muted); margin-top: 20px; display: none; }
|
| 39 |
+
.leaderboard { margin-top: 32px; }
|
| 40 |
+
.leaderboard h3 { margin-bottom: 12px; }
|
| 41 |
+
.lb-row { display: flex; justify-content: space-between; padding: 10px 0; border-bottom: 1px solid var(--border); font-size: 0.9rem; }
|
| 42 |
+
.lb-row .score { color: var(--success); font-weight: 600; }
|
| 43 |
</style>
|
| 44 |
</head>
|
| 45 |
<body>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 46 |
<div class="container">
|
| 47 |
+
<header>
|
| 48 |
+
<h1>💨 AFIP</h1>
|
| 49 |
+
<p>Acoustic Flatulence Intelligence Platform v3.0<br>Upload a .wav — get a FartScore, MIDI, and Solana wallet.</p>
|
| 50 |
+
</header>
|
| 51 |
+
|
| 52 |
+
<div class="actions">
|
| 53 |
+
<button onclick="document.getElementById('fileInput').click()">📁 Upload WAV</button>
|
| 54 |
+
<button class="secondary" onclick="runMock()">🎲 Generate Mock</button>
|
| 55 |
+
<button class="secondary" onclick="loadLeaderboard()">🏆 Leaderboard</button>
|
| 56 |
+
</div>
|
| 57 |
+
|
| 58 |
+
<div class="dropzone" id="dropzone" onclick="document.getElementById('fileInput').click()">
|
| 59 |
+
<input type="file" id="fileInput" accept=".wav" onchange="handleFile(this.files[0])">
|
| 60 |
+
<div style="font-size: 3rem; margin-bottom: 8px;">🎵</div>
|
| 61 |
+
<h3>Drop a WAV file</h3>
|
| 62 |
+
<p style="color: var(--text-muted);">or click to browse</p>
|
| 63 |
+
</div>
|
| 64 |
+
|
| 65 |
+
<div class="loading" id="loading">Analyzing...</div>
|
| 66 |
+
|
| 67 |
+
<div class="result" id="result">
|
| 68 |
+
<h3>Analysis Result <span class="badge" id="scoreBadge">FartScore: 0</span></h3>
|
| 69 |
+
<div class="grid" id="grid"></div>
|
| 70 |
+
<div class="notes" id="notes"></div>
|
| 71 |
+
<div style="margin-top: 12px;">
|
| 72 |
+
<a id="midiLink" href="#" style="color: var(--accent); text-decoration: none;">⬇ Download MIDI</a>
|
| 73 |
+
</div>
|
| 74 |
+
</div>
|
| 75 |
+
|
| 76 |
+
<div class="leaderboard" id="leaderboard" style="display:none;">
|
| 77 |
+
<h3>🏆 Leaderboard</h3>
|
| 78 |
+
<div id="lbList"></div>
|
| 79 |
+
</div>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 80 |
</div>
|
| 81 |
|
| 82 |
<script>
|
| 83 |
const $ = id => document.getElementById(id);
|
|
|
|
| 84 |
|
| 85 |
+
async function handleFile(file) {
|
| 86 |
+
if (!file) return;
|
| 87 |
+
$('loading').style.display = 'block';
|
| 88 |
+
$('result').classList.remove('show');
|
| 89 |
+
const fd = new FormData();
|
| 90 |
+
fd.append('file', file);
|
| 91 |
+
try {
|
| 92 |
+
const r = await fetch('/api/analyze', { method: 'POST', body: fd });
|
| 93 |
const data = await r.json();
|
| 94 |
+
showResult(data);
|
| 95 |
+
} catch (e) { alert('Error: ' + e.message); }
|
| 96 |
+
$('loading').style.display = 'none';
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 97 |
}
|
| 98 |
|
| 99 |
+
async function runMock() {
|
| 100 |
+
$('loading').style.display = 'block';
|
| 101 |
+
$('result').classList.remove('show');
|
| 102 |
+
try {
|
| 103 |
+
const r = await fetch('/api/mock', { method: 'POST' });
|
| 104 |
+
const data = await r.json();
|
| 105 |
+
showResult(data);
|
| 106 |
+
} catch (e) { alert('Error: ' + e.message); }
|
| 107 |
+
$('loading').style.display = 'none';
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 108 |
}
|
| 109 |
|
| 110 |
+
function showResult(data) {
|
| 111 |
+
$('scoreBadge').textContent = 'FartScore: ' + data.fartscore;
|
| 112 |
+
$('grid').innerHTML = `
|
| 113 |
+
<div class="kv"><span class="key">Duration</span><span>${data.duration_sec}s</span></div>
|
| 114 |
+
<div class="kv"><span class="key">Sample Rate</span><span>${data.sample_rate} Hz</span></div>
|
| 115 |
+
<div class="kv"><span class="key">Frames</span><span>${data.frame_count}</span></div>
|
| 116 |
+
<div class="kv"><span class="key">Notes</span><span>${data.note_count}</span></div>
|
| 117 |
+
<div class="kv"><span class="key">Solana Pub</span><span style="font-size:0.78rem;">${data.solana_public.substring(0,24)}...</span></div>
|
| 118 |
+
<div class="kv"><span class="key">Audio Hash</span><span style="font-size:0.78rem;">${data.audio_hash.substring(0,24)}...</span></div>
|
| 119 |
+
`;
|
| 120 |
+
$('notes').innerHTML = (data.notes || []).map(n =>
|
| 121 |
+
`<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>`
|
| 122 |
+
).join('');
|
| 123 |
+
$('midiLink').href = '/api/midi/' + data.audio_hash.substring(0,16);
|
| 124 |
+
$('result').classList.add('show');
|
|
|
|
|
|
|
|
|
|
| 125 |
}
|
| 126 |
|
| 127 |
+
async function loadLeaderboard() {
|
| 128 |
+
try {
|
| 129 |
+
const r = await fetch('/api/leaderboard');
|
| 130 |
+
const data = await r.json();
|
| 131 |
+
$('lbList').innerHTML = data.map((row, i) =>
|
| 132 |
+
`<div class="lb-row"><span>#${i+1} ${row.id}</span><span class="score">${row.fartscore}</span></div>`
|
| 133 |
+
).join('');
|
| 134 |
+
$('leaderboard').style.display = 'block';
|
| 135 |
+
} catch (e) { console.error(e); }
|
|
|
|
|
|
|
|
|
|
|
|
|
| 136 |
}
|
| 137 |
|
| 138 |
+
const dz = $('dropzone');
|
| 139 |
+
dz.addEventListener('dragover', e => { e.preventDefault(); dz.classList.add('dragover'); });
|
| 140 |
+
dz.addEventListener('dragleave', () => dz.classList.remove('dragover'));
|
| 141 |
+
dz.addEventListener('drop', e => {
|
| 142 |
+
e.preventDefault(); dz.classList.remove('dragover');
|
| 143 |
+
const f = e.dataTransfer.files[0];
|
| 144 |
+
if (f && f.name.endsWith('.wav')) handleFile(f);
|
| 145 |
+
});
|
| 146 |
</script>
|
| 147 |
</body>
|
| 148 |
</html>
|