| <!DOCTYPE html> |
| <html lang="en"> |
| <head> |
| <meta charset="UTF-8"> |
| <meta name="viewport" content="width=device-width, initial-scale=1.0"> |
| <title>AFIP v3 — Acoustic Flatulence Intelligence Platform</title> |
| <style> |
| :root{--bg:#05080f;--panel:#0d1117;--border:#21262d;--text:#c9d1d9;--muted:#8b949e;--accent:#58a6ff;--accent2:#3fb950;--red:#f85149;--orange:#d29922;--mono:'SF Mono',ui-monospace,monospace;} |
| *{box-sizing:border-box;margin:0;padding:0} |
| body{font-family:var(--mono);background:var(--bg);color:var(--text);line-height:1.6;overflow-x:hidden} |
| a{color:var(--accent);text-decoration:none} |
| @keyframes fadeIn{from{opacity:0;transform:translateY(12px)}to{opacity:1;transform:translateY(0)}} |
| @keyframes pulse{0%,100%{opacity:1}50%{opacity:.5}} |
| @keyframes scanline{0%{transform:translateY(-100%)}100%{transform:translateY(100%)}} |
| @keyframes glow{0%,100%{box-shadow:0 0 8px rgba(88,166,255,.15)}50%{box-shadow:0 0 20px rgba(88,166,255,.35)}} |
| @keyframes type{from{width:0}to{width:100%}} |
| @keyframes blink{0%,100%{opacity:1}50%{opacity:0}} |
| @keyframes spin{to{transform:rotate(360deg)}} |
|
|
| /* Header */ |
| header{background:var(--panel);border-bottom:1px solid var(--border);padding:14px 28px;display:flex;align-items:center;justify-content:space-between;position:sticky;top:0;z-index:200} |
| header h1{font-size:13px;font-weight:700;color:var(--accent2);letter-spacing:2px;text-transform:uppercase} |
| header .badge{font-size:10px;background:var(--border);padding:3px 10px;border-radius:4px;color:var(--muted);letter-spacing:.5px} |
|
|
| /* Hero Landing */ |
| .hero{min-height:40vh;display:flex;flex-direction:column;align-items:center;justify-content:center;text-align:center;padding:40px 20px;border-bottom:1px solid var(--border);position:relative;overflow:hidden} |
| .hero::before{content:'';position:absolute;top:0;left:0;right:0;bottom:0;background:radial-gradient(circle at 50% 50%,rgba(88,166,255,.04) 0%,transparent 70%);pointer-events:none} |
| .hero h2{font-size:32px;font-weight:700;margin-bottom:12px;color:var(--text);letter-spacing:-1px} |
| .hero .typewriter{font-size:14px;color:var(--muted);max-width:600px;margin:0 auto 28px;overflow:hidden;white-space:nowrap;border-right:2px solid var(--accent2);animation:type 3s steps(60,end),blink .8s step-end infinite;animation-fill-mode:forwards} |
| .hero .stats{display:flex;gap:32px;justify-content:center;flex-wrap:wrap;margin-bottom:24px} |
| .hero .stat{text-align:center} |
| .hero .stat .num{font-size:28px;font-weight:700;color:var(--accent)} |
| .hero .stat .lbl{font-size:10px;color:var(--muted);text-transform:uppercase;letter-spacing:1px} |
| .btn-primary{background:var(--accent);color:#000;border:none;padding:12px 28px;font-family:var(--mono);font-size:13px;font-weight:700;border-radius:6px;cursor:pointer;transition:all .2s} |
| .btn-primary:hover{background:#79b8ff;transform:translateY(-1px)} |
| .btn-secondary{background:transparent;color:var(--accent);border:1px solid var(--accent);padding:10px 20px;font-family:var(--mono);font-size:12px;border-radius:6px;cursor:pointer;transition:all .2s} |
| .btn-secondary:hover{background:rgba(88,166,255,.08)} |
|
|
| /* Layout */ |
| .container{max-width:1300px;margin:0 auto;padding:24px;display:none;animation:fadeIn .5s ease} |
| .container.visible{display:grid;grid-template-columns:300px 1fr;gap:24px} |
| @media(max-width:900px){.container.visible{grid-template-columns:1fr}} |
|
|
| /* Sidebar */ |
| .sidebar{background:var(--panel);border:1px solid var(--border);border-radius:10px;padding:20px;height:fit-content;position:sticky;top:70px} |
| .sidebar h3{font-size:11px;text-transform:uppercase;color:var(--muted);margin-bottom:16px;letter-spacing:1.5px} |
| .sidebar .btn{width:100%;padding:10px 14px;border:1px solid var(--border);background:var(--bg);color:var(--text);font-family:var(--mono);font-size:12px;cursor:pointer;border-radius:6px;margin-bottom:8px;transition:all .15s;text-align:left;display:flex;align-items:center;gap:8px} |
| .sidebar .btn:hover{border-color:var(--accent);color:var(--accent)} |
| .sidebar .btn.rec{background:var(--red);border-color:var(--red);color:#fff;animation:pulse 1.2s infinite} |
| .sidebar .status{margin-top:16px;padding-top:14px;border-top:1px solid var(--border);font-size:11px;color:var(--muted)} |
| .sidebar .status .row{display:flex;justify-content:space-between;margin-bottom:6px} |
| .sidebar .status .val{color:var(--text)} |
| .sidebar .status .val.live{color:var(--accent2)} |
|
|
| /* Main */ |
| .main{background:var(--panel);border:1px solid var(--border);border-radius:10px;padding:24px} |
| .tabs{display:flex;gap:6px;margin-bottom:20px;border-bottom:1px solid var(--border);padding-bottom:12px} |
| .tab{padding:7px 16px;font-family:var(--mono);font-size:11px;background:transparent;border:1px solid transparent;color:var(--muted);cursor:pointer;border-radius:6px;text-transform:uppercase;letter-spacing:.5px;transition:all .15s} |
| .tab.active{background:var(--bg);border-color:var(--border);color:var(--text)} |
| .tab:hover{color:var(--text)} |
| .section{display:none} |
| .section.active{display:block;animation:fadeIn .4s ease} |
|
|
| /* Dropzone */ |
| .dropzone{border:2px dashed var(--border);border-radius:12px;padding:56px 24px;text-align:center;cursor:pointer;transition:all .25s;margin-bottom:20px;position:relative} |
| .dropzone:hover{border-color:var(--accent);background:rgba(88,166,255,.02);animation:glow 2s infinite} |
| .dropzone.dragover{border-color:var(--accent2);background:rgba(63,185,80,.03)} |
| .dropzone .icon{font-size:36px;margin-bottom:12px;opacity:.6} |
| .dropzone h3{font-size:15px;margin-bottom:6px} |
| .dropzone p{font-size:11px;color:var(--muted)} |
|
|
| /* Loading */ |
| .loading{display:none;text-align:center;padding:48px;color:var(--muted);font-size:12px} |
| .loading.active{display:flex;flex-direction:column;align-items:center;gap:12px} |
| .spinner{width:18px;height:18px;border:2px solid var(--border);border-top-color:var(--accent);border-radius:50%;animation:spin .8s linear infinite} |
|
|
| /* Error */ |
| .error{display:none;background:rgba(248,81,73,.08);border:1px solid var(--red);color:var(--red);padding:12px 16px;border-radius:8px;font-size:12px;margin-bottom:16px} |
| .error.active{display:block} |
|
|
| /* Results */ |
| .result-grid{display:grid;grid-template-columns:repeat(auto-fit,minmax(240px,1fr));gap:16px;margin-bottom:20px} |
| .r-card{background:var(--bg);border:1px solid var(--border);border-radius:8px;padding:18px;transition:all .2s} |
| .r-card:hover{border-color:var(--accent)} |
| .r-card h4{font-size:10px;text-transform:uppercase;color:var(--muted);margin-bottom:10px;letter-spacing:1.2px;display:flex;align-items:center;gap:8px} |
| .r-card h4::before{content:'';display:inline-block;width:5px;height:5px;background:var(--accent2);border-radius:50%} |
| .r-card .metric{font-size:28px;font-weight:700;color:var(--accent);margin-bottom:4px} |
| .r-card .label{font-size:10px;color:var(--muted);text-transform:uppercase;letter-spacing:1px} |
| .r-card .value{font-size:11px;color:var(--text);word-break:break-all;font-family:var(--mono)} |
| .gauge{height:6px;background:var(--border);border-radius:3px;overflow:hidden;margin-top:10px} |
| .gauge-fill{height:100%;background:linear-gradient(90deg,var(--accent2),var(--accent));border-radius:3px;transition:width 1s ease} |
|
|
| /* Canvas */ |
| .canvas-wrap{background:var(--bg);border:1px solid var(--border);border-radius:8px;padding:14px;margin-bottom:16px} |
| .canvas-wrap h4{font-size:10px;text-transform:uppercase;color:var(--muted);margin-bottom:10px;letter-spacing:1.2px} |
| canvas{display:block;width:100%;height:150px;background:var(--bg)} |
|
|
| /* MIDI */ |
| .midi-player{display:flex;gap:10px;align-items:center;margin:16px 0;flex-wrap:wrap} |
|
|
| /* Receipt */ |
| .receipt{background:var(--bg);border:1px solid var(--border);border-radius:8px;padding:14px;margin-top:14px;font-size:11px} |
| .receipt .line{display:flex;justify-content:space-between;padding:5px 0;border-bottom:1px solid var(--border)} |
| .receipt .line:last-child{border-bottom:none} |
| .receipt .hash{color:var(--accent2);font-family:var(--mono);font-size:10px} |
| .receipt .lbl{color:var(--muted)} |
|
|
| /* Collapsible */ |
| .collapsible{cursor:pointer;padding:10px 0;border-bottom:1px solid var(--border);font-size:12px;display:flex;justify-content:space-between;align-items:center;transition:color .15s} |
| .collapsible:hover{color:var(--accent)} |
| .collapsible::after{content:'+';color:var(--muted);font-size:14px;transition:transform .2s} |
| .collapsible.open::after{content:'\2212'} |
| .collapsible-content{display:none;padding:10px 0;font-size:11px;color:var(--muted);animation:fadeIn .3s ease} |
| .collapsible-content.open{display:block} |
| .model-row{display:flex;justify-content:space-between;padding:5px 0;border-bottom:1px solid var(--border);font-size:11px} |
| .model-row .role{color:var(--text);font-size:10px} |
|
|
| /* Tables */ |
| .tbl{width:100%;font-size:11px;border-collapse:collapse} |
| .tbl th{text-align:left;padding:10px 8px;color:var(--muted);border-bottom:1px solid var(--border);font-weight:400;text-transform:uppercase;letter-spacing:.5px;font-size:10px} |
| .tbl td{padding:10px 8px;border-bottom:1px solid var(--border);color:var(--text)} |
| .tbl tr:hover{background:rgba(88,166,255,.03)} |
|
|
| /* Footer */ |
| .footer{text-align:center;padding:30px;color:var(--muted);font-size:11px;border-top:1px solid var(--border);margin-top:40px} |
| </style> |
| </head> |
| <body> |
|
|
| |
| <div id="landing" class="hero"> |
| <h2>Acoustic Flatulence Intelligence Platform</h2> |
| <div class="typewriter">36-model ensemble · YIN pitch detection · SMF MIDI generation · SHA-256 receipt ledger · Solana fingerprinting</div> |
| <div class="stats"> |
| <div class="stat"><div class="num">36</div><div class="lbl">Models</div></div> |
| <div class="stat"><div class="num">YIN</div><div class="lbl">Pitch Engine</div></div> |
| <div class="stat"><div class="num">SMF</div><div class="lbl">MIDI Writer</div></div> |
| <div class="stat"><div class="num">SHA-256</div><div class="lbl">Receipt Chain</div></div> |
| </div> |
| <div style="display:flex;gap:12px;flex-wrap:wrap;justify-content:center"> |
| <button class="btn-primary" onclick="enterApp()">Enter Terminal</button> |
| <button class="btn-secondary" onclick="demoMock()">Run Mock Analysis</button> |
| </div> |
| </div> |
|
|
| |
| <div id="app" class="container"> |
| <aside class="sidebar"> |
| <h3>Operations</h3> |
| <button class="btn" id="btnRecord" onclick="toggleRecord()">● Record Audio</button> |
| <button class="btn" id="btnUpload" onclick="document.getElementById('fileIn').click()">↑ Upload File</button> |
| <input type="file" id="fileIn" style="display:none" accept="audio/*" onchange="handleFile(this.files[0])"> |
| <button class="btn" onclick="switchTab('registry')">⊞ Model Registry</button> |
| <button class="btn" onclick="switchTab('history')">◔ History</button> |
| <button class="btn" onclick="switchTab('leaderboard')">▲ Leaderboard</button> |
| <button class="btn" onclick="demoMock()">⚡ Mock Demo</button> |
| <div class="status"> |
| <div class="row"><span>Status</span><span class="val live" id="connStatus">ONLINE</span></div> |
| <div class="row"><span>Latency</span><span class="val" id="latency">-- ms</span></div> |
| <div class="row"><span>Models</span><span class="val">36 loaded</span></div> |
| <div class="row"><span>DB</span><span class="val">afip.db</span></div> |
| </div> |
| </aside> |
|
|
| <main class="main"> |
| <div class="tabs"> |
| <button class="tab active" data-tab="analyze" onclick="switchTab('analyze')">Analyze</button> |
| <button class="tab" data-tab="registry" onclick="switchTab('registry')">Registry</button> |
| <button class="tab" data-tab="history" onclick="switchTab('history')">History</button> |
| <button class="tab" data-tab="leaderboard" onclick="switchTab('leaderboard')">Leaderboard</button> |
| </div> |
|
|
| |
| <div id="tab-analyze" class="section active"> |
| <div class="dropzone" id="dropzone" ondragover="event.preventDefault();this.classList.add('dragover')" ondragleave="this.classList.remove('dragover')" ondrop="handleDrop(event)"> |
| <div class="icon">📡</div> |
| <h3>Drop audio file or click to browse</h3> |
| <p>WAV · WebM · OGG · MP3 | Max 10MB</p> |
| </div> |
| <div class="loading" id="loading"><div class="spinner"></div><div>Running 36-model ensemble + YIN pitch detection + on-chain notarization...</div></div> |
| <div class="error" id="error"></div> |
|
|
| <div id="results" style="display:none"> |
| <div class="result-grid"> |
| <div class="r-card"><h4>FartScore</h4><div class="metric" id="fartScore">0</div><div class="label">Risk-Adjusted Acoustic Rating</div><div class="gauge"><div class="gauge-fill" id="scoreGauge" style="width:0%"></div></div></div> |
| <div class="r-card"><h4>Fingerprint</h4><div class="value" id="fingerprint">--</div><div class="label">Solana-Compatible Base58 Hash</div></div> |
| <div class="r-card"><h4>Duration</h4><div class="metric" id="duration">0.00s</div><div class="label">Acoustic Event Length</div></div> |
| <div class="r-card"><h4>Notes</h4><div class="metric" id="noteCount">0</div><div class="label">MIDI Segments Extracted</div></div> |
| </div> |
|
|
| <div class="canvas-wrap"><h4>Waveform</h4><canvas id="waveCanvas" width="900" height="150"></canvas></div> |
| <div class="canvas-wrap"><h4>Piano Roll</h4><canvas id="pianoCanvas" width="900" height="150"></canvas></div> |
|
|
| <div class="midi-player"> |
| <button class="btn-primary" onclick="playMIDI()">▶ Play MIDI</button> |
| <button class="btn-secondary" onclick="downloadMIDI()">↓ Download .MID</button> |
| <span style="font-size:11px;color:var(--muted)" id="midiInfo"></span> |
| </div> |
|
|
| <div class="r-card" style="margin-top:16px"><h4>Receipt Chain</h4> |
| <div class="receipt"> |
| <div class="line"><span class="lbl">Previous</span><span class="hash" id="prevReceipt">genesis</span></div> |
| <div class="line"><span class="lbl">Current</span><span class="hash" id="currReceipt">--</span></div> |
| </div> |
| </div> |
|
|
| <div class="r-card" style="margin-top:16px"><h4>36-Model Ensemble Analysis</h4><div id="modelOutputs"></div></div> |
| <div class="r-card" style="margin-top:16px"><h4>LLM Creative Outputs</h4><div id="llmOutputs"></div></div> |
| </div> |
| </div> |
|
|
| |
| <div id="tab-registry" class="section"> |
| <div class="r-card"><h4>ML Models (30)</h4><div id="mlList" style="max-height:400px;overflow-y:auto"></div></div> |
| <div class="r-card" style="margin-top:16px"><h4>LLM Models (6)</h4><div id="llmList" style="max-height:300px;overflow-y:auto"></div></div> |
| </div> |
|
|
| |
| <div id="tab-history" class="section"> |
| <table class="tbl"><thead><tr><th>ID</th><th>Time</th><th>Score</th><th>Notes</th><th>Fingerprint</th><th>Receipt</th></tr></thead><tbody id="historyBody"></tbody></table> |
| </div> |
|
|
| |
| <div id="tab-leaderboard" class="section"> |
| <table class="tbl"><thead><tr><th>Rank</th><th>Fingerprint</th><th>FartScore</th><th>Notes</th><th>Time</th></tr></thead><tbody id="leaderboardBody"></tbody></table> |
| </div> |
| </main> |
| </div> |
|
|
| <div class="footer">AFIP v3.0.0 — Acoustic Flatulence Intelligence Platform · 36-Model Ensemble · On-Chain Receipts</div> |
|
|
| <script> |
| const $ = id => document.getElementById(id); |
| let audioCtx, mediaRecorder, recordedChunks = [], currentMidiUrl = null, currentNotes = []; |
| |
| function enterApp() { |
| $('landing').style.display = 'none'; |
| $('app').classList.add('visible'); |
| } |
| |
| function switchTab(name) { |
| document.querySelectorAll('.tab').forEach(t => t.classList.remove('active')); |
| document.querySelectorAll('.section').forEach(s => s.classList.remove('active')); |
| document.querySelector(`[data-tab="${name}"]`)?.classList.add('active'); |
| $(`tab-${name}`).classList.add('active'); |
| if(name === 'history') loadHistory(); |
| if(name === 'leaderboard') loadLeaderboard(); |
| if(name === 'registry') loadRegistry(); |
| } |
| |
| function encodeWav(samples, sr) { |
| const n = samples.length; |
| const buf = new ArrayBuffer(44 + n * 2); |
| const v = new DataView(buf); |
| const ws = (o, s) => { for(let i=0;i<s.length;i++) v.setUint8(o+i, s.charCodeAt(i)); }; |
| ws(0,'RIFF'); v.setUint32(4,36+n*2,true); ws(8,'WAVE'); |
| ws(12,'fmt '); v.setUint32(16,16,true); v.setUint16(20,1,true); v.setUint16(22,1,true); |
| v.setUint32(24,sr,true); v.setUint32(28,sr*2,true); v.setUint16(32,2,true); v.setUint16(34,16,true); |
| ws(36,'data'); v.setUint32(40,n*2,true); |
| 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); } |
| return new Blob([buf],{type:'audio/wav'}); |
| } |
| |
| function mixToMono(buf) { |
| const L=buf.getChannelData(0), R=buf.getChannelData(1), out=new Float32Array(L.length); |
| for(let i=0;i<L.length;i++) out[i]=(L[i]+R[i])/2; |
| return out; |
| } |
| |
| async function toggleRecord() { |
| const btn = $('btnRecord'); |
| if(btn.classList.contains('rec')) { |
| mediaRecorder.stop(); btn.classList.remove('rec'); btn.textContent = '● Record Audio'; return; |
| } |
| try { |
| const stream = await navigator.mediaDevices.getUserMedia({audio:true}); |
| audioCtx = new (window.AudioContext||window.webkitAudioContext)(); |
| recordedChunks = []; |
| mediaRecorder = new MediaRecorder(stream); |
| mediaRecorder.ondataavailable = e => { if(e.data.size) recordedChunks.push(e.data); }; |
| mediaRecorder.onstop = async () => { |
| const blob = new Blob(recordedChunks,{type:'audio/webm'}); |
| const ab = await blob.arrayBuffer(); |
| const buf = await audioCtx.decodeAudioData(ab); |
| const mono = buf.numberOfChannels>1 ? mixToMono(buf) : buf.getChannelData(0); |
| const wav = encodeWav(mono, buf.sampleRate); |
| handleFile(new File([wav],'recording.wav',{type:'audio/wav'})); |
| stream.getTracks().forEach(t=>t.stop()); |
| }; |
| mediaRecorder.start(); |
| btn.classList.add('rec'); btn.textContent = '■ Stop Recording'; |
| } catch(e) { showError('Microphone denied: '+e.message); } |
| } |
| |
| function handleDrop(e) { |
| e.preventDefault(); |
| $('dropzone').classList.remove('dragover'); |
| if(e.dataTransfer.files[0]) handleFile(e.dataTransfer.files[0]); |
| } |
| |
| $('dropzone').addEventListener('click',()=>$('fileIn').click()); |
| |
| function showError(msg) { |
| $('error').textContent = msg; |
| $('error').classList.add('active'); |
| setTimeout(()=>$('error').classList.remove('active'), 6000); |
| } |
| |
| async function handleFile(file) { |
| if(!file) return; |
| $('loading').classList.add('active'); |
| $('results').style.display = 'none'; |
| $('error').classList.remove('active'); |
| const form = new FormData(); form.append('file', file); |
| const t0 = performance.now(); |
| try { |
| const r = await fetch('/analyze',{method:'POST',body:form}); |
| $('latency').textContent = Math.round(performance.now()-t0)+' ms'; |
| if(!r.ok) throw new Error(await r.text()); |
| renderResults(await r.json()); |
| } catch(e) { showError(e.message); } |
| $('loading').classList.remove('active'); |
| } |
| |
| async function demoMock() { |
| enterApp(); |
| $('loading').classList.add('active'); |
| $('results').style.display = 'none'; |
| $('error').classList.remove('active'); |
| const t0 = performance.now(); |
| try { |
| const r = await fetch('/mock'); |
| $('latency').textContent = Math.round(performance.now()-t0)+' ms'; |
| if(!r.ok) throw new Error(await r.text()); |
| renderResults(await r.json()); |
| } catch(e) { showError(e.message); } |
| $('loading').classList.remove('active'); |
| } |
| |
| function renderResults(d) { |
| $('results').style.display = 'block'; |
| $('fartScore').textContent = d.fartscore; |
| $('scoreGauge').style.width = d.fartscore+'%'; |
| $('fingerprint').textContent = d.fingerprint; |
| $('duration').textContent = d.duration_sec.toFixed(2)+'s'; |
| $('noteCount').textContent = d.note_count; |
| $('currReceipt').textContent = d.receipt.substring(0,28)+'...'; |
| $('prevReceipt').textContent = d.prev_receipt ? d.prev_receipt.substring(0,28)+'...' : 'genesis'; |
| currentMidiUrl = d.midi_url; |
| currentNotes = d.notes || []; |
| $('midiInfo').textContent = currentMidiUrl ? currentMidiUrl.split('/').pop() : 'No MIDI'; |
| |
| drawWaveform(d.notes || []); |
| drawPianoRoll(d.notes || []); |
| |
| |
| const mdiv = $('modelOutputs'); mdiv.innerHTML = ''; |
| if(d.model_outputs) { |
| const byTask = {}; |
| d.model_outputs.forEach(o => { (byTask[o.task]=byTask[o.task]||[]).push(o); }); |
| Object.entries(byTask).forEach(([task,items]) => { |
| const hdr = document.createElement('div'); hdr.className='collapsible'; hdr.textContent=`${task} (${items.length})`; |
| const body = document.createElement('div'); body.className='collapsible-content'; |
| body.innerHTML = items.map(o=>`<div class="model-row"><span class="role">${o.role}</span><span style="color:var(--muted)">${JSON.stringify(o.result).substring(0,90)}</span></div>`).join(''); |
| hdr.addEventListener('click',()=>{ hdr.classList.toggle('open'); body.classList.toggle('open'); }); |
| mdiv.appendChild(hdr); mdiv.appendChild(body); |
| }); |
| } |
| |
| |
| const ldiv = $('llmOutputs'); ldiv.innerHTML = ''; |
| if(d.llm_outputs) { |
| d.llm_outputs.forEach(o => { |
| const hdr = document.createElement('div'); hdr.className='collapsible'; hdr.textContent=o.role; |
| const body = document.createElement('div'); body.className='collapsible-content'; |
| body.innerHTML = `<b style="color:var(--text)">Prompt:</b> ${o.prompt}<br><b style="color:var(--text)">Result:</b> ${o.result.generated_text||JSON.stringify(o.result)}`; |
| hdr.addEventListener('click',()=>{ hdr.classList.toggle('open'); body.classList.toggle('open'); }); |
| ldiv.appendChild(hdr); ldiv.appendChild(body); |
| }); |
| } |
| } |
| |
| function drawWaveform(notes) { |
| const c=$('waveCanvas'), ctx=c.getContext('2d'); |
| c.width=c.offsetWidth||900; c.height=150; |
| ctx.fillStyle='#05080f'; ctx.fillRect(0,0,c.width,c.height); |
| ctx.strokeStyle='rgba(88,166,255,.4)'; ctx.lineWidth=1; ctx.beginPath(); |
| if(!notes.length) { for(let x=0;x<c.width;x++){ctx.moveTo(x,75);ctx.lineTo(x,75+Math.sin(x*.1)*30);} ctx.stroke(); return; } |
| const maxT=Math.max(...notes.map(n=>n.start+(n.duration||n.dur)),1); |
| for(let x=0;x<c.width;x++){ |
| const t=(x/c.width)*maxT; |
| const active=notes.some(n=>n.start<=t && t<=n.start+(n.duration||n.dur)); |
| const y=active? 30+Math.random()*90 : 75+Math.sin(x*.05)*10; |
| if(x===0) ctx.moveTo(x,y); else ctx.lineTo(x,y); |
| } |
| ctx.stroke(); |
| } |
| |
| function drawPianoRoll(notes) { |
| const c=$('pianoCanvas'), ctx=c.getContext('2d'); |
| c.width=c.offsetWidth||900; c.height=150; |
| ctx.fillStyle='#05080f'; ctx.fillRect(0,0,c.width,c.height); |
| if(!notes.length) return; |
| const maxT=Math.max(...notes.map(n=>n.start+(n.duration||n.dur)),1); |
| const minN=Math.min(...notes.map(n=>n.midi||n.note),20); |
| const maxN=Math.max(...notes.map(n=>n.midi||n.note),100); |
| const nRange=maxN-minN+5; |
| notes.forEach(n=>{ |
| const x=(n.start/maxT)*c.width; |
| const w=((n.duration||n.dur)/maxT)*c.width; |
| const y=c.height-((n.midi||n.note-minN+2)/nRange)*c.height; |
| const h=c.height/nRange; |
| ctx.fillStyle='#58a6ff'; ctx.globalAlpha=.75; |
| ctx.fillRect(x,y,Math.max(w,2),h); |
| ctx.globalAlpha=1; |
| }); |
| } |
| |
| function playMIDI() { |
| if(!currentNotes.length) return; |
| const ctx=new AudioContext(); |
| currentNotes.forEach(n=>{ |
| const o=ctx.createOscillator(), g=ctx.createGain(); |
| o.connect(g); g.connect(ctx.destination); |
| o.type='sine'; |
| o.frequency.value=440*Math.pow(2,((n.midi||n.note)-69)/12); |
| g.gain.value=(n.velocity||n.vel||70)/127*.3; |
| o.start(ctx.currentTime+n.start); |
| g.gain.exponentialRampToValueAtTime(.001,ctx.currentTime+n.start+(n.duration||n.dur)); |
| o.stop(ctx.currentTime+n.start+(n.duration||n.dur)); |
| }); |
| } |
| |
| function downloadMIDI() { |
| if(currentMidiUrl) window.location=currentMidiUrl; |
| } |
| |
| async function loadRegistry() { |
| try { |
| const r=await fetch('/registry'); const d=await r.json(); |
| $('mlList').innerHTML=d.ml_models.map(m=>`<div class="model-row"><span class="role">${m.role}</span><span style="color:var(--muted)">${m.id}</span></div>`).join(''); |
| $('llmList').innerHTML=d.llm_models.map(m=>`<div class="model-row"><span class="role">${m.role}</span><span style="color:var(--muted)">${m.id}</span></div>`).join(''); |
| } catch(e){ console.error(e); } |
| } |
| |
| async function loadHistory() { |
| try { |
| const r=await fetch('/history?limit=20'); const d=await r.json(); |
| $('historyBody').innerHTML=d.map(row=>`<tr><td>${row.id}</td><td>${row.ts?.slice(0,19)||''}</td><td style="color:var(--accent2);font-weight:700">${row.fartscore}</td><td>${row.note_count}</td><td style="font-size:10px">${row.fingerprint?.slice(0,24)}...</td><td style="font-size:10px">${row.receipt_hash?.slice(0,16)}...</td></tr>`).join(''); |
| } catch(e){ console.error(e); } |
| } |
| |
| async function loadLeaderboard() { |
| try { |
| const r=await fetch('/leaderboard'); const d=await r.json(); |
| $('leaderboardBody').innerHTML=d.map((row,i)=>`<tr><td style="color:var(--accent);font-weight:700">#${i+1}</td><td style="font-size:10px">${row.fingerprint?.slice(0,28)}...</td><td style="color:var(--accent2);font-weight:700">${row.fartscore}</td><td>${row.note_count}</td><td>${row.ts?.slice(0,19)||''}</td></tr>`).join(''); |
| } catch(e){ console.error(e); } |
| } |
| |
| |
| if(location.hash==='#app') enterApp(); |
| </script> |
| </body> |
| </html> |
|
|