diff --git a/backend/src/routes/v2.routes.ts b/backend/src/routes/v2.routes.ts index cef9bb2..101c5f6 100644 --- a/backend/src/routes/v2.routes.ts +++ b/backend/src/routes/v2.routes.ts @@ -13,10 +13,6 @@ interface ActivePlan { servedTrackIds?: string[]; /** Explicit feedback targets (skip, dislike, completion, promotion). */ excludedTrackIds?: string[]; - /** Main artists already heard in this Vibe; they get a mild rank penalty. */ - servedArtistIds?: string[]; - /** Artists explicitly skipped/disliked; they receive the stronger penalty. */ - downrankedArtistIds?: string[]; } const PLAN_TTL_SEC = 2 * 3600; @@ -43,13 +39,6 @@ function sessionExclusions(active: ActivePlan): string[] { ])]; } -function sessionArtistPenalties(active: ActivePlan): Map { - const penalties = new Map(); - for (const artistId of active.servedArtistIds ?? []) penalties.set(artistId, 0.2); - for (const artistId of active.downrankedArtistIds ?? []) penalties.set(artistId, 0.75); - return penalties; -} - export default async function v2Routes(fastify: FastifyInstance, options: { dbService: DbService; sessionDirector: SessionDirector }) { const { dbService, sessionDirector: director } = options; @@ -154,15 +143,6 @@ export default async function v2Routes(fastify: FastifyInstance, options: { dbSe const next = active.plan.shift()!; active.servedTrackIds = appendUniqueId(active.servedTrackIds, next.trackId); - const artistResult = await dbService.pgClient.query<{ artist_id: string }>( - `SELECT artist_id FROM track_artists_v2 - WHERE track_id = $1 AND role = 'main' - ORDER BY confidence DESC NULLS LAST LIMIT 1`, - [next.trackId] - ); - if (artistResult.rows[0]?.artist_id) { - active.servedArtistIds = appendUniqueId(active.servedArtistIds, artistResult.rows[0].artist_id); - } // Enrich with track details const track = await dbService.getTrackById(next.trackId); @@ -174,7 +154,7 @@ export default async function v2Routes(fastify: FastifyInstance, options: { dbSe active.plan, [next.trackId], active.seedTrackId ?? undefined, - { excludedTrackIds: sessionExclusions(active), artistPenalties: sessionArtistPenalties(active) } + { excludedTrackIds: sessionExclusions(active) } ); active.plan = refill; } @@ -239,17 +219,6 @@ export default async function v2Routes(fastify: FastifyInstance, options: { dbSe // Feedback may race /next or arrive after a client-side prefetch. In all // cases its track becomes ineligible for the rest of this session. active.excludedTrackIds = appendUniqueId(active.excludedTrackIds, trackId); - if (action === 'skipped' || action === 'disliked') { - const artistResult = await dbService.pgClient.query<{ artist_id: string }>( - `SELECT artist_id FROM track_artists_v2 - WHERE track_id = $1 AND role = 'main' - ORDER BY confidence DESC NULLS LAST LIMIT 1`, - [trackId] - ); - if (artistResult.rows[0]?.artist_id) { - active.downrankedArtistIds = appendUniqueId(active.downrankedArtistIds, artistResult.rows[0].artist_id); - } - } const sessionTrackIds = sessionExclusions(active); const refill = await director.replan( userId, @@ -257,7 +226,7 @@ export default async function v2Routes(fastify: FastifyInstance, options: { dbSe active.plan, [trackId], active.seedTrackId ?? undefined, - { excludedTrackIds: sessionTrackIds, artistPenalties: sessionArtistPenalties(active) } + { excludedTrackIds: sessionTrackIds } ); active.plan = refill; await setActivePlan(userId, active); diff --git a/backend/src/services/session-director.service.ts b/backend/src/services/session-director.service.ts index 40fec53..0f1c1cf 100644 --- a/backend/src/services/session-director.service.ts +++ b/backend/src/services/session-director.service.ts @@ -37,12 +37,6 @@ export interface RepetitionState { export interface PlanBuildOptions { /** Tracks already exposed during this Vibe session; they are ineligible. */ excludedTrackIds?: Iterable; - /** - * Session-local artist penalties. These deliberately affect ranking rather - * than eligibility: a Vibe may return to an artist after giving other - * artists a fair chance, but skips/dislikes push that artist much lower. - */ - artistPenalties?: Iterable; } const W_ENJOY = 1.0; @@ -50,7 +44,6 @@ const W_FATIGUE = 0.4; const W_DIVERSITY = 0.3; const W_ENTROPY = 0.2; const W_REPETITION = 0.5; -const W_SESSION_ARTIST_PENALTY = 0.5; const PLAN_SIZE = 20; /** @@ -732,8 +725,7 @@ export class SessionDirector { fatigue: FatigueState, budgets: DiversityBudget[], state: GeneratorContext['state'], - repetitionState: RepetitionState, - artistPenalties: ReadonlyMap = new Map() + repetitionState: RepetitionState ): Promise { if (candidates.length === 0) return []; @@ -789,11 +781,6 @@ export class SessionDirector { + W_DIVERSITY * diversityBonus + W_ENTROPY * entropyBonus; - // Session history should diversify the queue, not make an artist - // permanently ineligible. A skipped/disliked artist receives the larger - // penalty supplied by the route; a merely served artist gets a small one. - score -= W_SESSION_ARTIST_PENALTY * (artistPenalties.get(artistId) ?? 0); - if (wouldRepeat) { score *= 0.1; } @@ -912,7 +899,6 @@ export class SessionDirector { ]); if (seedTrackId) recentExclusionSet.add(seedTrackId); const recentExclusions = [...recentExclusionSet]; - const artistPenalties = new Map(options.artistPenalties ?? []); const toleranceMap: Record = {}; const discoveryBeliefs = allBeliefs.filter(b => b.profile === 'discovery'); for (const b of discoveryBeliefs) { @@ -956,7 +942,7 @@ export class SessionDirector { return []; } const ranked = await this.rankCandidates( - eligibleCandidates, fatigue, budgets, state, repetitionState, artistPenalties + eligibleCandidates, fatigue, budgets, state, repetitionState ); const loopDim = await this.detectAntiLoop(state, fatigue, budgets, recentPlays); @@ -1082,7 +1068,6 @@ export class SessionDirector { if (loopDim) { const rebuilt = await this.buildPlan(userId, sessionId, seedTrackId, { excludedTrackIds: new Set([...excludedTrackIds, ...remainingSlots.map(c => c.trackId)]), - artistPenalties: options.artistPenalties, }); return mergeUniquePlan(remainingSlots, rebuilt, excludedTrackIds, PLAN_SIZE); } @@ -1092,7 +1077,6 @@ export class SessionDirector { if (Math.abs(entropy - 0.55) > 0.2) { const rebuilt = await this.buildPlan(userId, sessionId, seedTrackId, { excludedTrackIds: new Set([...excludedTrackIds, ...remainingSlots.map(c => c.trackId)]), - artistPenalties: options.artistPenalties, }); return mergeUniquePlan(remainingSlots, rebuilt, excludedTrackIds, PLAN_SIZE); } @@ -1106,7 +1090,6 @@ export class SessionDirector { // queued. Keep the valid tail and append only fresh candidates. const rebuilt = await this.buildPlan(userId, sessionId, seedTrackId, { excludedTrackIds: new Set([...excludedTrackIds, ...remainingSlots.map(c => c.trackId)]), - artistPenalties: options.artistPenalties, }); return mergeUniquePlan(remainingSlots, rebuilt, excludedTrackIds, PLAN_SIZE); } diff --git a/backend/src/services/session-director.test.ts b/backend/src/services/session-director.test.ts index c6c417b..1874694 100644 --- a/backend/src/services/session-director.test.ts +++ b/backend/src/services/session-director.test.ts @@ -168,27 +168,6 @@ describe('SessionDirector', () => { expect(ranked[0].relevance).toBeGreaterThanOrEqual(ranked[ranked.length - 1].relevance); }); - it('downranks a skipped artist without making it ineligible', async () => { - const db = makeMockDb(); - (db.pgClient.query as any) - .mockResolvedValueOnce({ rows: [{ track_id: 't1', artist_id: 'a1' }, { track_id: 't2', artist_id: 'a2' }] }) - .mockResolvedValueOnce({ rows: [] }); - const director = new SessionDirector(db); - const candidates = [candidate('t1'), candidate('t2')]; - const fatigue = { artist: new Map(), genre: new Map(), track: new Map(), language: new Map(), vocal: 0 }; - const state = { energy: 0.5, noveltyHunger: 0.3, sessionAgeMin: 10, lastArtistIds: [], lastGenreIds: [], context: null }; - - const ranked = await director.rankCandidates( - candidates, - fatigue, - [], - state, - { recentTrackIds: new Set(), recentArtistIds: new Set() }, - new Map([['a1', 0.75]]) - ); - - expect(ranked.map(item => item.trackId)).toEqual(['t2', 't1']); - }); }); describe('buildState', () => {