From 57b9f9d78e94054dd52f66e04b0149cc9f463b3d Mon Sep 17 00:00:00 2001 From: Kagamine Rin Date: Fri, 29 May 2026 21:27:12 -0400 Subject: [PATCH] fix: Correct key/BPM colors and cleaner badge text MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Key color fix: - Previous offset put D# major at green (~150°) instead of orange (~30°) - New offset: (position + 8) * 30° so position 5 (D#/Eb) = 390° mod 360 = 30° orange - Position 8 (C) = 480° mod 360 = 120° green. Adjacent positions = adjacent hues. BPM color fix: - Previous wrap-around math went blue→purple→red, missing cyan/green/yellow/orange - New: simple linear interpolation 240° → 0° (long way: 240→180→120→60→0) - 60 BPM = blue (240°), 120 BPM = green (120°), 180 BPM = red (0°) Badge text cleanup: - BPM: just the number (e.g., '140'), no 'BPM' text. Tooltip shows full value. - Key: shortened format — 'D#m' for minor, 'D#' for major. Unicode flats/sharps. Tooltip shows full 'D# major' / 'D# minor'. - DetailPanel: same short format for consistency. Auto-fetch analysis: - App.tsx fetchClips() now fetches analysis for any ready clips missing BPM/key - Runs in background after clips load, updates state silently - No more needing to click into detail panel to see colors --- frontend/src/App.tsx | 13 +++++++ frontend/src/components/Gallery.tsx | 20 +++++++--- frontend/src/utils/musicColors.ts | 57 +++++++++++------------------ 3 files changed, 48 insertions(+), 42 deletions(-) diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index ba8cdee..6dfb87f 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -32,6 +32,19 @@ function App() { const res = await fetch('/api/clips') const data = await res.json() setClips(data.clips) + // Fetch analysis for clips that don't have it + const missing = data.clips.filter((c: Clip) => c.status === 'ready' && (!c.bpm || !c.key)) + missing.forEach(async (clip: Clip) => { + try { + const res = await fetch(`/api/clips/${clip.id}/analysis`) + if (res.ok) { + const analysis = await res.json() + setClips(prev => prev.map(c => c.id === clip.id ? { ...c, bpm: analysis.bpm, key: analysis.key } : c)) + } + } catch (err) { + // Silently fail — analysis is optional + } + }) } catch (err) { console.error('Failed to fetch clips:', err) } diff --git a/frontend/src/components/Gallery.tsx b/frontend/src/components/Gallery.tsx index 02dae7a..3a6f53a 100644 --- a/frontend/src/components/Gallery.tsx +++ b/frontend/src/components/Gallery.tsx @@ -251,21 +251,25 @@ export default function Gallery({ clips, onPlay, onDelete, onRename, onSelect, o const c = getBPMColor(clip.bpm) return c ? ( - {Math.round(clip.bpm)} BPM + {Math.round(clip.bpm)} ) : null })()} {clip.key && (() => { const c = getKeyColor(clip.key) + const shortKey = clip.key?.replace(/\s+(major|minor)$/i, '').replace(/b$/g, '♭').replace(/#/g, '♯') + const isMinor = clip.key?.toLowerCase().includes('minor') return c ? ( - {clip.key} + {shortKey}{isMinor ? 'm' : ''} ) : null })()} @@ -403,21 +407,25 @@ export default function Gallery({ clips, onPlay, onDelete, onRename, onSelect, o const c = getBPMColor(clip.bpm) return c ? ( - {Math.round(clip.bpm)} BPM + {Math.round(clip.bpm)} ) : null })()} {clip.key && (() => { const c = getKeyColor(clip.key) + const shortKey = clip.key?.replace(/\s+(major|minor)$/i, '').replace(/b$/g, '♭').replace(/#/g, '♯') + const isMinor = clip.key?.toLowerCase().includes('minor') return c ? ( - {clip.key} + {shortKey}{isMinor ? 'm' : ''} ) : null })()} diff --git a/frontend/src/utils/musicColors.ts b/frontend/src/utils/musicColors.ts index 221d6aa..1e12bf7 100644 --- a/frontend/src/utils/musicColors.ts +++ b/frontend/src/utils/musicColors.ts @@ -2,16 +2,16 @@ * Music color utilities — Camelot wheel key colors + BPM gradient. * * Key colors: Each key maps to a hue based on its Camelot wheel position. - * 12 positions × 30° = full color wheel. Major = bright, minor = slightly darker. + * Offset chosen so D#/Eb major (position 5) lands at orange (~30°). + * C major (position 8) lands at green (~120°). Adjacent positions = adjacent hues. * - * BPM gradient: Two root tempos define the hue range. - * Default: 60 BPM (blue, calm) → 180 BPM (red, intense). - * Anything outside the range clamps to the nearest edge. + * BPM gradient: Slow = blue (240°), fast = red (0°). + * Passes through cyan → green → yellow → orange in between. + * Clamps outside the range. */ // ─── Camelot Wheel Mapping ─────────────────────────────────────── -// Standard Camelot wheel: position → key name (as returned by librosa) const CAMELOT_MAJOR: Record = { 1: ['B major'], 2: ['F# major', 'Gb major'], @@ -42,7 +42,6 @@ const CAMELOT_MINOR: Record = { 12: ['C# minor', 'Db minor'], } -// Build reverse lookup: key name (lowercase) → { position, isMajor } const keyToCamelot = new Map() for (const [pos, keys] of Object.entries(CAMELOT_MAJOR)) { @@ -58,17 +57,16 @@ for (const [pos, keys] of Object.entries(CAMELOT_MINOR)) { // ─── Key Color ─────────────────────────────────────────────────── -export function getKeyColor(key: string | null | undefined): { bg: string; text: string; border: string } | null { +export function getKeyColor(key: string | null | undefined): { bg: string; text: string; border: string; hue: number } | null { if (!key) return null const normalized = key.trim().toLowerCase() const camelot = keyToCamelot.get(normalized) if (!camelot) return null - // Hue: position 1 = 0° (red), each step +30° - const hue = ((camelot.position - 1) * 30) % 360 + // Offset so position 5 (D#/Eb) = ~30° orange, position 8 (C) = ~120° green + const hue = ((camelot.position + 8) * 30) % 360 - // Major: brighter, more saturated. Minor: slightly darker, less saturated. const saturation = camelot.isMajor ? 85 : 70 const lightness = camelot.isMajor ? 55 : 50 @@ -76,53 +74,40 @@ export function getKeyColor(key: string | null | undefined): { bg: string; text: bg: `hsla(${hue}, ${saturation}%, ${lightness}%, 0.15)`, text: `hsl(${hue}, ${saturation}%, ${lightness}%)`, border: `hsla(${hue}, ${saturation}%, ${lightness}%, 0.3)`, + hue, } } // ─── BPM Color ───────────────────────────────────────────────────── export interface BPMRange { - min: number // slow edge BPM - max: number // fast edge BPM - minHue: number // hue for slow (default: 240 = blue) - maxHue: number // hue for fast (default: 0 = red) + min: number + max: number } const DEFAULT_BPM_RANGE: BPMRange = { min: 60, max: 180, - minHue: 240, // blue — calm, slow - maxHue: 0, // red — intense, fast } export function getBPMColor( bpm: number | null | undefined, range: Partial = {} -): { bg: string; text: string; border: string } | null { +): { bg: string; text: string; border: string; hue: number } | null { if (bpm == null) return null - const { min, max, minHue, maxHue } = { ...DEFAULT_BPM_RANGE, ...range } - - // Clamp BPM to range + const { min, max } = { ...DEFAULT_BPM_RANGE, ...range } const clamped = Math.max(min, Math.min(max, bpm)) + const t = (clamped - min) / (max - min) - // Interpolate hue (handle wrap-around if minHue > maxHue) - let t = (clamped - min) / (max - min) - let hue: number - if (minHue > maxHue) { - // Going across the 0° boundary (e.g., 240 → 0) - const span = (360 - minHue) + maxHue - hue = (minHue + t * span) % 360 - } else { - hue = minHue + t * (maxHue - minHue) - } - - const saturation = 85 - const lightness = 55 + // Slow (60) = 240° blue → Fast (180) = 0° red + // Long way around: 240° → 180°(cyan) → 120°(green) → 60°(yellow) → 0°(red) + const hue = 240 - t * 240 return { - bg: `hsla(${hue}, ${saturation}%, ${lightness}%, 0.15)`, - text: `hsl(${hue}, ${saturation}%, ${lightness}%)`, - border: `hsla(${hue}, ${saturation}%, ${lightness}%, 0.3)`, + bg: `hsla(${hue}, 85%, 55%, 0.15)`, + text: `hsl(${hue}, 85%, 55%)`, + border: `hsla(${hue}, 85%, 55%, 0.3)`, + hue, } }