Files
KeySelector/docs/KeySelector_SPEC.md
T

18 KiB
Raw Blame History

KeySelector — Spezifikation

Übergabe-Dokument für die Entwicklung. Stand: 2026-08-26 Verzeichnis: C:\GitHub\_Ideen\

1. Konzept (ein Satz)

Browser-basiertes DJ-Tool, das per Mikrofon-Audio den laufenden Song erkennt (AcoustID-Fingerprinting), automatisch dessen Tonart (als Camelot-Code) und BPM ermittelt, und basierend darauf harmonisch + rhythmisch passende Folge-Songs vorschlägt. Läuft dauerhaft kostenlos, ist für den rein privaten Gebrauch gedacht, und ist auch vom Smartphone aus nutzbar.

Vorgeschlagener Name: KeyCue / MixMaestro / Cue (zur Auswahl) Domain: keyselector.orfel.de, keysel.orfel.de, ks.orfel.de Hosting: Jannik-Cloud, im services/keyselector/-Pattern

2. Architektur

keyselector/
├── public/                    # Frontend (Vanilla JS, kein Build)
│   ├── index.html              # Standalone HTML (KeySelector_Mockup_Wheel.html als Vorlage)
│   ├── app.js                  # Frontend-Logik (Recognize-Polling, Hero-Toggles)
│   ├── wheel.js                # SVG-Wheel-Renderer (12 + 12 Polygone in 2 Ringen)
│   ├── cover.js                # Cover-Loader (iTunes Search API)
│   ├── search.js               # Suchfeld-Fallback (Inline-Expand bei nicht-erkanntem Song)
│   └── styles.css              # CSS mit Custom Properties für Dark/Light
├── server/
│   ├── index.js                # Node.js (Express oder Fastify)
│   ├── routes/
│   │   ├── recognize.js        # POST /api/recognize (Audio-Sample → AcoustID → Track)
│   │   ├── metadata.js         # GET /api/metadata?track=... (Key, BPM, Tags via GetSongBPM, Last.fm, Essentia.js)
│   │   ├── suggestions.js      # GET /api/suggestions?key=4B&bpm=118 (Camelot-Logik + AcousticBrainz-DB)
│   │   └── cover.js            # GET /api/cover?artist=...&title=... (iTunes + 24h Cache)
│   └── workers/
│       └── poll-quota.js       # ntfy-Notification bei niedrigen Quota-Levels
├── db/
│   ├── schema.sql              # AcousticBrainz-Import-Schema
│   ├── seed-acousticbrainz.sh  # Init-Seed beim Deploy
│   └── migrations/
├── shared/
│   ├── mixing-rules.js         # Komplette Mixed In Key Tabelle (siehe §5.1)
│   ├── color-matrix.js         # Camelot-Code → Hex-Farbe (siehe §5.3)
│   ├── camelot-angle.js        # Camelot-Code → Winkel (siehe §5.2)
│   └── camelot-to-musickey.js  # Camelot-Code → Standard-Musik-Key
├── docker-compose.yml          # PostgreSQL + Backend + Caddy
├── generate-env.sh             # API-Keys: AcoustID, GetSongBPM, Last.fm, ntfy
└── README.md

Stack-Entscheidungen:

  • Frontend: Vanilla JS/HTML, kein Build-Prozess. Wheel als reines SVG. Dauerhören im Browser via navigator.mediaDevices.getUserMedia + MediaRecorder + AudioContext. AcousticBrainz-Modelle für Genre/Mood als TensorFlow.js im Browser.
  • Backend: Node.js (passt zu Essentia.js und Jannik-Cloud). AcoustID via Chromaprint, GetSongBPM via REST, Last.fm via REST, ntfy selbst gehostet.
  • Datenbank: PostgreSQL (gemeinsame Instanz in Jannik-Cloud). AcousticBrainz-Daten-Import als Init-Seed (CC0, ca. 7,5 Mio. Songs). Wird durch eigene Erkennungen erweitert.
  • Caching: Redis für kurzen State (zuletzt erkannter Song, adaptiver Status). PostgreSQL für persistente Daten. Optional: 24h-Cache für Cover-URLs.

