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,51 @@
|
||||
// Pure helpers for the OPTIONAL local-LLM (ollama) classifier. The HTTP call lives in
|
||||
// resolver.js; the prompt builder + reply parser live here so they unit-test with
|
||||
// `node --test` (no network). The LLM answers one question: "which of these candidate
|
||||
// substrings are music artists/albums?" — the precision gate when it is configured.
|
||||
|
||||
import { norm } from "./lib.js";
|
||||
|
||||
// Small, fast, runs comfortably on a 16GB M2 (3.1B params, Q4 ~2GB). Override with OLLAMA_MODEL.
|
||||
export const DEFAULT_MODEL = "qwen2.5:3b";
|
||||
|
||||
// Chat messages for an ollama /api/chat call (used with stream:false, format:"json").
|
||||
export function buildClassifyMessages(candidates) {
|
||||
const system =
|
||||
"You are a music-name classifier for a tool that links artist/album mentions in Reddit " +
|
||||
"music discussions. You are given a JSON array of candidate text snippets pulled from reddit " +
|
||||
"comments. For EACH candidate decide whether it names a music ARTIST/BAND, a music " +
|
||||
"ALBUM/release, or NEITHER. The snippets come from music threads, so prefer the music reading " +
|
||||
"when a candidate is a plausible band/album even if it is also an ordinary word. Mark ordinary " +
|
||||
"words, sentence fragments, generic phrases, place names, and non-music proper nouns as \"none\". " +
|
||||
'Reply with ONLY a JSON object of the form {"results":[{"name":<candidate verbatim>,' +
|
||||
'"kind":"artist"|"album"|"none"}]}. Use each candidate string VERBATIM as its name.';
|
||||
const user = JSON.stringify({ candidates });
|
||||
return [
|
||||
{ role: "system", content: system },
|
||||
{ role: "user", content: user },
|
||||
];
|
||||
}
|
||||
|
||||
// Parse the model's JSON reply into a Set of norm(name) the model marked artist/album.
|
||||
// Defensive: only names that were actually in the candidate list count (guards against the
|
||||
// model inventing names); an unparseable reply yields an empty Set, and the caller then
|
||||
// falls back to the Spotify-exact-match gate.
|
||||
export function parseClassification(text, candidates) {
|
||||
const allowed = new Set(candidates.map(norm));
|
||||
const confirmed = new Set();
|
||||
let obj;
|
||||
try {
|
||||
obj = JSON.parse(text);
|
||||
} catch {
|
||||
return confirmed;
|
||||
}
|
||||
const rows = Array.isArray(obj?.results) ? obj.results : Array.isArray(obj) ? obj : [];
|
||||
for (const row of rows) {
|
||||
if (!row || typeof row.name !== "string") continue;
|
||||
const k = norm(row.name);
|
||||
if (!allowed.has(k)) continue;
|
||||
const kind = String(row.kind ?? "").toLowerCase();
|
||||
if (kind === "artist" || kind === "album") confirmed.add(k);
|
||||
}
|
||||
return confirmed;
|
||||
}
|
||||
Reference in New Issue
Block a user