semantic-audio-search/index.js (116 lines of code) (raw):

import { Scatterplot } from "deepscatter"; import { getCachedJSON } from "./utils"; // Start loading metadata and positions asynchronously as soon as possible. let metadata = {}; getCachedJSON( "https://huggingface.co/datasets/Xenova/MusicBenchEmbedded/resolve/main/metadata.json", ) .then((data) => { metadata = data; }) .catch((e) => console.error(e)); let positions = {}; getCachedJSON( "https://huggingface.co/datasets/Xenova/MusicBenchEmbedded/resolve/main/positions.json", ) .then((data) => { positions = data; }) .catch((e) => console.error(e)); const scatterplot = new Scatterplot("#deepscatter"); window.scatterplot = scatterplot; // For debugging // Initial call scatterplot .plotAPI({ source_url: "https://huggingface.co/datasets/Xenova/MusicBenchEmbedded/resolve/main/atlas", max_points: 52768, // a full cap. alpha: 35, // Target saturation for the full page. zoom_balance: 0.5, // Rate at which points increase size. https://observablehq.com/@bmschmidt/zoom-strategies-for-huge-scatterplots-with-three-js point_size: 3, // Default point size before application of size scaling background_color: "transparent", encoding: { // TODO Add colours x: { field: "x", transform: "literal", }, y: { field: "y", transform: "literal", }, jitter_radius: { method: "uniform", constant: 5, }, }, tooltip_opacity: 1, duration: 4000, // For slow initial transition }) .then((_) => scatterplot.plotAPI({ encoding: { jitter_radius: { method: "uniform", constant: 0, }, }, }), ); // Custom hover function scatterplot.tooltip_html = (datum) => { const item = metadata[datum.ix]; if (!item) return "Loading..."; const location = item.location; setTimeout(() => { // Slight hack to append the audio element after the text const tooltip = document.querySelector(".tooltip"); if (tooltip) { tooltip.innerHTML = ` ${item.main_caption} <br> <audio id="tooltip-audio" controls src="https://huggingface.co/datasets/Xenova/MusicBenchEmbedded/resolve/main/audio/${location}"></audio> `; } }, 0); return item.main_caption; }; // Make references to DOM elements const OVERLAY = document.getElementById("overlay"); const INPUT_ELEMENT = document.getElementById("query"); const SEARCH_BUTTON = document.getElementById("search"); // Set up worker const worker = new Worker(new URL("./worker.js", import.meta.url), { type: "module", }); worker.addEventListener("message", (e) => { switch (e.data.status) { case "initiate": OVERLAY.innerText = "Loading model and embeddings database..."; OVERLAY.style.display = "flex"; OVERLAY.style.pointerEvents = "all"; break; case "ready": OVERLAY.style.display = "none"; OVERLAY.style.pointerEvents = "none"; break; case "complete": // Output is an array of [score, index] pairs. Get top item const index = e.data.output[0][1]; const position = positions?.[index]; if (position) { // Just in case the position hasn't loaded yet (this should never happen) // Zoom to result scatterplot.plotAPI({ zoom: { bbox: { x: [position[0] - 0.5, position[0] + 0.5], y: [position[1] - 0.5, position[1] + 0.5], }, }, duration: 2000, }); } break; } }); const search = () => { worker.postMessage({ status: "search", query: INPUT_ELEMENT.value, }); }; // Set up event listeners INPUT_ELEMENT.addEventListener("keypress", (event) => { if (event.keyCode === 13) search(); }); SEARCH_BUTTON.addEventListener("click", search);