3. Referenz-Mockup

C:\GitHub\_Ideen\KeySelector_Mockup_Wheel.html (31 KB, standalone, UTF-8 ohne BOM)

Was der Mockup zeigt:

  • Topbar: Brand, 🎤 Listening-Status, Light/Dark-Toggle
  • Hero-Karte: 240×240 Cover (links) + Song-Info (Titel, Artist, 3 Stats: Key/BPM/Match, Tags) + persistent Wheel (140×140, unten rechts in Info)
  • Suggestions-Grid: 10 Tracks, sortiert nach Kompatibilitäts-Stufe
  • Demo-Daten: Alphaville – "Made in Japan" (4B / A-Dur, 118 BPM, 98% Match)
  • Cover via iTunes Search API (braucht Server wegen CORS, nicht file://)

Was funktioniert:

  • ✅ Dark/Light Toggle mit CSS-Custom-Properties
  • ✅ Wheel inline expand (Klick → 520×520, × zum Schließen)
  • ✅ Cover inline expand (gleiche Mechanik)
  • ✅ Cover-Loading via iTunes API mit Fallback
  • ✅ Musik-Keys unter Camelot-Codes im Wheel
  • ✅ Stats semantisch eingefärbt (Key grün, BPM amber, Match grün)

Was noch zu fixen ist (laut User 2026-08-26, "ist egal"):

  • ⚠ Wheel-Geometrie visuell noch nicht perfekt. Die Logik (codeToAngle + 24 Polygone) ist korrekt, das Rendering muss der Entwickler nachjustieren (Stroke-Widths, Anti-Aliasing auf den Polygon-Kanten, evtl. ViewBox-Anpassung).

Was noch fehlt für Production (aus dem Mockup, im Detail siehe §6):

  • Listening-Toggle (Pause)
  • Suchfeld-Fallback
  • Echte AcoustID-Anbindung
  • AcousticBrainz-Initial-Seed
  • Suggestions-Engine (Camelot-Logik mit DB)

4. Datenstrukturen (alle aus dem Mockup übernehmen)

4.1 keys — alle 24 Camelot-Keys mit Music-Key und Farbe (Mixed In Key Standard)

const keys = [
  // Major (B) außen, Minor (A) innen — direkt unter dem entsprechenden Major
  ['12B', 'E',   '#00E5FF'], ['12A', 'Dbm', '#00E5FF'],
  ['1B',  'B',   '#5BE8B5'], ['1A',  'Abm', '#5BE8B5'],
  ['2B',  'F#',  '#8AEB86'], ['2A',  'Ebm', '#8AEB86'],
  ['3B',  'Db',  '#B9F062'], ['3A',  'Bbm', '#B9F062'],
  ['4B',  'Ab',  '#E4DF6A'], ['4A',  'Fm',  '#E4DF6A'],
  ['5B',  'Eb',  '#FFA07A'], ['5A',  'Cm',  '#FFA07A'],
  ['6B',  'Bb',  '#FF7582'], ['6A',  'Gm',  '#FF7582'],
  ['7B',  'F',   '#E85D9E'], ['7A',  'Dm',  '#E85D9E'],
  ['8B',  'C',   '#C86DD7'], ['8A',  'Am',  '#C86DD7'],
  ['9B',  'G',   '#9F72EE'], ['9A',  'Em',  '#9F72EE'],
  ['10B', 'D',   '#8B94F8'], ['10A', 'Bm',  '#8B94F8'],
  ['11B', 'A',   '#68B6F9'], ['11A', 'F#m', '#68B6F9'],
];

4.2 codeToAngle — Camelot-Code → Winkel im Wheel

Major und Minor am GLEICHEN Winkel. 12 Uhr = 0°, clockwise (30° pro Stunde).

const codeToAngle = {
  '12B':   0, '12A':   0,
  '1B':   30, '1A':   30,
  '2B':   60, '2A':   60,
  '3B':   90, '3A':   90,
  '4B':  120, '4A':  120,
  '5B':  150, '5A':  150,
  '6B':  180, '6A':  180,
  '7B':  210, '7A':  210,
  '8B':  240, '8A':  240,
  '9B':  270, '9A':  270,
  '10B': 300, '10A': 300,
  '11B': 330, '11A': 330,
};

4.3 colorMatrix — separate Farb-Matrix (Mixed In Key Standard)

const colorMatrix = {
  '12B': '#00E5FF', '12A': '#00E5FF',
  '1B':  '#5BE8B5', '1A':  '#5BE8B5',
  '2B':  '#8AEB86', '2A':  '#8AEB86',
  '3B':  '#B9F062', '3A':  '#B9F062',
  '4B':  '#E4DF6A', '4A':  '#E4DF6A',
  '5B':  '#FFA07A', '5A':  '#FFA07A',
  '6B':  '#FF7582', '6A':  '#FF7582',
  '7B':  '#E85D9E', '7A':  '#E85D9E',
  '8B':  '#C86DD7', '8A':  '#C86DD7',
  '9B':  '#9F72EE', '9A':  '#9F72EE',
  '10B': '#8B94F8', '10A': '#8B94F8',
  '11B': '#68B6F9', '11A': '#68B6F9',
};

Globale Farbänderung: Edit dieser Matrix → alle Komponenten (Wheel, Suggestions, Stats) folgen automatisch.

4.4 mixingRules — komplette Mixed In Key Harmonielehre

Pro Camelot-Code 8 Kompatibilitäts-Stufen: [Perfect, Quinte -1, Quinte +1, Energy, Scale, Diagonal, Jaw's, Mood]

const mixingRules = {
  '1A':  ['1A',  '12A', '2A',  '3A',  '1B',  '12B', '8A',  '4B' ],
  '2A':  ['2A',  '1A',  '3A',  '4A',  '2B',  '1B',  '9A',  '5B' ],
  '3A':  ['3A',  '2A',  '4A',  '5A',  '3B',  '2B',  '10A', '6B' ],
  '4A':  ['4A',  '3A',  '5A',  '6A',  '4B',  '3B',  '11A', '7B' ],
  '5A':  ['5A',  '4A',  '6A',  '7A',  '5B',  '4B',  '12A', '8B' ],
  '6A':  ['6A',  '5A',  '7A',  '8A',  '6B',  '5B',  '1A',  '9B' ],
  '7A':  ['7A',  '6A',  '8A',  '9A',  '7B',  '6B',  '2A',  '10B'],
  '8A':  ['8A',  '7A',  '9A',  '10A', '8B',  '7B',  '3A',  '11B'],
  '9A':  ['9A',  '8A',  '10A', '11A', '9B',  '8B',  '4A',  '12B'],
  '10A': ['10A', '9A',  '11A', '12A', '10B', '9B',  '5A',  '1B' ],
  '11A': ['11A', '10A', '12A', '1A',  '11B', '10B', '6A',  '2B' ],
  '12A': ['12A', '11A', '1A',  '2A',  '12B', '11B', '7A',  '3B' ],
  '1B':  ['1B',  '12B', '2B',  '3B',  '1A',  '2A',  '8B',  '10A'],
  '2B':  ['2B',  '1B',  '3B',  '4B',  '2A',  '3A',  '9B',  '11A'],
  '3B':  ['3B',  '2B',  '4B',  '5B',  '3A',  '4A',  '10B', '12A'],
  '4B':  ['4B',  '3B',  '5B',  '6B',  '4A',  '5A',  '11B', '1A' ],
  '5B':  ['5B',  '4B',  '6B',  '7B',  '5A',  '6A',  '12B', '2A' ],
  '6B':  ['6B',  '5B',  '7B',  '8B',  '6A',  '7A',  '1B',  '3A' ],
  '7B':  ['7B',  '6B',  '8B',  '9B',  '7A',  '8A',  '2B',  '4A' ],
  '8B':  ['8B',  '7B',  '9B',  '10B', '8A',  '9A',  '3B',  '5A' ],
  '9B':  ['9B',  '8B',  '10B', '11B', '9A',  '10A', '4B',  '6A' ],
  '10B': ['10B', '9B',  '11B', '12B', '10A', '11A', '5B',  '7A' ],
  '11B': ['11B', '10B', '12B', '1B',  '11A', '12A', '6B',  '8A' ],
  '12B': ['12B', '11B', '1B',  '2B',  '12A', '1A',  '7B',  '9A' ],
};

const compatLabels = ['Perfect', 'Quinte -1', 'Quinte +1', 'Energy', 'Scale', 'Diagonal', "Jaw's", 'Mood'];

4.5 Music-Key Mapping (Mixed In Key Standard)

Camelot Music-Key
12B / 12A E / C#m (Dbm)
1B / 1A B / G#m (Abm)
2B / 2A F# / D#m (Ebm)
3B / 3A Db / Bbm
4B / 4A Ab / Fm
5B / 5A Eb / Cm
6B / 6A Bb / Gm
7B / 7A F / Dm
8B / 8A C / Am
9B / 9A G / Em
10B / 10A D / Bm
11B / 11A A / F#m

5. UI-Komponenten (aus dem Mockup extrahieren)

5.1 Hero-Karte

┌──────────────────────────────────────────────────────────────┐
│ ▌  ┌────────┐                                                │
│ ▌  │        │   <Song-Titel> (30px, bold)                   │
│ ▌  │ Cover  │   <Artist>      (15px, dim)                    │
│ ▌  │ 240px  │                                                │
│ ▌  │        │   ┌────────┬────────┬────────┐                  │
│ ▌  │        │   │  Key   │  BPM   │ Match  │   ← Stats       │
│ ▌  │        │   │  4B    │  118   │  98%   │     (semantisch │
│ ▌  │        │   │ A-Dur  │  ± 2   │acoustid│      eingefärbt)│
│ ▌  │        │   └────────┴────────┴────────┘                  │
│ ▌  │        │                                                │
│ ▌  │        │   [4B] [Synth-Pop] [New Wave] [80s]             │
│ ▌  │        │   [Energetic] [Retro] [Euphoric]                │
│ ▌  │        │                                                │
│ ▌  │        │                          ┌──────────┐          │
│ ▌  │        │                          │  Wheel   │          │
│ ▌  │        │                          │  140px   │  ← persistent│
│ ▌  │        │                          └──────────┘          │
│ ▌  └────────┘                                                │
│ ▌  ▌  = 3px Akzent-Border in Key-Farbe (var(--key-current))   │
└──────────────────────────────────────────────────────────────┘

Hero-Expand-Modi (CSS-Klassen):

  • .hero.wheel-mode → Grid wird 1-Spalte, Cover + Info-Top + Info-Mid display:none, Wheel expandiert auf 520×520, ×-Button sichtbar
  • .hero.cover-mode → Cover auf 480×480, Info display:none, ×-Button sichtbar
  • .hero (normal) → 240px Cover | flex Info, persistent Wheel 140×140 unten rechts

5.2 Camelot-Wheel (24 Polygone in 2 Ringen)

  • Außenring (Major, B): 12 Polygone, 15° pro Segment
  • Innenring (Minor, A): 12 Polygone, je direkt innen neben dem entsprechenden Major
  • Current-Key: voll in Key-Farbe gefüllt
  • Kompatible Keys (primary): stark getönt (55% Alpha)
  • Kompatible Keys (secondary): leicht getönt (28% Alpha)
  • Andere Keys: dezent getönt (10% Alpha)
  • Center: Current-Key + Music-Key in großer Schrift

Wheel-Render-Algorithmus (pro Segment):

// 1. Hole Winkel aus codeToAngle[c] (NICHT aus Index!)
// 2. Bestimme rOut und rIn basierend auf isMajor
// 3. Berechne 4 Eckpunkte: p(rOut, a1), p(rOut, a2), p(rIn, a2), p(rIn, a1)
//    p(r, a) = [r * sin(rad), -r * cos(rad)]
// 4. Zeichne <polygon> mit den 4 Punkten
// 5. Platziere <text>-Labels (Camelot-Code + Music-Key) bei
//    p((rOut + rIn) / 2, aMid) = Mittelpunkt des Segments

5.3 Suggestions-Grid (10 Tracks)

Sortiert nach Kompatibilitäts-Stufe zu currentKey:

Stufe Compat-Label Visuelle Gewichtung
0 Perfect Höchste Priorität
1 Quinte -1 Hoch
2 Quinte +1 Hoch
3 Energy Mittel-Hoch
4 Scale Mittel-Hoch
5 Diagonal Mittel
6 Jaw's Niedrig
7 Mood Niedrigste

Jede Suggestion-Card:

┌──────────┐
│  Cover   │  ← 1:1 Thumbnail (iTunes API)
│  200px   │
└──────────┘
[4B grün]  120 BPM     ← Key-Badge in Key-Farbe, BPM rechts
Big in Japan          ← Titel
Alphaville · A        ← Artist + Music-Key
─────────────────────
PERFECT               ← Compat-Badge

6. Acceptance Criteria

  • Wheel rendert 24 Segmente in 2 Ringen, Major und Minor am GLEICHEN Winkel (12B+12A beide bei 0°)
  • Current-Key voll in Key-Farbe, primary-compat stark getönt, secondary-compat leicht getönt
  • Music-Keys (C, D, E, F#, etc.) sichtbar unter den Camelot-Codes im Wheel
  • Center zeigt Current-Key + Music-Key prominent
  • Hero expandiert inline bei Klick auf Wheel (smooth transition zu wheel-mode)
  • Hero expandiert inline bei Klick auf Cover (smooth transition zu cover-mode)
  • ×-Button schließt Expanded-Mode
  • Suggestions-Grid mit 10 Tracks, sortiert nach Compat-Stufe
  • Jede Suggestion zeigt Key-Badge in Key-Farbe + Compat-Badge
  • Dark/Light Toggle funktioniert, alle CSS-Variablen wechseln
  • Cover lädt via iTunes API (CORS-Server, nicht file://)
  • Layout passt in 1920×1080 ohne Scroll
  • Mobile Fallback (Single-Column Hero bei <760px)
  • UTF-8 ohne BOM (PowerShell-Encoding-Falle beachten)
  • Globale Farbänderung erfolgt durch Edit der colorMatrix (single source of truth)
  • Listening-Toggle: Klick auf 🎤 → Paused, Icon-Wechsel zu ⏸
  • Suchfeld-Fallback: Inline-Expand bei nicht-erkanntem Song
  • Echte AcoustID-Anbindung statt iTunes (Audio-Fingerprinting)
  • GetSongBPM-Integration mit Camelot↔Open-Key-Umrechnung
  • Last.fm + Essentia.js für Tags + Genre/Mood
  • AcousticBrainz-Postgres-Initial-Seed (7,5 Mio. Songs)
  • Suggestions-Engine nutzt mixingRules für Camelot-kompatible Vorschläge

7. Noch zu klären (vom User)

  1. Wheel-Geometrie visuell polieren — Logik ist korrekt, Rendering braucht Feinschliff (laut User "ist egal" vorerst zurückgestellt)
  2. Branding/Theme: DJ-Live-Look (Mockup v3, AI-slop), schlicht-monochrome, oder Synthwave/Neon? Empfehlung: schlicht, mit einzelnen Farbakzenten aus der colorMatrix.
  3. Listening-Toggle UX: Icon-Wechsel auf Pause-Icon, oder zusätzlicher Pause-Button neben dem Mic-Icon?
  4. Suchfeld-Position: Modal, Inline-Expand im Hero, oder separate Seite?
  5. Auth-Modell: Basic Auth über Caddy (jetzt), Authentik-SSO wenn verfügbar (später)?

8. Production-Schritte (Reihenfolge, mit Zeitaufwand)

Phase 1: Mockup → Production-Komponente (1-2 Tage)

  1. Wheel-Rendering finalisieren (Stroke-Widths, Anti-Aliasing) — 2-4h
  2. CSS in separate styles.css extrahieren — 1h
  3. JS in Module aufteilen (wheel.js, cover.js, app.js) — 2h
  4. Listening-Toggle implementieren — 30min
  5. Suchfeld-Fallback (Inline-Expand) — 2-3h

Phase 2: Backend + AcoustID (1-2 Tage)

  1. Express/Fastify Server-Setup — 2h
  2. AcoustID-Anbindung (POST /api/recognize) — 4-6h
  3. GetSongBPM + Last.fm (GET /api/metadata) — 4-6h
  4. iTunes Cover-Cache (24h) — 1-2h

Phase 3: Suggestions-Engine + DB (2-3 Tage)

  1. AcousticBrainz-Import (Init-Seed) — 6-8h
  2. Postgres-Schema + Migrations — 2-3h
  3. Suggestions-Engine (GET /api/suggestions) mit mixingRules — 8-12h
  4. ntfy-Worker für Quota-Alerts — 2h

Phase 4: Polish + Deploy (1 Tag)

  1. PWA (manifest, Service Worker) — 2-3h
  2. Docker-Setup (docker-compose.yml, Caddy, generate-env.sh) — 1-2h
  3. Auth (Basic Auth) — 1-2h
  4. Backup-Hook — 30min

Total-Aufwand: ca. 40-60 Stunden

9. Datei-Referenzen (in diesem Ideen-Ordner)

Datei Zweck
KeySelector.txt Vollständige Spezifikation (Text-Form, deploy-ready als Prompt)
KeySelector_Mockup_Wheel.html Interaktiver Mockup (standalone, im Browser öffnen)
KeySelector_Mockup_Wheel.css CSS-Stylesheet, aus dem Mockup extrahiert (10 KB, UTF-8 ohne BOM)
KeySelector_SPEC.md Diese Datei — Übergabe-Briefing für Entwickler
erledigt/KeySelector_Konzept.txt Original-Konzept (8 KB, gemerged)
erledigt/Music-Chooser.txt Vorheriger, obsoleter Entwurf

10. Jannik-Cloud-Integration

# services/keyselector/docker-compose.yml
services:
  keyselector:
    build: .
    restart: unless-stopped
    ports: []  # kein direkter Port, geht über Caddy
    environment:
      ACOUSTID_API_KEY: ${ACOUSTID_API_KEY}
      GETSONGBPM_API_KEY: ${GETSONGBPM_API_KEY}
      LASTFM_API_KEY: ${LASTFM_API_KEY}
      NTFY_URL: ${NTFY_URL}
      DATABASE_URL: postgresql://keyselector:${DB_PW}@postgres:5432/keyselector
      REDIS_URL: redis://redis:6379
    depends_on:
      - postgres
      - redis

# services/keyselector/keyselector.caddy
keyselector.orfel.de {
  reverse_proxy keyselector:3000
  encode gzip
}

API-Keys werden via generate-env.sh erzeugt und als .env.age (AGE-verschlüsselt) abgelegt. Shared Postgres-Instanz (eine DB pro Service, wird beim Deploy automatisch angelegt). Shared Redis für kurzen State.

11. Kontakt

  • User: Jannik
  • Workspace: C:\GitHub\
  • Cloud: Jannik-Cloud
  • Domain-Wildcard: *.orfel.de
  • Verfügbare Tools: AcoustID, GetSongBPM, Last.fm, ntfy (selbst gehostet), iTunes Search API
  • Domain für KeySelector: keyselector.orfel.de, keysel.orfel.de, ks.orfel.de

Erstellt 2026-08-26. Übergabe-Dokument für die Entwicklung von KeySelector.