josephrw commited on
Commit
cbce236
·
verified ·
1 Parent(s): 9cfffde

Upload static/index.html with huggingface_hub

Browse files
Files changed (1) hide show
  1. 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 v3 — Acoustic Flatulence Intelligence Platform</title>
7
  <style>
8
- *{margin:0;padding:0;box-sizing:border-box}
9
- body{font-family:'SF Mono',Monaco,monospace;background:#0a0a0a;color:#d4d4d4;line-height:1.5;min-height:100vh}
10
- header{background:#111;border-bottom:1px solid #333;padding:1rem 2rem;display:flex;align-items:center;gap:1rem}
11
- header h1{color:#00ff88;font-size:1.3rem;letter-spacing:2px}
12
- header .badge{font-size:.7rem;background:#333;color:#aaa;padding:2px 8px;border-radius:3px}
13
- .container{max-width:1100px;margin:0 auto;padding:2rem}
14
- .card{background:#141414;border:1px solid #2a2a2a;border-radius:6px;padding:1.5rem;margin-bottom:1.5rem}
15
- .card h2{color:#00ff88;font-size:.9rem;text-transform:uppercase;letter-spacing:1px;margin-bottom:1rem;border-bottom:1px solid #2a2a2a;padding-bottom:.5rem}
16
- .btn{background:#00ff88;color:#000;border:none;padding:.6rem 1.2rem;font-family:inherit;font-size:.85rem;cursor:pointer;border-radius:3px;transition:opacity .2s}
17
- .btn:hover{opacity:.85}
18
- .btn:disabled{background:#333;color:#888;cursor:not-allowed}
19
- .btn-red{background:#ff4444;color:#fff}
20
- .record-row{display:flex;gap:1rem;align-items:center;flex-wrap:wrap;margin-bottom:1rem}
21
- #status{color:#aaa;font-size:.8rem}
22
- #status.rec{color:#ff4444}
23
- #status.ok{color:#00ff88}
24
- .results{display:none}
25
- .metric-grid{display:grid;grid-template-columns:repeat(auto-fit,minmax(200px,1fr));gap:1rem;margin-bottom:1rem}
26
- .metric{background:#0f0f0f;border:1px solid #222;padding:1rem;border-radius:4px}
27
- .metric-label{font-size:.7rem;color:#888;text-transform:uppercase}
28
- .metric-value{font-size:1.4rem;color:#00ff88;margin-top:.3rem}
29
- .wallet-box{background:#0f0f0f;border:1px solid #00ff88;padding:1rem;border-radius:4px;word-break:break-all;font-size:.8rem}
30
- .wallet-box .label{color:#888;font-size:.7rem;text-transform:uppercase}
31
- .wallet-box .key{color:#00ff88;margin-top:.3rem}
32
- .score-ring{width:120px;height:120px;margin:1rem auto;position:relative}
33
- .score-ring svg{transform:rotate(-90deg)}
34
- .score-ring text{position:absolute;top:50%;left:50%;transform:translate(-50%,-50%);font-size:1.8rem;color:#00ff88}
35
- .piano-roll{width:100%;height:150px;background:#0a0a0a;border:1px solid #222;margin:1rem 0}
36
- table{width:100%;border-collapse:collapse;font-size:.8rem}
37
- th,td{padding:.5rem;text-align:left;border-bottom:1px solid #222}
38
- th{color:#888;text-transform:uppercase;font-size:.7rem}
39
- tr:hover{background:#151515}
40
- a{color:#00ff88;text-decoration:none}
41
- a:hover{text-decoration:underline}
42
- #uploadInput{display:none}
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
- <div class="card">
54
- <h2>Audio Input</h2>
55
- <div class="record-row">
56
- <button id="recordBtn" class="btn btn-red">● Record</button>
57
- <button id="stopBtn" class="btn" disabled>■ Stop</button>
58
- <button id="uploadBtn" class="btn">Upload WAV</button>
59
- <input type="file" id="uploadInput" accept="audio/wav">
60
- <span id="status">Ready</span>
61
- </div>
62
- </div>
63
-
64
- <div class="card results" id="resultsCard">
65
- <h2>Analysis Report</h2>
66
- <div class="metric-grid">
67
- <div class="metric"><div class="metric-label">Duration</div><div class="metric-value" id="mDuration">—</div></div>
68
- <div class="metric"><div class="metric-label">Notes Detected</div><div class="metric-value" id="mNotes">—</div></div>
69
- <div class="metric"><div class="metric-label">FartScore</div><div class="metric-value" id="mScore">—</div></div>
70
- <div class="metric"><div class="metric-label">Sample Rate</div><div class="metric-value" id="mSR"></div></div>
71
- </div>
72
-
73
- <div style="display:flex;gap:2rem;flex-wrap:wrap;align-items:flex-start">
74
- <div style="flex:1;min-width:280px">
75
- <div class="wallet-box">
76
- <div class="label">Solana Wallet (Deterministic)</div>
77
- <div class="key" id="walletPub"></div>
78
- <div class="label" style="margin-top:.8rem">Private Key</div>
79
- <div class="key" style="color:#ff4444" id="walletPriv">—</div>
80
- </div>
81
- <div style="margin-top:1rem">
82
- <button id="dlMidi" class="btn">Download MIDI</button>
83
- <button id="playMidi" class="btn">Play Preview</button>
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 loadLeaderboard(){
115
- const r = await fetch('/api/leaderboard');
 
 
 
 
 
 
116
  const data = await r.json();
117
- $('leaderboard').querySelector('tbody').innerHTML = data.map((row,i) =>
118
- `<tr><td>#${i+1}</td><td>${row.created?.slice(0,19)||''}</td><td style="color:#00ff88;font-weight:bold">${row.fartscore}</td><td style="font-size:.7rem">${row.solana_pub}</td></tr>`
119
- ).join('');
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 uploadFile(file){
177
- $('status').textContent = 'Uploading...';
178
- const fd = new FormData(); fd.append('file', file);
179
- try{
180
- const r = await fetch('/api/analyze',{method:'POST',body:fd});
181
- const data = await r.json();
182
- showResults(data);
183
- }catch(e){ $('status').textContent = 'Error: '+e.message; }
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 drawPianoRoll(notes){
213
- const c = $('pianoRoll'), ctx = c.getContext('2d');
214
- c.width = c.offsetWidth; c.height = 150;
215
- ctx.fillStyle = '#0a0a0a'; ctx.fillRect(0,0,c.width,c.height);
216
- if(!notes.length) return;
217
- const maxT = Math.max(...notes.map(n=>n.start+n.dur), 1);
218
- const minN = Math.min(...notes.map(n=>n.note), 20);
219
- const maxN = Math.max(...notes.map(n=>n.note), 100);
220
- const nRange = maxN - minN + 5;
221
- notes.forEach(n => {
222
- const x = (n.start / maxT) * c.width;
223
- const w = (n.dur / maxT) * c.width;
224
- const y = c.height - ((n.note - minN + 2) / nRange) * c.height;
225
- const h = c.height / nRange;
226
- ctx.fillStyle = '#00ff88'; ctx.globalAlpha = 0.7;
227
- ctx.fillRect(x, y, Math.max(w, 2), h);
228
- ctx.globalAlpha = 1;
229
- });
230
  }
231
 
232
- function playNotes(notes){
233
- const ctx = new AudioContext();
234
- notes.forEach(n => {
235
- const o = ctx.createOscillator();
236
- const g = ctx.createGain();
237
- o.connect(g); g.connect(ctx.destination);
238
- o.type = 'sine';
239
- o.frequency.value = 440 * Math.pow(2, (n.note - 69)/12);
240
- g.gain.value = n.vel / 127 * 0.3;
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
- loadLeaderboard(); loadHistory();
 
 
 
 
 
 
 
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>