eigenben's picture
Add nearest-neighbor explorer app
bc9f351 verified
Raw
History Blame Contribute Delete
4.91 kB
"""Jazz Harmony Explorer — nearest-neighbor search over published tune embeddings.
Runs entirely from the released bundle (no chord data): cosine similarity
over 6,900 x 128 vectors, plus the precomputed 2-D UMAP layout for context.
"""
import csv
import gradio as gr
import matplotlib
matplotlib.use("Agg")
import matplotlib.pyplot as plt
import numpy as np
from huggingface_hub import hf_hub_download
DATASET = "eigenben/jazz-harmony-embeddings"
_bundle = np.load(hf_hub_download(DATASET, "embeddings.npz", repo_type="dataset"))
EMB = _bundle["embeddings"]
with open(hf_hub_download(DATASET, "metadata.csv", repo_type="dataset"), newline="") as _handle:
ROWS = list(csv.DictReader(_handle))
PROJ = np.load(hf_hub_download(DATASET, "projection_2d.npz", repo_type="dataset"))["projection"]
SURFACE = "#fcfcfb"
BASE = "#c3c2b7"
BLUE = "#2a78d6"
RED = "#e34948"
MUTED = "#898781"
def find_anchor(title: str) -> int | None:
needle = title.strip().lower()
if not needle:
return None
exact = next((i for i, r in enumerate(ROWS) if r["title"].strip().lower() == needle), None)
if exact is not None:
return exact
return next((i for i, r in enumerate(ROWS) if needle in r["title"].lower()), None)
def neighbors(anchor: int, k: int) -> list[tuple[int, float]]:
scores = EMB @ EMB[anchor]
out = []
seen = {ROWS[anchor]["title"].strip().lower()}
for i in np.argsort(-scores):
title = ROWS[i]["title"].strip().lower()
if i == anchor or title in seen:
continue
seen.add(title)
out.append((int(i), float(scores[i])))
if len(out) == k:
break
return out
def map_figure(anchor: int, hits: list[tuple[int, float]]):
fig, ax = plt.subplots(figsize=(7, 5.6), dpi=120)
fig.patch.set_facecolor(SURFACE)
ax.set_facecolor(SURFACE)
for spine in ax.spines.values():
spine.set_visible(False)
ax.set_xticks([])
ax.set_yticks([])
ax.scatter(PROJ[:, 0], PROJ[:, 1], s=3, c=BASE, alpha=0.5, linewidths=0)
hit_rows = [i for i, _ in hits]
ax.scatter(PROJ[hit_rows, 0], PROJ[hit_rows, 1], s=42, c=BLUE, linewidths=0, zorder=3)
ax.scatter([PROJ[anchor, 0]], [PROJ[anchor, 1]], s=110, c=RED, linewidths=0, zorder=4)
ax.set_title(
f'"{ROWS[anchor]["title"]}" (red) and its neighbors (blue)',
fontsize=11, loc="left", color="#0b0b0b",
)
ax.text(
0, -0.04,
"2-D UMAP of all 6,900 tunes. Axes are arbitrary; only closeness is meaningful.",
transform=ax.transAxes, color=MUTED, fontsize=8,
)
fig.tight_layout()
return fig
def search(title: str, k: int):
anchor = find_anchor(title)
if anchor is None:
return (
gr.Markdown(f"No tune matches **{title}** — try a shorter substring."),
None,
None,
)
hits = neighbors(anchor, int(k))
table = [
[rank, f"{score:.3f}", ROWS[i]["title"], ROWS[i]["composer"], ROWS[i]["source"]]
for rank, (i, score) in enumerate(hits, start=1)
]
heading = gr.Markdown(
f"### {ROWS[anchor]['title']}"
+ (f" — {ROWS[anchor]['composer']}" if ROWS[anchor]["composer"] else "")
+ f"\n`{ROWS[anchor]['id']}` — closest tunes by learned harmonic similarity:"
)
return heading, table, map_figure(anchor, hits)
with gr.Blocks(title="Jazz Harmony Explorer") as demo:
gr.Markdown(
"# Jazz Harmony Explorer\n"
"A small transformer, trained from scratch on ~8,000 chord charts, placed every "
"jazz standard in a 128-d vector space where related harmony sits close together. "
"Search a tune to see its nearest harmonic neighbors — contrafacts (same chords, "
"different melody) should surface without the model ever being told about them. "
"Try **Oleo**, **Giant Steps**, **Cherokee**, or **Ornithology**.\n\n"
"[Model](https://huggingface.co/eigenben/jazz-harmony-embeddings) · "
"[Embeddings](https://huggingface.co/datasets/eigenben/jazz-harmony-embeddings) · "
"[Code & write-up](https://github.com/eigenben/jazz-harmony-embeddings)"
)
with gr.Row():
title_box = gr.Textbox(value="Oleo", label="tune title", scale=3)
k_slider = gr.Slider(5, 30, value=10, step=1, label="neighbors", scale=1)
go = gr.Button("Search", variant="primary", scale=1)
heading = gr.Markdown()
with gr.Row():
table = gr.Dataframe(
headers=["rank", "similarity", "title", "composer", "source"],
interactive=False,
)
plot = gr.Plot(label="corpus map")
for trigger in (go.click, title_box.submit, k_slider.release):
trigger(search, inputs=[title_box, k_slider], outputs=[heading, table, plot])
demo.load(search, inputs=[title_box, k_slider], outputs=[heading, table, plot])
demo.launch()