brain: typed negatives count against the track, warm words never include what the station was told to avoid, a young station gets only a faint region prior from the other stations (no words), and the listener's avoid list wins
shots / shots (push) Canceled after 42s

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01K6897PWcUEwHiqZMcVnNF6
This commit is contained in:
teahubxyzandClaude Fable 5.1 committed 2026-10-21 06:20:23 +00:00
1 parent d4b4f48b03
commit a4dab59f21
1 file changed
+10 -5
+10 -5
View File
@@ -7,12 +7,14 @@
============================================================ */
/* ---------- signals: every reaction and every play, as one signed weight per track ---------- */
const NEG = /\b(no|not|less|stop|without|drop|enough|never|skip|hate|too much|lose|kill|cut)\b/i;
function reactionWeight(spec, row) {
const r = String(spec.reaction || "");
let w = 0;
for (const ch of r.replace(/"[^"]*"/g, "")) { if ("🔥❤️⚡🎸🎹🌧️".includes(ch) && ch !== "️") w += 1; if ("🚫😴".includes(ch)) w -= 1; }
if (/\[skipped\]/.test(r)) w -= 1.5;
if (/"[^"]*"/.test(r)) w += 0.3; // words mean engagement, mildly warm
// typed words: engagement, mildly warm, unless they say no ("no violin", "less strings", "stop the classical")
for (const m of r.matchAll(/"([^"]*)"/g)) w += NEG.test(m[1]) ? -0.6 : 0.3;
const plays = (row && row.plays) || spec.plays || 0;
if (plays > 1) w += Math.log2(plays) * 0.6; // on repeat: 4 plays ≈ a 🔥, 100 ≈ four of them
if (row && row.kept) w += 1.5;
@@ -119,10 +121,13 @@ function tasteText() {
const p0 = net.predict({ ...base, tags: "" });
const scored = [...vocab.keys()].filter((w) => vocab.get(w) >= 2).map((w) => [w, net.predict({ ...base, tags: w }) - p0]).sort((a, b) => b[1] - a[1]);
const likes = scored.slice(0, 6).filter((s) => s[1] > 0.02).map((s) => s[0]).join(", "), dislikes = scored.slice(-4).filter((s) => s[1] < -0.02).map((s) => s[0]).join(", ");
const which = brains.station ? "this station" : "all their stations";
return `Taste model (a small net trained on the listener's reactions and play counts across ${which}, ${net.n} signals): most likely to land: ${top}. Least likely: ${low}.` +
(likes ? ` Words it warms to: ${likes}.` : "") + (dislikes ? ` Words it cools on: ${dislikes}.` : "") +
` Aim near the likely region and lean on the warm words unless the listener's own words say otherwise; keep some variety.`;
// never recommend what this station was told to avoid; a young station borrows only the region from the others, not their words
const avoidWords = new Set([...(state.taste.avoid || []), ...(state.taste.notes || []).filter((n) => NEG.test(n))].flatMap((s) => tokens(s)));
const likesList = scored.slice(0, 8).filter((s) => s[1] > 0.02 && !avoidWords.has(s[0])).slice(0, 6).map((s) => s[0]).join(", ");
if (!brains.station) return `Taste prior (from the listener's other stations, ${net.n} signals; a faint hint only — this station's seed and the listener's own words come first): they tend to land around ${top}; less so ${low}.`;
return `Taste model (a small net trained on the listener's reactions and play counts on this station, ${net.n} signals): most likely to land: ${top}. Least likely: ${low}.` +
(likesList ? ` Words it warms to: ${likesList}.` : "") + (dislikes ? ` Words it cools on: ${dislikes}.` : "") +
` Aim near the likely region; keep some variety. The listener's explicit asks above and below outrank this model.`;
}
/* the page's hooks (index.html › hooks): the taste text rides into the DJ's prompt, and a station opening retrains */
hooks.djPrompt = (base) => {