mirror of
https://github.com/Chevron7Locked/kima-hub.git
synced 2026-06-19 07:37:17 +00:00
6dadd18c62
- UnifiedPanel replaces separate ActivityPanel/VibePanel as global 3rd column on all pages - 2D vibe map: chart-style track labels with leader lines, toggle with localStorage persistence - 2D vibe map: double-click to play track - 3D galaxy: halved particles, randomized orbiters (0-2), disabled post-processing, 30fps pulse throttle - Lyrics: uniform line-by-line rendering for both synced and plain lyrics - Security: added X-Frame-Options, X-Content-Type-Options, Referrer-Policy headers - Security: login error allowlist prevents reflected message injection - Security: removed dead token-from-URL code in auth context - Security: removed dangerouslyAllowSVG, dead login page fetch, hardcoded test credentials - Border radius consistency across all 3 main layout columns - Removed 4 vibe test pages, 3 unused scene files, dead VibeOverlayEnhanced and VibeInfoPanel - Added E2E full UX audit test suite (42 tests, all passing)
65 lines
2.1 KiB
TypeScript
65 lines
2.1 KiB
TypeScript
import type { AudioFeatures } from "@/lib/audio-state-context";
|
|
|
|
const MATCH_FEATURES = [
|
|
{ key: "energy", min: 0, max: 1 },
|
|
{ key: "valence", min: 0, max: 1 },
|
|
{ key: "arousal", min: 0, max: 1 },
|
|
{ key: "danceability", min: 0, max: 1 },
|
|
{ key: "bpm", min: 60, max: 200 },
|
|
{ key: "moodHappy", min: 0, max: 1 },
|
|
{ key: "moodSad", min: 0, max: 1 },
|
|
{ key: "moodRelaxed", min: 0, max: 1 },
|
|
{ key: "moodAggressive", min: 0, max: 1 },
|
|
{ key: "moodParty", min: 0, max: 1 },
|
|
{ key: "moodAcoustic", min: 0, max: 1 },
|
|
{ key: "moodElectronic", min: 0, max: 1 },
|
|
] as const;
|
|
|
|
function normalize(value: number | null | undefined, min: number, max: number): number {
|
|
if (value == null) return 0;
|
|
return Math.max(0, Math.min(1, (value - min) / (max - min)));
|
|
}
|
|
|
|
export function computeVibeMatchScore(
|
|
source: AudioFeatures | null | undefined,
|
|
current: AudioFeatures | null | undefined,
|
|
): number | null {
|
|
if (!source || !current) return null;
|
|
|
|
const sourceVector: number[] = [];
|
|
const currentVector: number[] = [];
|
|
|
|
for (const feature of MATCH_FEATURES) {
|
|
const sVal = (source as Record<string, unknown>)[feature.key];
|
|
const cVal = (current as Record<string, unknown>)[feature.key];
|
|
const sNorm = normalize(
|
|
typeof sVal === "number" ? sVal : null,
|
|
feature.min,
|
|
feature.max,
|
|
);
|
|
const cNorm = normalize(
|
|
typeof cVal === "number" ? cVal : null,
|
|
feature.min,
|
|
feature.max,
|
|
);
|
|
const weight = feature.key.startsWith("mood") ? 1.3 : 1.0;
|
|
sourceVector.push(sNorm * weight);
|
|
currentVector.push(cNorm * weight);
|
|
}
|
|
|
|
let dotProduct = 0;
|
|
let magSource = 0;
|
|
let magCurrent = 0;
|
|
|
|
for (let i = 0; i < sourceVector.length; i++) {
|
|
dotProduct += sourceVector[i] * currentVector[i];
|
|
magSource += sourceVector[i] * sourceVector[i];
|
|
magCurrent += currentVector[i] * currentVector[i];
|
|
}
|
|
|
|
const magnitude = Math.sqrt(magSource) * Math.sqrt(magCurrent);
|
|
if (magnitude === 0) return null;
|
|
|
|
return Math.round((dotProduct / magnitude) * 100);
|
|
}
|