ACE-Step-Inspire / mood_engine.py
41-807's picture
Upload folder using huggingface_hub
7e225ca verified
Raw
History Blame Contribute Delete
10.9 kB
"""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))