41-807's picture
Upload folder using huggingface_hub
6cb9905 verified
Raw
History Blame Contribute Delete
37.4 kB
"""ACE-Step Inspire — creative text-to-song Space for ACE-Step 1.5."""
from __future__ import annotations
import logging
import os
import random
import sys
import time
import traceback
from typing import Optional
# ZeroGPU: import spaces BEFORE torch
try:
import spaces
HAS_SPACES = True
except ImportError:
HAS_SPACES = False
for _proxy in ("http_proxy", "https_proxy", "HTTP_PROXY", "HTTPS_PROXY", "ALL_PROXY"):
os.environ.pop(_proxy, None)
os.environ.setdefault("GRADIO_ANALYTICS_ENABLED", "False")
os.environ.setdefault("HF_MODULES_CACHE", "/tmp/hf_modules")
os.environ.setdefault("MPLCONFIGDIR", "/tmp/matplotlib")
# ── Logging (stdout so Hugging Face Space logs capture everything) ───────────
LOG_LEVEL = os.environ.get("LOG_LEVEL", "INFO").upper()
logging.basicConfig(
level=getattr(logging, LOG_LEVEL, logging.INFO),
format="%(asctime)s | %(levelname)-7s | %(name)s | %(message)s",
datefmt="%H:%M:%S",
stream=sys.stdout,
force=True,
)
log = logging.getLogger("ace-inspire")
# Keep third-party noise down unless debugging
logging.getLogger("httpx").setLevel(logging.WARNING)
logging.getLogger("httpcore").setLevel(logging.WARNING)
logging.getLogger("urllib3").setLevel(logging.WARNING)
import gradio as gr
import torch
from diffusers import AceStepPipeline
from audio_export import AUDIO_FORMATS, DEFAULT_AUDIO_FORMAT, write_audio
from lyrics_gen import build_caption, generate_lyrics
from mood_engine import default_dims, resolve_mood_bundle, resolve_style_bundle
from presets import (
ALL_STYLES,
ALL_VOCALS,
DEFAULT_DURATION,
DEFAULT_GENRE,
DEFAULT_MODEL,
GENRES,
INSTRUMENTS_ALL,
MODELS,
STRUCTURES,
USE_CASES,
VOCAL_LANGUAGES,
suggest_use_case_bpm,
)
from console_ui import (
AUTO_OPTS_HTML,
BRAND_HTML,
CONSOLE_CSS,
CONSOLE_JS,
DURATION_BAY_HTML,
FORMAT_PADS_HTML,
GENRE_PADS_HTML,
INSTRUMENTAL_PAD_HTML,
INSTRUMENTS_BROWSER_HTML,
KNOB_HTML,
LANGUAGE_PADS_HTML,
MOOD_BOARD_HTML,
PRESET_HTML,
STRUCTURE_PADS_HTML,
STYLE_PADS_HTML,
USECASE_PADS_HTML,
VOCAL_PADS_HTML,
)
def _gpu_mem_str() -> str:
if not torch.cuda.is_available():
return "cuda=unavailable"
try:
free, total = torch.cuda.mem_get_info()
alloc = torch.cuda.memory_allocated()
reserved = torch.cuda.memory_reserved()
return (
f"gpu_free={free/1e9:.2f}G/{total/1e9:.2f}G "
f"alloc={alloc/1e9:.2f}G reserved={reserved/1e9:.2f}G"
)
except Exception as e:
return f"gpu_mem_err={e}"
log.info(
"Boot | HAS_SPACES=%s SPACE_ID=%s cuda_available=%s torch=%s",
HAS_SPACES,
os.environ.get("SPACE_ID"),
torch.cuda.is_available(),
torch.__version__,
)
# ── Pipeline cache (CPU-resident; moved to CUDA inside @spaces.GPU) ──────────
_pipes: dict[str, AceStepPipeline] = {}
_current_repo: Optional[str] = None
def _load_pipe(repo_id: str) -> AceStepPipeline:
global _current_repo
if repo_id in _pipes:
log.info("Pipeline cache hit: %s", repo_id)
return _pipes[repo_id]
# Keep only one heavy pipeline in memory on ZeroGPU
if _pipes:
log.info("Clearing cached pipelines: %s", list(_pipes))
_pipes.clear()
t0 = time.perf_counter()
log.info("Loading pipeline from_pretrained(%s) dtype=bfloat16 …", repo_id)
try:
pipe = AceStepPipeline.from_pretrained(repo_id, torch_dtype=torch.bfloat16)
except Exception as e:
log.exception("from_pretrained failed for %s", repo_id)
raise gr.Error(
f"Failed to load model `{repo_id}`.\n"
f"This Space only supports Diffusers AceStepPipeline checkpoints.\n\n{e}"
) from e
if hasattr(pipe, "vae") and hasattr(pipe.vae, "enable_tiling"):
pipe.vae.enable_tiling()
log.debug("VAE tiling enabled")
_pipes[repo_id] = pipe
_current_repo = repo_id
log.info("Pipeline ready: %s (%.1fs)", repo_id, time.perf_counter() - t0)
return pipe
def _model_cfg(model_name: str) -> dict:
return MODELS.get(model_name, MODELS[DEFAULT_MODEL])
# Preload default checkpoint on CPU during startup so ZeroGPU time isn't spent downloading.
try:
_DEFAULT_REPO = MODELS[DEFAULT_MODEL]["repo_id"]
log.info("Preloading default model on CPU: %s", _DEFAULT_REPO)
_load_pipe(_DEFAULT_REPO)
log.info("Default model ready | %s", _gpu_mem_str())
except Exception as e:
log.exception("Default model preload skipped: %s", e)
def apply_style_defaults(genre: str, style: str):
"""BPM + mood matrix + instruments for the selected style."""
gname = genre or DEFAULT_GENRE
g = GENRES.get(gname) or GENRES[DEFAULT_GENRE]
sname = style or g["styles"][0]
# Pad genre/style can race — resolve to a genre that owns this style.
if sname not in g["styles"]:
for name, gg in GENRES.items():
if sname in gg["styles"]:
gname, g = name, gg
break
else:
sname = g["styles"][0]
dims_s, label, key, meter, instruments, bpm = resolve_style_bundle(gname, sname)
return (
gr.update(value=bpm),
gr.update(choices=INSTRUMENTS_ALL, value=instruments),
dims_s,
label,
key,
meter,
)
def apply_use_case_bpm(use_case: str):
"""Set tempo from the use-case default when one is chosen."""
bpm = suggest_use_case_bpm(use_case or "(none)")
if bpm is None:
return gr.update()
return gr.update(value=int(bpm))
def apply_genre(genre: str):
"""Fill creative defaults from genre + first style (all still user-overridable)."""
g = GENRES[genre]
style0 = g["styles"][0]
dims_s, label, key, meter, instruments, bpm = resolve_style_bundle(genre, style0)
# Keep full choice unions so pad-driven values never fail Gradio validation.
return (
gr.update(choices=ALL_STYLES, value=style0),
gr.update(choices=ALL_VOCALS, value=g["vocal"][0]),
gr.update(choices=INSTRUMENTS_ALL, value=instruments),
gr.update(value=bpm),
dims_s,
label,
key,
meter,
)
def surprise_me(genre: str, theme: str, language: str = "English", instructions: str = ""):
"""Randomize creative controls within the selected genre."""
g = GENRES.get(genre, GENRES[DEFAULT_GENRE])
style = random.choice(g["styles"])
vocal = random.choice(g["vocal"])
n_inst = min(3, len(g["instruments"]))
instruments = random.sample(g["instruments"], k=n_inst)
bpm = random.randint(*g["bpm"])
# jitter mood dims around genre defaults
base = default_dims(genre)
jittered = {k: max(0, min(100, v + random.randint(-18, 18))) for k, v in base.items()}
dims_s, label, key, meter = resolve_mood_bundle(jittered, genre, g.get("keys"))
structure = random.choice([s for s in STRUCTURES if s != "Instrumental (no lyrics)"])
use_case = random.choice(USE_CASES[1:])
lyrics = generate_lyrics(
genre,
label,
theme or label,
structure,
instrumental=False,
language=language,
instructions=instructions or "",
)
caption = build_caption(
genre,
style,
label,
instruments,
vocal,
bpm,
use_case,
instrumental=False,
auto_extra=True,
notes=instructions or "",
)
log.info("Surprise | style=%s mood=%s bpm=%s key=%s lang=%s", style, label, bpm, key, language)
return (
style,
vocal,
instruments,
bpm,
dims_s,
label,
key,
meter,
structure,
use_case,
lyrics,
caption,
)
def on_generate_lyrics(genre, mood, theme, structure, instrumental, seed, language, instructions=""):
seed_i = int(seed) if seed is not None and int(seed) >= 0 else None
log.info(
"Lyrics gen | genre=%s mood=%s structure=%s instrumental=%s seed=%s lang=%s theme=%r notes=%r",
genre,
mood,
structure,
instrumental,
seed_i,
language,
(theme or "")[:80],
(instructions or "")[:80],
)
text = generate_lyrics(
genre=genre,
mood=mood or "emotional",
theme=theme or mood or genre,
structure_name=structure,
instrumental=bool(instrumental),
seed=seed_i,
language=language,
instructions=instructions or "",
)
log.info("Lyrics gen done | chars=%d lang=%s", len(text), language)
return text
def peek_prompt(genre, style, mood, instruments, vocal, bpm, use_case, instrumental, lyrics, instructions=""):
"""Build current caption + show lyrics separately (lyrics are NOT inside the caption)."""
caption = build_caption(
genre=genre,
style=style,
mood=mood,
instruments=instruments if isinstance(instruments, list) else [],
vocal=vocal,
bpm=int(bpm) if bpm else 120,
use_case=use_case,
instrumental=bool(instrumental),
auto_extra=True,
notes=instructions or "",
)
lyric_text = "[Instrumental]" if instrumental else ((lyrics or "").strip() or "(no lyrics yet)")
view = (
"=== CAPTION / STYLE PROMPT ===\n"
f"{caption}\n\n"
"=== LYRICS (separate ACE-Step input, not part of caption) ===\n"
f"{lyric_text}"
)
log.info("Prompt peek | caption=%r", caption[:200])
return view, gr.update(visible=True)
def _meter_label(timesignature: str) -> str:
ts = str(timesignature or "4")
return "6/8" if ts == "6" else f"{ts}/4"
def _generate_impl(
model_name,
lyrics,
duration,
bpm,
keyscale,
timesignature,
language_label,
instrumental,
steps,
guidance,
shift,
seed,
random_seed,
genre=None,
style=None,
mood=None,
instruments=None,
vocal=None,
use_case=None,
audio_format=None,
auto_play=False, # client-side only; kept in signature for Gradio wiring
instructions="",
):
t_run = time.perf_counter()
log.info("=" * 60)
log.info(
"Generate start | model=%r duration=%s bpm=%s key=%s meter=%s lang=%s instrumental=%s format=%s",
model_name,
duration,
bpm,
keyscale,
timesignature,
language_label,
instrumental,
audio_format,
)
log.info("CUDA before | available=%s | %s", torch.cuda.is_available(), _gpu_mem_str())
try:
cfg = _model_cfg(model_name)
repo_id = cfg["repo_id"]
log.info("Resolved model | name=%r repo=%s turbo=%s defaults=%s", model_name, repo_id, cfg["turbo"], cfg)
t0 = time.perf_counter()
pipe = _load_pipe(repo_id)
log.info("Moving pipeline to CUDA … | %s", _gpu_mem_str())
pipe.to("cuda")
log.info("Pipeline on CUDA (%.1fs) | %s", time.perf_counter() - t0, _gpu_mem_str())
duration = int(duration)
duration = max(10, min(duration, 3600))
fmt = (audio_format or DEFAULT_AUDIO_FORMAT).strip()
if fmt not in AUDIO_FORMATS:
fmt = DEFAULT_AUDIO_FORMAT
if instrumental:
lyrics_text = "[Instrumental]"
else:
lyrics_text = (lyrics or "").strip() or "[Instrumental]"
seed_val = -1 if seed is None else int(seed)
use_seed = random.randint(0, 2**31 - 1) if random_seed or seed_val < 0 else seed_val
generator = torch.Generator(device="cuda").manual_seed(use_seed)
steps = int(steps) if steps else cfg["steps"]
guidance = float(guidance) if guidance is not None else cfg["guidance"]
shift = float(shift) if shift is not None else cfg["shift"]
if cfg["turbo"]:
guidance = 1.0
lang = VOCAL_LANGUAGES.get(language_label, "en")
try:
bpm_i = int(float(bpm)) if bpm is not None and str(bpm).strip() != "" else None
except (TypeError, ValueError):
bpm_i = None
if bpm_i is not None and bpm_i < 1:
bpm_i = None
# ACE docs: BPM soft-control range is ~30–300. Outside that, metas are OOD.
bpm_meta = None
bpm_note = ""
if bpm_i is not None:
bpm_meta = max(30, min(300, bpm_i))
if bpm_meta != bpm_i:
bpm_note = f" (metas clamped {bpm_i}{bpm_meta}; ACE range 30–300)"
log.warning("BPM %s outside ACE metas range; clamping to %s", bpm_i, bpm_meta)
log.info("Auto-building prompt | live_bpm=%s metas_bpm=%s", bpm_i, bpm_meta)
notes = (instructions or "").strip()
prompt = build_caption(
genre=genre or DEFAULT_GENRE,
style=style or "",
mood=mood or "",
instruments=instruments if isinstance(instruments, list) else [],
vocal=vocal or "",
bpm=int(bpm_i or bpm_meta or 120),
use_case=use_case or "(none)",
instrumental=bool(instrumental),
auto_extra=True,
notes=notes,
)
if not prompt or not str(prompt).strip():
raise gr.Error("Could not build a prompt from the current controls.")
# Tempo is soft text conditioning; turbo has no CFG — stress BPM in the instruction.
bpm_for_inst = bpm_meta if bpm_meta is not None else int(bpm_i or 120)
instruction = (
"Fill the audio semantic mask based on the given conditions. "
f"Strictly match tempo: exactly {bpm_for_inst} BPM with a clear steady pulse at that speed. "
"Do not use half-time or double-time feels that hide the target BPM:"
)
if notes:
instruction = f"{instruction} Additional creative notes (follow these; do not sing them): {notes}"
kwargs = dict(
prompt=str(prompt).strip(),
lyrics=lyrics_text,
audio_duration=float(duration),
vocal_language=lang,
num_inference_steps=steps,
guidance_scale=guidance,
shift=shift,
generator=generator,
instruction=instruction,
bpm=bpm_meta,
keyscale=keyscale or None,
timesignature=str(timesignature) if timesignature else None,
task_type="text2music",
)
log.info(
"Inference params | bpm_meta=%s bpm_req=%s key=%s meter=%s steps=%s guidance=%s shift=%s seed=%s duration=%ss lang=%s turbo=%s",
bpm_meta,
bpm_i,
keyscale,
timesignature,
steps,
guidance,
shift,
use_seed,
duration,
lang,
cfg["turbo"],
)
log.info("Instruction: %r", instruction)
log.info("Prompt (%d chars): %r", len(kwargs["prompt"]), kwargs["prompt"][:240])
log.info("Lyrics (%d chars): %r", len(lyrics_text), lyrics_text[:240].replace("\n", " | "))
if cfg["turbo"] and bpm_meta is not None:
log.info(
"Tempo note: Turbo is guidance-distilled (no CFG). BPM is soft metadata only — "
"XL SFT follows tempo more reliably."
)
t_inf = time.perf_counter()
log.info("pipe(...) starting …")
try:
output = pipe(**kwargs)
audio = output.audios[0]
except Exception as e:
log.error("pipe(...) failed after %.1fs | %s", time.perf_counter() - t_inf, _gpu_mem_str())
log.error("Traceback:\n%s", traceback.format_exc())
raise gr.Error(f"Generation failed:\n{type(e).__name__}: {e}") from e
log.info(
"pipe(...) done in %.1fs | audio_type=%s shape=%s | %s",
time.perf_counter() - t_inf,
type(audio).__name__,
getattr(audio, "shape", None),
_gpu_mem_str(),
)
if isinstance(audio, torch.Tensor):
audio = audio.detach().float().cpu().numpy()
if audio.ndim == 2:
if audio.shape[0] <= 8 and audio.shape[0] < audio.shape[1]:
audio = audio.T
sr = getattr(pipe, "sample_rate", 48000)
out_path = write_audio(audio, sr, fmt)
size_mb = os.path.getsize(out_path) / 1e6
ext = os.path.splitext(out_path)[1].lstrip(".").upper() or fmt
meta = (
f"Model: {repo_id}\n"
f"Seed: {use_seed} | Steps: {steps} | Guidance: {guidance} | Shift: {shift}\n"
f"Duration: {duration}s | BPM: {bpm_meta}{bpm_note} | Key: {keyscale} | Meter: {_meter_label(str(timesignature))}\n"
f"Language: {lang} | Format: {ext}\n"
f"Wall time: {time.perf_counter() - t_run:.1f}s | File: {size_mb:.1f} MB @ {sr} Hz"
)
if cfg["turbo"]:
meta += (
"\nTempo: Turbo follows BPM softly (no CFG). "
"For stronger tempo lock, switch to XL SFT and raise Guidance."
)
log.info(
"Generate OK | %.1fs | file=%s format=%s (%.1f MB) | %s",
time.perf_counter() - t_run,
out_path,
ext,
size_mb,
_gpu_mem_str(),
)
log.info("=" * 60)
prompt_bundle = (
"=== CAPTION / STYLE PROMPT ===\n"
f"{prompt}\n\n"
"=== LYRICS (separate ACE-Step input, not part of caption) ===\n"
f"{lyrics_text}"
)
return (
gr.update(value=out_path, autoplay=bool(auto_play)),
meta,
prompt_bundle,
"<div class='empty-hint' style='color:#3dffb0'>Track loaded on deck.</div>",
out_path,
)
except gr.Error:
log.error("Generate aborted (Gradio Error) after %.1fs", time.perf_counter() - t_run)
raise
except Exception as e:
log.error("Generate crashed after %.1fs | %s", time.perf_counter() - t_run, _gpu_mem_str())
log.error("Traceback:\n%s", traceback.format_exc())
raise gr.Error(f"Unexpected error:\n{type(e).__name__}: {e}") from e
finally:
sys.stdout.flush()
sys.stderr.flush()
if HAS_SPACES:
generate_music = spaces.GPU(duration=300)(_generate_impl)
else:
generate_music = _generate_impl
# ── UI ───────────────────────────────────────────────────────────────────────
dark_theme = gr.themes.Base(
primary_hue="amber",
secondary_hue="slate",
neutral_hue="zinc",
font=[gr.themes.GoogleFont("Rajdhani"), "ui-sans-serif", "system-ui"],
font_mono=[gr.themes.GoogleFont("Share Tech Mono"), "monospace"],
).set(
body_background_fill="#07080b",
body_text_color="#e8ecf4",
block_background_fill="#141821",
block_border_color="#2a3142",
block_label_text_color="#6b7385",
button_primary_background_fill="#ffb020",
button_primary_text_color="#1a1200",
border_color_primary="#2a3142",
input_background_fill="#0a0c11",
)
with gr.Blocks(
title="INSPIRE · ACE-Step",
theme=dark_theme,
css=CONSOLE_CSS,
js=CONSOLE_JS,
elem_classes=["console-shell"],
) as demo:
gr.HTML(BRAND_HTML)
# ── Generate (1/8) + playback deck (7/8), equal height ────────────────
# Plain Row (not Group) so Gradio does not paint a full-bleed card wider than the racks.
with gr.Row(elem_classes=["deck-shell", "deck-shell-row"], equal_height=True):
with gr.Column(scale=1, min_width=0, elem_classes=["deck-gen-col"]):
generate_btn = gr.Button(
"Generate\nsong",
variant="primary",
elem_id="generate-song-btn",
)
with gr.Column(scale=7, min_width=0, elem_classes=["deck-panel"], elem_id="deck-panel"):
gr.HTML("<div class='deck-label'>PLAYBACK DECK</div>")
deck_hint = gr.HTML(
"<div class='empty-hint'>No track yet — set the bay, then hit GENERATE SONG.</div>",
elem_id="deck-hint",
)
audio_out = gr.Audio(
label=None,
show_label=False,
type="filepath",
elem_id="deck-audio",
interactive=False,
min_width=0,
show_download_button=True,
autoplay=False,
)
# Hidden download target — clicked by JS when "Download automatically" is on
autodl_btn = gr.DownloadButton(
label="Download track",
value=None,
elem_id="inspire-autodl",
visible=True,
)
_dims0, _mood0, _key0, _meter0 = resolve_mood_bundle(None, DEFAULT_GENRE, GENRES[DEFAULT_GENRE]["keys"])
# ── Three racks ─────────────────────────────────────────────────────────
with gr.Row(elem_classes=["rack-row"]):
# LEFT — machine
with gr.Column(scale=2, min_width=200, elem_classes=["rack-panel"]):
gr.HTML("<div class='rack-title'>MACHINE</div>")
gr.HTML(DURATION_BAY_HTML)
duration = gr.Number(
value=DEFAULT_DURATION,
precision=0,
minimum=10,
maximum=3600,
label="Duration (sec)",
elem_id="duration",
elem_classes=["pad-hidden"],
)
model = gr.Dropdown(
choices=list(MODELS.keys()),
value=DEFAULT_MODEL,
label="Model",
elem_id="model",
)
gr.HTML(FORMAT_PADS_HTML)
audio_format = gr.Dropdown(
choices=list(AUDIO_FORMATS),
value=DEFAULT_AUDIO_FORMAT,
label="Export format",
elem_id="audio_format",
elem_classes=["pad-hidden"],
)
gr.HTML(AUTO_OPTS_HTML)
auto_download = gr.Checkbox(
label="Download automatically",
value=False,
elem_id="auto_download",
elem_classes=["pad-hidden"],
)
auto_play = gr.Checkbox(
label="Play automatically",
value=False,
elem_id="auto_play",
elem_classes=["pad-hidden"],
)
gr.HTML(PRESET_HTML)
# CENTER — creative
with gr.Column(scale=4, min_width=360, elem_classes=["rack-panel"]):
gr.HTML("<div class='rack-title'>MIX BAY · CREATIVE</div>")
gr.HTML(USECASE_PADS_HTML)
use_case = gr.Dropdown(
choices=USE_CASES,
value="(none)",
label="Use case",
elem_id="use_case",
elem_classes=["pad-hidden"],
)
gr.HTML(GENRE_PADS_HTML)
genre = gr.Dropdown(
choices=list(GENRES.keys()),
value=DEFAULT_GENRE,
label="Genre",
elem_id="genre",
elem_classes=["pad-hidden"],
)
gr.HTML(STYLE_PADS_HTML)
style = gr.Dropdown(
choices=ALL_STYLES,
value=GENRES[DEFAULT_GENRE]["styles"][0],
label="Style",
elem_id="style",
elem_classes=["pad-hidden"],
)
gr.HTML(VOCAL_PADS_HTML)
vocal = gr.Dropdown(
choices=ALL_VOCALS,
value=GENRES[DEFAULT_GENRE]["vocal"][0],
label="Vocal character",
elem_id="vocal",
elem_classes=["pad-hidden"],
)
theme = gr.Textbox(
label="Theme",
placeholder="neon heartbreak, victory after failure…",
lines=1,
max_lines=1,
elem_id="inspire_theme",
)
instructions = gr.Textbox(
label="Additional instructions",
placeholder="Writer / production notes — used as context, not sung "
"(e.g. female POV, no rain metaphors, keep verses short…)",
lines=3,
elem_id="inspire_instructions",
)
gr.HTML("<div class='rack-title' style='margin-top:0.55rem'>MOOD MATRIX</div>")
gr.HTML(MOOD_BOARD_HTML)
# Hidden fields driven by the custom mood board / used by generation
mood_dims = gr.Textbox(value=_dims0, elem_id="mood_dims", label="dims")
mood = gr.Textbox(value=_mood0, elem_id="mood_label_box", label="mood")
keyscale = gr.Textbox(value=_key0, elem_id="key_box", label="key")
timesignature = gr.Textbox(value=_meter0, elem_id="meter_box", label="meter")
gr.HTML(INSTRUMENTS_BROWSER_HTML)
instruments = gr.CheckboxGroup(
choices=INSTRUMENTS_ALL,
value=[i for i in GENRES[DEFAULT_GENRE]["instruments"] if i in INSTRUMENTS_ALL][:3]
or INSTRUMENTS_ALL[:3],
label="Instruments / textures",
elem_id="instrument-pads",
elem_classes=["pad-hidden"],
)
# RIGHT — tempo + lyrics
with gr.Column(scale=2, min_width=200, elem_classes=["rack-panel"]):
gr.HTML("<div class='rack-title'>PERFORMANCE</div>")
gr.HTML(KNOB_HTML)
bpm = gr.Number(
value=GENRES[DEFAULT_GENRE]["default_bpm"],
precision=0,
minimum=1,
label="BPM",
elem_id="bpm_number",
elem_classes=["pad-hidden"],
)
gr.HTML("<div class='rack-title' style='margin-top:0.85rem'>LYRICS</div>")
gr.HTML(LANGUAGE_PADS_HTML)
language = gr.Dropdown(
choices=list(VOCAL_LANGUAGES.keys()),
value="English",
label="Language",
elem_id="language",
elem_classes=["pad-hidden"],
)
gr.HTML(STRUCTURE_PADS_HTML)
structure = gr.Dropdown(
choices=list(STRUCTURES.keys()),
value=list(STRUCTURES.keys())[0],
label="Structure",
elem_id="structure",
elem_classes=["pad-hidden"],
)
gr.HTML(INSTRUMENTAL_PAD_HTML)
instrumental = gr.Checkbox(
label="Instrumental",
value=False,
elem_id="instrumental",
elem_classes=["pad-hidden"],
)
gen_lyrics_btn = gr.Button("Generate lyrics", elem_id="lyrics-btn")
lyrics = gr.Textbox(
label="Pad",
lines=10,
placeholder="[verse] / [chorus] …",
elem_id="lyrics",
)
# Hidden state for prompt (filled on generate / peek)
prompt_state = gr.State("")
meta_state = gr.State("No run yet.")
# ── Action bar ──────────────────────────────────────────────────────────
with gr.Row(elem_classes=["action-bar"]):
surprise_btn = gr.Button("Surprise", elem_id="surprise-btn")
prompt_btn = gr.Button("Prompt", elem_id="prompt-btn")
info_btn = gr.Button("Info", elem_id="info-btn")
expert_btn = gr.Button("Expert", elem_id="expert-btn")
# ── Modals ──────────────────────────────────────────────────────────────
with gr.Column(visible=False, elem_classes=["modal-shell"]) as prompt_modal:
with gr.Group(elem_classes=["modal-card"]):
gr.HTML("<div class='rack-title'>CAPTION + LYRICS</div>")
prompt_view = gr.Textbox(label=None, show_label=False, lines=14, interactive=True)
close_prompt = gr.Button("Close")
with gr.Column(visible=False, elem_classes=["modal-shell"]) as info_modal:
with gr.Group(elem_classes=["modal-card"]):
gr.HTML("<div class='rack-title'>RUN INFO</div>")
meta_view = gr.Textbox(label=None, show_label=False, lines=8, interactive=False)
close_info = gr.Button("Close")
with gr.Column(visible=False, elem_classes=["modal-shell"]) as expert_modal:
with gr.Group(elem_classes=["modal-card", "expert-modal-card"]):
gr.HTML("<div class='rack-title'>EXPERT</div>")
steps = gr.Slider(4, 60, value=_model_cfg(DEFAULT_MODEL)["steps"], step=1, label="Steps", elem_id="steps")
guidance = gr.Slider(1.0, 15.0, value=_model_cfg(DEFAULT_MODEL)["guidance"], step=0.5, label="Guidance", elem_id="guidance")
shift = gr.Slider(1.0, 5.0, value=3.0, step=0.5, label="Shift", elem_id="shift")
seed = gr.Number(value=-1, precision=0, label="Audio seed", elem_id="seed")
random_seed = gr.Checkbox(value=True, label="Random seed", elem_id="random_seed")
lyric_seed = gr.Number(value=-1, precision=0, label="Lyric seed", elem_id="lyric_seed")
close_expert = gr.Button("Close")
# BPM / pad-driven fields are hidden via .pad-hidden in CONSOLE_CSS
# ── Wiring ───────────────────────────────────────────────────────────────
genre.change(
fn=apply_genre,
inputs=[genre],
outputs=[style, vocal, instruments, bpm, mood_dims, mood, keyscale, timesignature],
)
style.change(
fn=apply_style_defaults,
inputs=[genre, style],
outputs=[bpm, instruments, mood_dims, mood, keyscale, timesignature],
)
use_case.change(
fn=apply_use_case_bpm,
inputs=[use_case],
outputs=[bpm],
)
def _sync_expert(model_name):
cfg = _model_cfg(model_name)
return cfg["steps"], cfg["guidance"], cfg["shift"]
model.change(fn=_sync_expert, inputs=[model], outputs=[steps, guidance, shift])
gen_lyrics_btn.click(
fn=on_generate_lyrics,
inputs=[genre, mood, theme, structure, instrumental, lyric_seed, language, instructions],
outputs=[lyrics],
js="""
(genre, mood, theme, structure, instrumental, seed, language, instructions) => {
const L = (window.readLiveConsole && window.readLiveConsole()) || {};
return [
L.genre != null ? L.genre : genre,
mood,
(L.theme != null && L.theme !== '') ? L.theme : theme,
L.structure != null ? L.structure : structure,
L.instrumental != null ? L.instrumental : instrumental,
seed,
L.language != null ? L.language : language,
(L.instructions != null && L.instructions !== '') ? L.instructions : instructions,
];
}
""",
)
surprise_btn.click(
fn=surprise_me,
inputs=[genre, theme, language, instructions],
outputs=[style, vocal, instruments, bpm, mood_dims, mood, keyscale, timesignature, structure, use_case, lyrics, prompt_view],
js="""
(genre, theme, language, instructions) => {
const L = (window.readLiveConsole && window.readLiveConsole()) || {};
return [
L.genre != null ? L.genre : genre,
(L.theme != null && L.theme !== '') ? L.theme : theme,
L.language != null ? L.language : language,
(L.instructions != null && L.instructions !== '') ? L.instructions : instructions,
];
}
""",
).then(
fn=lambda p: p,
inputs=[prompt_view],
outputs=[prompt_state],
).then(
fn=lambda: None,
js="""
() => {
if (window.releaseConsoleOwned) window.releaseConsoleOwned();
const pull = () => { if (window.syncBpmFromGradio) window.syncBpmFromGradio(); };
setTimeout(pull, 80);
setTimeout(pull, 250);
return [];
}
""",
)
# Visible console UI is source of truth — Gradio hidden fields often lag.
_SYNC_LIVE_JS = """
(model, lyrics, duration, bpm, keyscale, timesignature, language, instrumental, steps, guidance, shift, seed, random_seed, genre, style, mood, instruments, vocal, use_case, audio_format, auto_play, instructions) => {
const L = (window.readLiveConsole && window.readLiveConsole()) || {};
return [
model,
lyrics,
L.duration != null ? L.duration : duration,
L.bpm != null ? L.bpm : bpm,
L.keyscale != null && L.keyscale !== '' ? L.keyscale : keyscale,
L.timesignature != null && L.timesignature !== '' ? L.timesignature : timesignature,
L.language != null ? L.language : language,
L.instrumental != null ? L.instrumental : instrumental,
steps,
guidance,
shift,
seed,
random_seed,
L.genre != null ? L.genre : genre,
L.style != null ? L.style : style,
L.mood != null && L.mood !== '' ? L.mood : mood,
(L.instruments && L.instruments.length) ? L.instruments : instruments,
L.vocal != null ? L.vocal : vocal,
L.use_case != null ? L.use_case : use_case,
L.audio_format != null ? L.audio_format : audio_format,
L.auto_play != null ? L.auto_play : auto_play,
(L.instructions != null && L.instructions !== '') ? L.instructions : instructions,
];
}
"""
generate_btn.click(
fn=generate_music,
inputs=[
model,
lyrics,
duration,
bpm,
keyscale,
timesignature,
language,
instrumental,
steps,
guidance,
shift,
seed,
random_seed,
genre,
style,
mood,
instruments,
vocal,
use_case,
audio_format,
auto_play,
instructions,
],
outputs=[audio_out, meta_view, prompt_view, deck_hint, autodl_btn],
js=_SYNC_LIVE_JS,
).then(
fn=lambda p, m: (p, m),
inputs=[prompt_view, meta_view],
outputs=[prompt_state, meta_state],
).then(
fn=lambda: None,
js="""
() => {
const L = (window.readLiveConsole && window.readLiveConsole()) || {};
const wantDl = !!L.auto_download;
if (wantDl) {
let dlDone = false;
const clickDl = () => {
if (dlDone) return true;
const r = document.getElementById('inspire-autodl');
if (!r) return false;
const el = r.matches('button, a') ? r : r.querySelector('button, a[download], a[href]');
if (!el) return false;
dlDone = true;
el.click();
return true;
};
if (!clickDl()) {
setTimeout(clickDl, 300);
setTimeout(clickDl, 900);
}
}
// Gradio autoplay handles Play automatically (JS play() is blocked after the GPU wait).
// If two <audio> nodes race, keep only the one that just started.
const root = document.getElementById('deck-audio');
if (root && !root.dataset.playDedupe) {
root.dataset.playDedupe = '1';
root.addEventListener('play', (e) => {
const active = e.target;
if (!(active instanceof HTMLMediaElement)) return;
root.querySelectorAll('audio').forEach((el) => {
if (el !== active && !el.paused) {
try { el.pause(); } catch (_) {}
}
});
}, true);
}
return [];
}
""",
)
prompt_btn.click(
fn=peek_prompt,
inputs=[genre, style, mood, instruments, vocal, bpm, use_case, instrumental, lyrics, instructions],
outputs=[prompt_view, prompt_modal],
js="""
(genre, style, mood, instruments, vocal, bpm, use_case, instrumental, lyrics, instructions) => {
const L = (window.readLiveConsole && window.readLiveConsole()) || {};
return [
L.genre != null ? L.genre : genre,
L.style != null ? L.style : style,
L.mood != null && L.mood !== '' ? L.mood : mood,
(L.instruments && L.instruments.length) ? L.instruments : instruments,
L.vocal != null ? L.vocal : vocal,
L.bpm != null ? L.bpm : bpm,
L.use_case != null ? L.use_case : use_case,
L.instrumental != null ? L.instrumental : instrumental,
lyrics,
(L.instructions != null && L.instructions !== '') ? L.instructions : instructions,
];
}
""",
)
info_btn.click(
fn=lambda state, view: (view or state or "No run yet.", gr.update(visible=True)),
inputs=[meta_state, meta_view],
outputs=[meta_view, info_modal],
)
expert_btn.click(fn=lambda: gr.update(visible=True), outputs=[expert_modal])
close_prompt.click(fn=lambda: gr.update(visible=False), outputs=[prompt_modal])
close_info.click(fn=lambda: gr.update(visible=False), outputs=[info_modal])
close_expert.click(fn=lambda: gr.update(visible=False), outputs=[expert_modal])
if __name__ == "__main__":
demo.queue(max_size=10).launch(
server_name="0.0.0.0",
server_port=7860,
ssr_mode=False,
)