"""LLM composer: plain-English song description -> MiniMax Music 3 inputs. Produces a title, short display tags, tagged lyrics and the three-part Structured Caption (Global Metadata / Vocal Details / Arrangement) the model was trained on, following the official prompting guide. Runs on the HF Inference Router (no GPU). """ import json import os import re import time from openai import OpenAI ROUTER_URL = "https://router.huggingface.co/v1" # (model, timeout_s, extra_body). Measured per compose (~600 output tokens): gpt-oss-120b on Groq # with low reasoning ~2s; DeepSeek-V4-Flash with thinking disabled ~7s via :fastest (Fireworks), # 14-35s on DeepInfra. Thinking/reasoning must be off: reasoning tokens otherwise dominate latency # (6-27s). Under load any provider can 429 transiently, so the chain is walked twice. _NO_THINK = {"thinking": {"type": "disabled"}} COMPOSER_MODELS = ( ("openai/gpt-oss-120b:groq", 30, {"reasoning_effort": "low"}), ("deepseek-ai/DeepSeek-V4-Flash-0731:fastest", 45, _NO_THINK), ("deepseek-ai/DeepSeek-V4-Flash-0731:deepinfra", 75, _NO_THINK), ("deepseek-ai/DeepSeek-V4-Flash-0731:novita", 100, _NO_THINK), ) VISUAL_MODELS = ( ("openai/gpt-oss-120b:groq", 30, {"reasoning_effort": "low"}), ("deepseek-ai/DeepSeek-V4-Flash-0731:fastest", 45, _NO_THINK), ) _CAPTION_CONTRACT = """The three caption fields follow the exact labeled style the model was trained on. Be concrete and musical; describe an energy arc and instrument lifecycles, never a static equipment list or decorative adjectives. Never contradict an explicit user constraint: instrumental stays instrumental; never reverse a required vocal gender, tempo limit, required instrument, or exclusion. Do not quote or paraphrase lyric lines inside the caption. Total caption length roughly 250-400 words. global_metadata: one paragraph, in order: "Basic Attributes: bpm is . key is , and scale is . ." then "Global Emotional Progression: ." then "Application Scenarios & Imagery: ." then "Sonics & Production Profile: ." vocal_details: one paragraph: "Vocal Gender & Timbre: Singer A (), ." then "Vocal Style: ." then "Harmony/Backing Vocals: ." then "Vocal FX: ." For instrumental pieces write "Instrumental, no vocals." and name the instrument or texture carrying the lead melodic role. arrangement: one paragraph: "Instrument Lifecycle Description (Primary/Secondary Layering): Primary: . Secondary: ." then "Groove & Foundation Progression: ." then "Embellishments, Textures & Spatial FX: ." State what enters, exits, changes or intensifies for every section of the song, aligned with the lyric section tags.""" _LYRICS_RULES = """lyrics: singable lyrics using ONLY these section tags, each ALWAYS ALONE on its own line: [intro] [verse] [pre-chorus] [chorus] [post-chorus] [bridge] [instrumental] [solo] [outro]. Never put words on the same line as a tag. Size the structure to the duration: <=30s: one verse + one chorus; ~60s: verse/pre-chorus/chorus/verse/chorus; >=120s: full structure with bridge and outro. Roughly 12-16 sung words per 10 seconds. Musical instructions (tempo, instruments, dynamics) never belong in the lyrics. If the song is instrumental, use [instrumental] sections with no words. Write the lyrics in the language the user asks for (default: English).""" COMPOSER_SYSTEM = f"""You write inputs for MiniMax Music 3, a lyrics+description music generation model. Given a song description and a target duration, produce: 1. title: a short, catchy song title (2-5 words, no quotes). 2. tags: 3-5 short comma-separated style tags for a music feed card, e.g. "synth-pop, female vocals, 120 bpm, euphoric". 3. {_LYRICS_RULES} 4-6. global_metadata, vocal_details, arrangement: a structured caption. {_CAPTION_CONTRACT} Unless the user explicitly asks for an instrumental, the song HAS a singer: vocal_details must describe that singer (gender, timbre, style) and must never say "Instrumental". Answer with ONLY a JSON object with keys: title, tags, lyrics, global_metadata, vocal_details, arrangement. Inside the lyrics string, separate lines with JSON newline escapes, never with the two literal characters backslash and n.""" VISUAL_SYSTEM = ( "Reply with exactly ONE concrete visual noun (a physical object, animal, or natural element) " "that captures the essence of this song. No explanation, no punctuation, just the single word." ) SECTION_TAG_RE = re.compile(r"^\s*\[(intro|verse|pre-chorus|chorus|post-chorus|bridge|instrumental|solo|outro)\]\s*$", re.I) def _client(): key = os.environ.get("HF_TOKEN", "") if not key: raise RuntimeError("HF_TOKEN is not configured on this Space (needed for the composer LLM).") return OpenAI(base_url=ROUTER_URL, api_key=key, max_retries=0) def _chat(system, user, *, temperature=0.8, max_tokens=6000, models=COMPOSER_MODELS, passes=2): client = _client() last_error = None for attempt in range(passes): for model, timeout, extra in models: try: completion = client.with_options(timeout=timeout).chat.completions.create( model=model, messages=[{"role": "system", "content": system}, {"role": "user", "content": user}], temperature=temperature, max_tokens=max_tokens, extra_body=extra, ) text = completion.choices[0].message.content or "" text = re.sub(r".*?", "", text, flags=re.DOTALL).strip() if not text: raise ValueError(f"empty reply (finish_reason={completion.choices[0].finish_reason})") return text except Exception as e: # noqa: BLE001 print(f"[composer] {model} failed (pass {attempt + 1}): {type(e).__name__}: {e}", flush=True) last_error = e if attempt < passes - 1: time.sleep(3) raise RuntimeError("The composer model is overloaded right now; please try again in a moment.") from last_error def _parse_json(text, required): start, end = text.find("{"), text.rfind("}") if start == -1 or end <= start: raise ValueError("no JSON object in composer reply") data = json.loads(text[start : end + 1], strict=False) # tolerate raw newlines inside strings missing = [k for k in required if not str(data.get(k, "")).strip()] if missing: raise ValueError(f"composer reply missing keys: {missing}") return data def normalize_lyrics(lyrics: str) -> str: """Put every section tag alone on its own line (the model drops words that share a tag's line).""" out = [] text = (lyrics or "").replace("\r", "").replace("\\n", "\n") for line in text.split("\n"): m = re.match(r"^\s*(\[[^\]]+\])\s*(.*)$", line) if m and m.group(2).strip(): out.append(m.group(1).lower()) out.append(m.group(2).strip()) elif m: out.append(m.group(1).lower()) else: out.append(line.rstrip()) text = "\n".join(out) return re.sub(r"\n{3,}", "\n\n", text).strip() def compose(description: str, duration: float, instrumental: bool = False) -> dict: """Return {title, tags, lyrics, caption, global_metadata, vocal_details, arrangement}.""" user = f"Song description: {description.strip()}\nTarget duration: {int(duration)} seconds." if instrumental: user += "\nThis song is INSTRUMENTAL: no vocals at all. Use [instrumental] sections only, with no words." else: user += "\nThe song has sung vocals (pick a fitting singer if the description does not specify one)." required = ("title", "lyrics", "global_metadata", "vocal_details", "arrangement") last_error = None for _ in range(2): try: data = _parse_json(_chat(COMPOSER_SYSTEM, user), required) vd = str(data["vocal_details"]).lower() if not instrumental and vd.startswith("instrumental"): raise ValueError("vocal_details says instrumental for a vocal song") break except (ValueError, json.JSONDecodeError) as e: print(f"[composer] bad reply: {e}", flush=True) last_error = e else: raise RuntimeError("The composer returned an unusable reply; please try again.") from last_error lyrics = normalize_lyrics(str(data["lyrics"])) if instrumental and not any(SECTION_TAG_RE.match(l) for l in lyrics.split("\n")): lyrics = "[instrumental]\n\n" + lyrics title = str(data["title"]).strip().strip('"\'')[:80] or "Untitled" tags = str(data.get("tags", "")).strip().strip(".")[:120] global_metadata = str(data["global_metadata"]).strip() vocal_details = str(data["vocal_details"]).strip() arrangement = str(data["arrangement"]).strip() caption = "\n".join(s for s in (global_metadata, vocal_details, arrangement) if s) return { "title": title, "tags": tags, "lyrics": lyrics, "caption": caption, "global_metadata": global_metadata, "vocal_details": vocal_details, "arrangement": arrangement, } def visual_word(title: str, tags: str, lyrics: str, description: str) -> str: """One evocative noun for the cover-art prompt; falls back to the description's first words.""" fallback = " ".join((description or title or "music").split()[:2]) try: text = _chat( VISUAL_SYSTEM, f"Title: {title}\nTags: {tags}\nLyrics: {lyrics[:300]}", temperature=0.7, max_tokens=2000, models=VISUAL_MODELS, passes=1, ) word = text.split()[0].strip('."\'!,') if text.split() else "" return word or fallback except Exception as e: # noqa: BLE001 print(f"[composer] visual word failed: {e}", flush=True) return fallback