"""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()