Spaces:
Running on Zero
Running on Zero
| """Named mood faders → label, key, meter. Radar uses top-5 moods only.""" | |
| from __future__ import annotations | |
| import json | |
| from typing import Any | |
| from presets import GENRES, MOOD_EXTRA | |
| # All mood names available as sliders (stable order). | |
| _ALL: list[str] = [] | |
| for _g in GENRES.values(): | |
| for _m in _g.get("moods", []): | |
| if _m not in _ALL: | |
| _ALL.append(_m) | |
| for _m in MOOD_EXTRA: | |
| if _m not in _ALL: | |
| _ALL.append(_m) | |
| MOOD_NAMES: tuple[str, ...] = tuple(_ALL) | |
| _MAJOR = ["C major", "G major", "D major", "A major", "F major", "Bb major", "Eb major"] | |
| _MINOR = ["A minor", "E minor", "D minor", "C minor", "F minor", "G minor", "B minor"] | |
| # Heuristics for key / meter from named moods | |
| _MINOR_BIAS = { | |
| "dark", | |
| "melancholic", | |
| "mysterious", | |
| "aggressive", | |
| "brooding", | |
| "gritty", | |
| "heartbroken", | |
| "bittersweet", | |
| "introspective", | |
| "suspenseful", | |
| } | |
| _MAJOR_BIAS = { | |
| "uplifting", | |
| "euphoric", | |
| "playful", | |
| "romantic", | |
| "celebratory", | |
| "sunny", | |
| "hopeful", | |
| "confident", | |
| "festive", | |
| "joyful", | |
| "anthemic", | |
| "catchy", | |
| } | |
| _WALTZISH = {"intimate", "tender", "sensual", "wistful", "pastoral", "smoky"} | |
| _COMPOUND = {"hypnotic", "groovy", "peak-time", "euphoric", "festive"} | |
| def _blank() -> dict[str, int]: | |
| return {m: 0 for m in MOOD_NAMES} | |
| def default_dims(genre: str) -> dict[str, int]: | |
| """Genre moods start high; shared extras low; primary genre mood strongest.""" | |
| dims = _blank() | |
| g = GENRES.get(genre, {}) | |
| primary = list(g.get("moods", [])) | |
| for i, name in enumerate(primary): | |
| if name in dims: | |
| dims[name] = max(35, 88 - i * 12) | |
| # slight presence on related extras so the board isn't empty-looking | |
| for name in MOOD_EXTRA: | |
| if name in dims and dims[name] == 0: | |
| dims[name] = 8 | |
| return dims | |
| def parse_dims(raw: Any) -> dict[str, int]: | |
| base = _blank() | |
| data: dict = {} | |
| if isinstance(raw, dict): | |
| data = raw | |
| elif isinstance(raw, str) and raw.strip(): | |
| try: | |
| data = json.loads(raw) | |
| except json.JSONDecodeError: | |
| data = {} | |
| for k in MOOD_NAMES: | |
| try: | |
| base[k] = max(0, min(100, int(data.get(k, base[k])))) | |
| except (TypeError, ValueError): | |
| pass | |
| return base | |
| def top_moods(dims: dict[str, int], n: int = 5) -> list[tuple[str, int]]: | |
| ranked = sorted(dims.items(), key=lambda kv: (-kv[1], kv[0])) | |
| return [(k, v) for k, v in ranked if v > 0][:n] or ranked[:n] | |
| def mood_label(dims: dict[str, int]) -> str: | |
| tops = top_moods(dims, 3) | |
| return " ".join(k for k, _ in tops) if tops else "atmospheric" | |
| def derive_key(dims: dict[str, int], genre_keys: list[str] | None = None) -> str: | |
| minor_s = sum(dims.get(m, 0) for m in _MINOR_BIAS) | |
| major_s = sum(dims.get(m, 0) for m in _MAJOR_BIAS) | |
| want_major = major_s >= minor_s | |
| pool = genre_keys or (_MAJOR if want_major else _MINOR) | |
| majors = [k for k in pool if "major" in k.lower()] | |
| minors = [k for k in pool if "minor" in k.lower()] | |
| preferred = majors if want_major else minors | |
| if preferred: | |
| warm = dims.get("romantic", 0) + dims.get("warm", 0) + dims.get("sensual", 0) | |
| return preferred[warm % len(preferred)] | |
| return pool[0] if pool else ("C major" if want_major else "A minor") | |
| def derive_meter(dims: dict[str, int]) -> str: | |
| if sum(dims.get(m, 0) for m in _WALTZISH) >= 90: | |
| return "3" | |
| if sum(dims.get(m, 0) for m in _COMPOUND) >= 140: | |
| return "6" | |
| if dims.get("aggressive", 0) >= 70 and dims.get("raw", 0) + dims.get("chaotic", 0) >= 40: | |
| return "2" | |
| return "4" | |
| def resolve_mood_bundle( | |
| raw_dims: Any, genre: str, genre_keys: list[str] | None = None | |
| ) -> tuple[str, str, str, str]: | |
| dims = default_dims(genre) if not raw_dims else parse_dims(raw_dims) | |
| if isinstance(raw_dims, dict) and not raw_dims: | |
| dims = default_dims(genre) | |
| label = mood_label(dims) | |
| key = derive_key(dims, genre_keys) | |
| meter = derive_meter(dims) | |
| return json.dumps(dims), label, key, meter | |
| # Style keyword → mood names to emphasize (soft defaults on style change) | |
| _STYLE_MOOD_HINTS: list[tuple[tuple[str, ...], tuple[str, ...]]] = [ | |
| (("dark", "doom", "drill", "trap", "industrial", "black"), ("dark", "aggressive", "brooding", "mysterious", "suspenseful")), | |
| (("ballad", "intimate", "quiet", "lovers", "tender"), ("romantic", "intimate", "vulnerable", "heartfelt", "tender")), | |
| (("dance", "club", "party", "house", "techno", "trance", "edm", "peak"), ("euphoric", "energetic", "peak-time", "festive", "hypnotic")), | |
| (("chill", "lo-fi", "lofi", "downtempo", "study", "ambient"), ("relaxed", "dreamy", "cozy", "focus", "melancholy")), | |
| (("epic", "heroic", "power", "trailer", "orchestral", "fanfare"), ("epic", "triumphant", "dramatic", "cinematic", "anthemic")), | |
| (("punk", "thrash", "metalcore", "hard", "garage"), ("aggressive", "raw", "rebellious", "energetic", "chaotic")), | |
| (("jazz", "bebop", "swing", "noir", "smoky"), ("sophisticated", "smoky", "playful", "nocturnal", "swinging")), | |
| (("folk", "acoustic", "americana", "campfire", "pastoral"), ("wistful", "warm", "pastoral", "storytelling", "hopeful")), | |
| (("soul", "r&b", "funk", "gospel", "neo-soul"), ("sensual", "smooth", "empowered", "romantic", "late-night")), | |
| (("reggae", "dub", "ska", "dancehall", "roots"), ("sunny", "relaxed", "groovy", "spiritual", "festive")), | |
| (("latin", "salsa", "bachata", "cumbia", "bossa", "reggaeton"), ("festive", "sensual", "sunny", "romantic", "dancefloor")), | |
| (("country", "bluegrass", "outlaw"), ("heartfelt", "homey", "bittersweet", "celebratory", "road-trip")), | |
| (("k-pop", "idol", "city-pop"), ("catchy", "glossy", "playful", "confident", "dramatic")), | |
| (("baroque", "classical", "chamber", "concerto", "minimalist", "romantic orchestral"), ("elegant", "serene", "passionate", "dramatic", "solemn")), | |
| (("blues", "delta", "chicago blues"), ("soulful", "heartbroken", "gritty", "late-night", "resilient")), | |
| (("afro", "world", "balkan", "fusion", "global"), ("celebratory", "joyful", "hypnotic", "adventurous", "warm")), | |
| ] | |
| # Style keyword → preferred instruments (merged with genre defaults) | |
| _STYLE_INSTRUMENT_HINTS: list[tuple[tuple[str, ...], tuple[str, ...]]] = [ | |
| (("orchestral", "epic", "trailer", "fanfare", "cinematic", "soundtrack"), ("string orchestra", "brass section", "timpani", "choir", "orchestral hits", "french horns")), | |
| (("baroque", "classical", "chamber", "concerto"), ("string quartet", "grand piano", "harpsichord", "woodwind section", "cello", "solo violin", "chamber ensemble")), | |
| (("solo piano", "piano", "ballad"), ("grand piano", "soft piano", "piano motif", "warm electric piano")), | |
| (("trap", "drill", "cloud"), ("808 bass", "trap snares", "crisp hi-hats", "atmospheric pads", "vocal chops")), | |
| (("house", "techno", "trance", "edm", "dance", "club"), ("four-on-the-floor kick", "analog synths", "sidechain bass", "arpeggiators", "risers", "filtered pads")), | |
| (("lo-fi", "lofi", "chill", "study", "downtempo"), ("dusty piano samples", "soft drums", "vinyl crackle", "mellow bass", "tape hiss", "jazzy chords")), | |
| (("metal", "thrash", "doom", "metalcore"), ("distorted guitars", "double bass drums", "thunderous bass", "shred solos", "orchestral hits")), | |
| (("punk", "garage", "hard rock"), ("distorted electric guitars", "live drums", "bass guitar", "electric guitar")), | |
| (("jazz", "bebop", "swing"), ("upright bass", "piano", "brushed drums", "saxophone", "trumpet", "rhodes")), | |
| (("folk", "acoustic", "americana", "campfire"), ("acoustic guitar", "fingerpicking", "harmonica", "mandolin", "light percussion")), | |
| (("reggae", "dub", "ska"), ("offbeat guitar skank", "deep bass", "rimshot drums", "organ bubble", "echo delays")), | |
| (("latin", "salsa", "bachata", "cumbia", "bossa"), ("syncopated percussion", "nylon guitar", "congas", "horn section", "bass groove")), | |
| (("country", "bluegrass"), ("acoustic guitar", "pedal steel", "fiddle", "banjo", "twangy electric guitar")), | |
| (("blues",), ("blues guitar", "harmonica", "upright piano", "shuffle drums", "walking bass")), | |
| (("soul", "r&b", "gospel", "funk"), ("warm electric piano", "smooth bass", "brushed drums", "horn section", "gospel organ", "rhodes")), | |
| (("synth", "synthwave", "electropop", "synth-pop"), ("bright synths", "analog synths", "arpeggiators", "punchy drums", "bass guitar")), | |
| (("ambient", "minimalist"), ("atmospheric pads", "filtered pads", "field recordings", "soft piano", "string orchestra")), | |
| ] | |
| def style_mood_dims(genre: str, style: str) -> dict[str, int]: | |
| """Genre mood defaults, nudged to fit the selected style.""" | |
| dims = default_dims(genre) | |
| s = (style or "").lower() | |
| if not s: | |
| return dims | |
| matched = False | |
| matched_moods: set[str] = set() | |
| for keys, moods in _STYLE_MOOD_HINTS: | |
| if any(k in s for k in keys): | |
| matched = True | |
| matched_moods.update(moods) | |
| for m in moods: | |
| if m in dims: | |
| dims[m] = min(100, max(dims[m] + 28, 62)) | |
| break | |
| if matched: | |
| # Soften non-boosted genre primaries slightly so the style read is clearer | |
| for m in list(GENRES.get(genre, {}).get("moods", [])): | |
| if m in dims and m not in matched_moods and dims[m] > 40: | |
| dims[m] = max(28, dims[m] - 12) | |
| return dims | |
| def style_instruments(genre: str, style: str) -> list[str]: | |
| """Preselect instruments for genre + style (always a subset of INSTRUMENTS_ALL).""" | |
| from presets import INSTRUMENTS_ALL | |
| g = GENRES.get(genre, GENRES.get(next(iter(GENRES)))) | |
| base = [i for i in (g or {}).get("instruments", []) if i in INSTRUMENTS_ALL] | |
| s = (style or "").lower() | |
| extra: list[str] = [] | |
| for keys, insts in _STYLE_INSTRUMENT_HINTS: | |
| if any(k in s for k in keys): | |
| for i in insts: | |
| if i and i in INSTRUMENTS_ALL: | |
| extra.append(i) | |
| break | |
| out: list[str] = [] | |
| for i in extra + base: | |
| if i not in out: | |
| out.append(i) | |
| if not out: | |
| out = list(INSTRUMENTS_ALL[:4]) | |
| return out[:5] | |
| def resolve_style_bundle(genre: str, style: str) -> tuple[str, str, str, str, list[str], int]: | |
| """Mood JSON, label, key, meter, instruments, bpm for a genre+style pair.""" | |
| from presets import suggest_style_bpm | |
| g = GENRES.get(genre) or GENRES[next(iter(GENRES))] | |
| dims = style_mood_dims(genre, style) | |
| dims_s, label, key, meter = resolve_mood_bundle(dims, genre, g.get("keys")) | |
| instruments = style_instruments(genre, style) | |
| bpm = suggest_style_bpm(genre, style) | |
| return dims_s, label, key, meter, instruments, bpm | |
| def mood_names_json() -> str: | |
| return json.dumps(list(MOOD_NAMES)) | |