feat: spotify-only matching + optional local-LLM gate + google→bandcamp fallback (closes task/drop-musicbrainz-google-fallback, closes task/ollama-classifier)
Source: human-feedback note 01KVDYV3A35B57N15CRBQDXPCH (project/reddit-spotify-linker, 2026-06-18) — "remove the musicbrainz integration and just use spotify"; a google search for "<name> bandcamp" fallback; and "would a local llm be better able to categorise these ... include instructions for running something suitable via ollama (16GB M2)". - Remove MusicBrainz entirely (viaMusicBrainz + url-rels + rate-gate; lib relUrls/mbResult). Spotify is now the only music API; it both gates by exact-name match and supplies the direct artist link. - Optional ollama LLM gate (OLLAMA_URL enables it, OLLAMA_MODEL default qwen2.5:3b) called over plain HTTP -> stays zero npm-dep. It classifies which candidates are real artists/albums (the matching-quality lever). Pure prompt/parse logic in new llm.js (unit-tested in llm.test.js); the fetch + resolveAll gate wiring live in resolver.js. - A confirmed name links to its direct Spotify page when available, else a Google search for "<name> bandcamp". New "google" primary platform rendered by the extension (styles.css blue accent + platform-aware link title in content.js). - README: drop MusicBrainz; document the two gates + a runnable local-LLM setup for a 16GB MacBook M2. AGENTS.md: flip the old "no ML/NER" standing decision per the human override. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,55 @@
|
||||
// Unit tests for the optional LLM classifier's pure logic (run with `node --test`). Zero-dep.
|
||||
import { test } from "node:test";
|
||||
import assert from "node:assert/strict";
|
||||
import { buildClassifyMessages, parseClassification, DEFAULT_MODEL } from "./llm.js";
|
||||
|
||||
test("DEFAULT_MODEL is a small ollama model", () => {
|
||||
assert.equal(DEFAULT_MODEL, "qwen2.5:3b");
|
||||
});
|
||||
|
||||
test("buildClassifyMessages embeds the candidate list as JSON for the user turn", () => {
|
||||
const msgs = buildClassifyMessages(["Tool", "the best"]);
|
||||
assert.equal(msgs.length, 2);
|
||||
assert.equal(msgs[0].role, "system");
|
||||
assert.match(msgs[0].content, /artist/i);
|
||||
assert.equal(msgs[1].role, "user");
|
||||
assert.deepEqual(JSON.parse(msgs[1].content), { candidates: ["Tool", "the best"] });
|
||||
});
|
||||
|
||||
test("parseClassification keeps only artist/album candidates that were in the list", () => {
|
||||
const candidates = ["Cattle Decapitation", "Devourment", "the best part"];
|
||||
const reply = JSON.stringify({
|
||||
results: [
|
||||
{ name: "Cattle Decapitation", kind: "artist" },
|
||||
{ name: "Devourment", kind: "ARTIST" }, // case-insensitive kind
|
||||
{ name: "the best part", kind: "none" },
|
||||
],
|
||||
});
|
||||
const got = parseClassification(reply, candidates);
|
||||
assert.ok(got.has("cattle decapitation"));
|
||||
assert.ok(got.has("devourment"));
|
||||
assert.ok(!got.has("the best part"));
|
||||
});
|
||||
|
||||
test("parseClassification accepts album kind and a bare array reply", () => {
|
||||
const got = parseClassification(
|
||||
JSON.stringify([{ name: "Reek of Putrefaction", kind: "album" }]),
|
||||
["Reek of Putrefaction"],
|
||||
);
|
||||
assert.ok(got.has("reek of putrefaction"));
|
||||
});
|
||||
|
||||
test("parseClassification ignores hallucinated names not in the candidate list", () => {
|
||||
const got = parseClassification(
|
||||
JSON.stringify({ results: [{ name: "Some Band I Invented", kind: "artist" }] }),
|
||||
["Real Candidate"],
|
||||
);
|
||||
assert.equal(got.size, 0);
|
||||
});
|
||||
|
||||
test("parseClassification returns an empty set on unparseable / junk replies", () => {
|
||||
assert.equal(parseClassification("not json at all", ["X"]).size, 0);
|
||||
assert.equal(parseClassification("", ["X"]).size, 0);
|
||||
assert.equal(parseClassification(JSON.stringify({ nope: true }), ["X"]).size, 0);
|
||||
assert.equal(parseClassification(JSON.stringify({ results: [null, { kind: "artist" }] }), ["X"]).size, 0);
|
||||
});
|
||||
Reference in New Issue
Block a user