Spaces:
Running
Running
Download src/console.js from fernandofernandes/fly-recital: direct link, hf CLI and curl.
- Browser
- Download file 8.41 kB
-
https://huggingface.co/spaces/fernandofernandes/fly-recital/resolve/main/src/console.js
- Command line
-
hf download hf://spaces/fernandofernandes/fly-recital/src/console.js
-
curl -L -o console.js https://huggingface.co/spaces/fernandofernandes/fly-recital/resolve/main/src/console.js
8.41 kB
| // The instrument console: everything drawn here is a measurement from the running | |
| // model. Nothing is decorative noise and nothing is pre-recorded. | |
| const dpr = () => Math.min(window.devicePixelRatio || 1, 2); | |
| function fit(canvas) { | |
| const ratio = dpr(); | |
| const width = canvas.clientWidth; | |
| const height = canvas.clientHeight; | |
| if (canvas.width !== width * ratio || canvas.height !== height * ratio) { | |
| canvas.width = Math.max(1, Math.round(width * ratio)); | |
| canvas.height = Math.max(1, Math.round(height * ratio)); | |
| } | |
| const context = canvas.getContext('2d'); | |
| context.setTransform(ratio, 0, 0, ratio, 0, 0); | |
| return { context, width, height }; | |
| } | |
| /** Measured soma positions, lit by |h|. Rates, not spikes. */ | |
| export class NeuronCloud { | |
| constructor(canvas) { | |
| this.canvas = canvas; | |
| this.points = null; | |
| this.values = null; | |
| } | |
| setPositions(flat) { | |
| // Project the two widest anatomical axes so the familiar brain outline shows. | |
| const count = flat.length / 3; | |
| let minX = Infinity; let maxX = -Infinity; | |
| let minY = Infinity; let maxY = -Infinity; | |
| for (let i = 0; i < count; i += 1) { | |
| const x = flat[i * 3]; | |
| const y = flat[i * 3 + 1]; | |
| if (x < minX) minX = x; | |
| if (x > maxX) maxX = x; | |
| if (y < minY) minY = y; | |
| if (y > maxY) maxY = y; | |
| } | |
| const spanX = Math.max(1, maxX - minX); | |
| const spanY = Math.max(1, maxY - minY); | |
| this.aspect = spanX / spanY; | |
| this.points = new Float32Array(count * 2); | |
| for (let i = 0; i < count; i += 1) { | |
| this.points[i * 2] = (flat[i * 3] - minX) / spanX; | |
| // MaleCNS y grows downward relative to the usual viewing orientation. | |
| this.points[i * 2 + 1] = 1 - (flat[i * 3 + 1] - minY) / spanY; | |
| } | |
| this.values = new Float32Array(count); | |
| } | |
| update(values) { this.values = values; } | |
| draw() { | |
| const { context, width, height } = fit(this.canvas); | |
| context.clearRect(0, 0, width, height); | |
| if (!this.points) return; | |
| const count = this.points.length / 2; | |
| const scale = Math.min(width / this.aspect, height) * 0.94; | |
| const boxWidth = scale * this.aspect; | |
| const offsetX = (width - boxWidth) / 2; | |
| const offsetY = (height - scale) / 2; | |
| let peak = 1e-6; | |
| for (let i = 0; i < count; i += 1) if (this.values[i] > peak) peak = this.values[i]; | |
| for (let i = 0; i < count; i += 1) { | |
| const x = offsetX + this.points[i * 2] * boxWidth; | |
| const y = offsetY + this.points[i * 2 + 1] * scale; | |
| // A fixed square-root display curve, the same convention the recorded demo used. | |
| const level = Math.sqrt(Math.min(1, this.values[i] / peak)); | |
| if (level < 0.06) { | |
| context.fillStyle = 'rgba(70,104,116,0.34)'; | |
| context.fillRect(x, y, 1.1, 1.1); | |
| continue; | |
| } | |
| const alpha = 0.22 + level * 0.78; | |
| context.fillStyle = level > 0.72 | |
| ? `rgba(233,250,255,${alpha})` | |
| : `rgba(${Math.round(90 + level * 150)},${Math.round(200 + level * 45)},${Math.round(215 + level * 35)},${alpha})`; | |
| const size = 1.1 + level * 2.2; | |
| context.fillRect(x - size / 2, y - size / 2, size, size); | |
| } | |
| } | |
| } | |
| /** Sampled neuron magnitudes over recent tokens, newest column on the right. */ | |
| export class StateRaster { | |
| constructor(canvas, columns = 96) { | |
| this.canvas = canvas; | |
| this.columns = columns; | |
| this.rows = 0; | |
| this.buffer = []; | |
| } | |
| setRows(rows) { | |
| this.rows = rows; | |
| this.buffer = []; | |
| } | |
| push(values) { | |
| this.buffer.push(values); | |
| if (this.buffer.length > this.columns) this.buffer.shift(); | |
| } | |
| clear() { this.buffer = []; } | |
| draw() { | |
| const { context, width, height } = fit(this.canvas); | |
| context.clearRect(0, 0, width, height); | |
| if (!this.rows || !this.buffer.length) return; | |
| const columnWidth = width / this.columns; | |
| const rowHeight = height / this.rows; | |
| let peak = 1e-6; | |
| for (const column of this.buffer) { | |
| for (let r = 0; r < column.length; r += 1) if (column[r] > peak) peak = column[r]; | |
| } | |
| const start = this.columns - this.buffer.length; | |
| for (let c = 0; c < this.buffer.length; c += 1) { | |
| const column = this.buffer[c]; | |
| const x = (start + c) * columnWidth; | |
| for (let r = 0; r < column.length; r += 1) { | |
| const level = Math.sqrt(Math.min(1, column[r] / peak)); | |
| if (level < 0.05) continue; | |
| context.fillStyle = `rgba(232,183,106,${0.1 + level * 0.9})`; | |
| context.fillRect(x, r * rowHeight, Math.max(1, columnWidth - 0.35), Math.max(0.7, rowHeight - 0.25)); | |
| } | |
| } | |
| } | |
| } | |
| /** Per-token top-1 probability, drawn like a deck's loaded waveform. */ | |
| export class ProbabilityWave { | |
| constructor(canvas, capacity = 120) { | |
| this.canvas = canvas; | |
| this.capacity = capacity; | |
| this.values = []; | |
| } | |
| push(value) { | |
| this.values.push(value); | |
| if (this.values.length > this.capacity) this.values.shift(); | |
| } | |
| clear() { this.values = []; } | |
| draw() { | |
| const { context, width, height } = fit(this.canvas); | |
| context.clearRect(0, 0, width, height); | |
| const middle = height / 2; | |
| context.fillStyle = 'rgba(255,255,255,0.05)'; | |
| context.fillRect(0, middle - 0.5, width, 1); | |
| if (!this.values.length) return; | |
| const barWidth = width / this.capacity; | |
| const start = this.capacity - this.values.length; | |
| for (let i = 0; i < this.values.length; i += 1) { | |
| const value = this.values[i]; | |
| const half = Math.max(1, value * (height / 2 - 2)); | |
| const x = (start + i) * barWidth; | |
| // Confident tokens read violet; uncertain ones flare magenta. | |
| context.fillStyle = value > 0.66 | |
| ? `rgba(143,123,240,${0.55 + value * 0.45})` | |
| : `rgba(226,88,156,${0.5 + value * 0.5})`; | |
| context.fillRect(x, middle - half, Math.max(1, barWidth - 0.6), half * 2); | |
| } | |
| } | |
| } | |
| /** A draggable EQ-style knob. These control the sampler for real. */ | |
| export class Knob { | |
| constructor(element, { min, max, value, format, onChange }) { | |
| this.element = element; | |
| this.canvas = element.querySelector('canvas'); | |
| this.readout = element.querySelector('b'); | |
| this.min = min; | |
| this.max = max; | |
| this.value = value; | |
| this.format = format; | |
| this.onChange = onChange; | |
| this.accent = element.dataset.knob === 'pace' ? '#4fd1b5' : '#e8b76a'; | |
| this.#bind(); | |
| this.draw(); | |
| } | |
| #bind() { | |
| let startY = 0; | |
| let startValue = 0; | |
| let active = false; | |
| const down = (event) => { | |
| active = true; | |
| startY = (event.touches ? event.touches[0] : event).clientY; | |
| startValue = this.value; | |
| this.element.setPointerCapture?.(event.pointerId); | |
| event.preventDefault(); | |
| }; | |
| const move = (event) => { | |
| if (!active) return; | |
| const y = (event.touches ? event.touches[0] : event).clientY; | |
| const span = this.max - this.min; | |
| this.set(startValue + ((startY - y) / 120) * span); | |
| event.preventDefault(); | |
| }; | |
| const up = () => { active = false; }; | |
| this.element.addEventListener('pointerdown', down); | |
| window.addEventListener('pointermove', move, { passive: false }); | |
| window.addEventListener('pointerup', up); | |
| this.element.addEventListener('dblclick', () => this.set(this.defaultValue ?? this.value)); | |
| } | |
| set(value) { | |
| this.value = Math.min(this.max, Math.max(this.min, value)); | |
| this.draw(); | |
| this.onChange?.(this.value); | |
| } | |
| draw() { | |
| const { context, width, height } = fit(this.canvas); | |
| context.clearRect(0, 0, width, height); | |
| const cx = width / 2; | |
| const cy = height / 2 + 2; | |
| const radius = Math.min(width, height) / 2 - 4; | |
| const from = Math.PI * 0.75; | |
| const to = Math.PI * 2.25; | |
| const fraction = (this.value - this.min) / (this.max - this.min); | |
| context.lineWidth = 2.5; | |
| context.lineCap = 'round'; | |
| context.strokeStyle = 'rgba(255,255,255,0.09)'; | |
| context.beginPath(); | |
| context.arc(cx, cy, radius, from, to); | |
| context.stroke(); | |
| context.strokeStyle = this.accent; | |
| context.beginPath(); | |
| context.arc(cx, cy, radius, from, from + (to - from) * fraction); | |
| context.stroke(); | |
| const angle = from + (to - from) * fraction; | |
| context.beginPath(); | |
| context.moveTo(cx + Math.cos(angle) * (radius - 7), cy + Math.sin(angle) * (radius - 7)); | |
| context.lineTo(cx + Math.cos(angle) * (radius - 1), cy + Math.sin(angle) * (radius - 1)); | |
| context.stroke(); | |
| if (this.readout) this.readout.textContent = this.format(this.value); | |
| } | |
| } | |