Spaces:
Running on Zero
Running on Zero
ace-step-jam: AI music generation studio
Browse filesDescribe any song in plain English β an LLM writes tags and lyrics,
ACE-Step v1.5 generates the audio, and Z-Image-Turbo creates a
thumbnail. Community feed for sharing songs.
Built on ACE-Step/acestep-v15-xl-turbo + Tongyi-MAI/Z-Image-Turbo.
- README.md +9 -12
- app.py +319 -58
- index.html +519 -675
- requirements.txt +1 -1
README.md
CHANGED
|
@@ -1,5 +1,5 @@
|
|
| 1 |
---
|
| 2 |
-
title:
|
| 3 |
emoji: π΅
|
| 4 |
colorFrom: gray
|
| 5 |
colorTo: gray
|
|
@@ -8,24 +8,21 @@ sdk_version: 6.12.0
|
|
| 8 |
app_file: app.py
|
| 9 |
pinned: false
|
| 10 |
license: mit
|
| 11 |
-
short_description:
|
| 12 |
models:
|
| 13 |
- ACE-Step/Ace-Step1.5
|
| 14 |
- ACE-Step/acestep-v15-xl-turbo
|
| 15 |
-
|
| 16 |
-
- ACE-Step/Ace-Step1.5
|
| 17 |
-
- ACE-Step/acestep-v15-xl-turbo
|
| 18 |
---
|
| 19 |
|
| 20 |
-
#
|
| 21 |
|
| 22 |
-
|
| 23 |
|
| 24 |
-
**Model**: `ACE-Step/acestep-v15-xl-turbo` β
|
| 25 |
|
| 26 |
## Usage
|
| 27 |
|
| 28 |
-
1.
|
| 29 |
-
2.
|
| 30 |
-
3.
|
| 31 |
-
4. Use **β¨ Inspire me** to auto-generate lyrics via LLM
|
|
|
|
| 1 |
---
|
| 2 |
+
title: ace-step-jam
|
| 3 |
emoji: π΅
|
| 4 |
colorFrom: gray
|
| 5 |
colorTo: gray
|
|
|
|
| 8 |
app_file: app.py
|
| 9 |
pinned: false
|
| 10 |
license: mit
|
| 11 |
+
short_description: Describe any song β AI writes & produces it
|
| 12 |
models:
|
| 13 |
- ACE-Step/Ace-Step1.5
|
| 14 |
- ACE-Step/acestep-v15-xl-turbo
|
| 15 |
+
- Tongyi-MAI/Z-Image-Turbo
|
|
|
|
|
|
|
| 16 |
---
|
| 17 |
|
| 18 |
+
# ace-step-jam
|
| 19 |
|
| 20 |
+
Describe any song in plain English β an LLM writes the tags and lyrics, then [ACE-Step v1.5](https://github.com/ace-step/ACE-Step) generates the audio.
|
| 21 |
|
| 22 |
+
**Model**: [`ACE-Step/acestep-v15-xl-turbo`](https://huggingface.co/ACE-Step/acestep-v15-xl-turbo) β 8-step turbo distillation, ~2 seconds per minute of audio on GPU.
|
| 23 |
|
| 24 |
## Usage
|
| 25 |
|
| 26 |
+
1. Describe the song you want (genre, mood, story β anything)
|
| 27 |
+
2. Hit **Generate** β the AI composes lyrics, picks tags, and produces audio
|
| 28 |
+
3. Click the waveform to seek, download the WAV, or share to the community feed
|
|
|
app.py
CHANGED
|
@@ -1,13 +1,16 @@
|
|
| 1 |
import os
|
| 2 |
import sys
|
|
|
|
|
|
|
| 3 |
import base64
|
|
|
|
| 4 |
import tempfile
|
| 5 |
import traceback
|
|
|
|
| 6 |
import numpy as np
|
| 7 |
import soundfile as sf
|
| 8 |
|
| 9 |
# ββ CRITICAL: import spaces BEFORE torch and acestep βββββββββββββββββββββββββ
|
| 10 |
-
# spaces monkey-patches CUDA init hooks so ZeroGPU can intercept GPU allocation.
|
| 11 |
try:
|
| 12 |
import spaces
|
| 13 |
HAS_SPACES = True
|
|
@@ -20,43 +23,69 @@ for _v in ["http_proxy", "https_proxy", "HTTP_PROXY", "HTTPS_PROXY", "ALL_PROXY"
|
|
| 20 |
os.environ["GRADIO_ANALYTICS_ENABLED"] = "False"
|
| 21 |
|
| 22 |
# Fix PermissionError on ZeroGPU: /home/user/.cache is not writable.
|
| 23 |
-
# Redirect HF module cache + matplotlib config to /tmp.
|
| 24 |
os.environ.setdefault("HF_MODULES_CACHE", "/tmp/hf_modules")
|
| 25 |
os.environ.setdefault("MPLCONFIGDIR", "/tmp/matplotlib")
|
| 26 |
|
| 27 |
-
# Add bundled nano-vllm to path
|
| 28 |
_current_dir = os.path.dirname(os.path.abspath(__file__))
|
| 29 |
_nano_vllm = os.path.join(_current_dir, "acestep", "third_parts", "nano-vllm")
|
| 30 |
if os.path.exists(_nano_vllm):
|
| 31 |
sys.path.insert(0, _nano_vllm)
|
| 32 |
|
|
|
|
|
|
|
| 33 |
import torch
|
|
|
|
| 34 |
from acestep.handler import AceStepHandler
|
| 35 |
from gradio import Server
|
| 36 |
from fastapi.responses import HTMLResponse
|
| 37 |
from openai import OpenAI
|
| 38 |
|
| 39 |
# ββ Model Loading βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 40 |
-
# AceStepHandler downloads both ACE-Step/Ace-Step1.5 (VAE + text encoder)
|
| 41 |
-
# and ACE-Step/acestep-v15-xl-turbo (transformer) into persistent_storage_path.
|
| 42 |
-
# preload_from_hub in README pre-downloads them at build time so startup is fast.
|
| 43 |
|
| 44 |
def _get_storage_path():
|
| 45 |
-
|
| 46 |
-
|
| 47 |
-
t = "/data/.write_test"
|
| 48 |
-
with open(t, "w") as f:
|
| 49 |
-
f.write("x")
|
| 50 |
-
os.remove(t)
|
| 51 |
-
return "/data"
|
| 52 |
-
except OSError:
|
| 53 |
-
pass
|
| 54 |
-
p = os.path.join(_current_dir, "data")
|
| 55 |
os.makedirs(p, exist_ok=True)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 56 |
return p
|
| 57 |
|
| 58 |
_storage = _get_storage_path()
|
| 59 |
-
print(f"[startup]
|
|
|
|
| 60 |
|
| 61 |
handler = AceStepHandler(persistent_storage_path=_storage)
|
| 62 |
_status, _ready = handler.initialize_service(
|
|
@@ -70,8 +99,149 @@ _status, _ready = handler.initialize_service(
|
|
| 70 |
)
|
| 71 |
print(f"[startup] Handler: ready={_ready} β {_status}")
|
| 72 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 73 |
# ββ GPU Inference Function ββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 74 |
-
# RULE: @spaces.GPU and @app.api MUST be on SEPARATE functions. Never stack.
|
| 75 |
|
| 76 |
if HAS_SPACES:
|
| 77 |
@spaces.GPU(duration=120)
|
|
@@ -103,17 +273,15 @@ def _run_inference(prompt, lyrics, audio_duration, infer_steps, seed) -> str:
|
|
| 103 |
raise RuntimeError(result.get("error", "generation failed"))
|
| 104 |
|
| 105 |
audio_dict = result["audios"][0]
|
| 106 |
-
tensor = audio_dict["tensor"]
|
| 107 |
sr = audio_dict["sample_rate"]
|
| 108 |
|
| 109 |
-
# Convert to numpy [samples, channels] or [samples] for soundfile
|
| 110 |
data = tensor.cpu().float().numpy()
|
| 111 |
if data.ndim == 2:
|
| 112 |
-
data = data.T
|
| 113 |
if data.shape[1] == 1:
|
| 114 |
-
data = data[:, 0]
|
| 115 |
|
| 116 |
-
# Peak-normalize as safety net (v1.5 DCAE should already be scaled correctly)
|
| 117 |
peak = np.abs(data).max()
|
| 118 |
if peak > 1e-4:
|
| 119 |
data = (data / peak * 0.95).astype(np.float32)
|
|
@@ -124,19 +292,109 @@ def _run_inference(prompt, lyrics, audio_duration, infer_steps, seed) -> str:
|
|
| 124 |
|
| 125 |
|
| 126 |
# ββ gr.Server App βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 127 |
-
app = Server(title="
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 128 |
|
|
|
|
|
|
|
|
|
|
|
|
|
| 129 |
|
| 130 |
-
#
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 131 |
@app.api(name="generate", concurrency_limit=1, time_limit=180)
|
| 132 |
def generate(
|
| 133 |
prompt: str,
|
| 134 |
lyrics: str,
|
| 135 |
audio_duration: float = 60.0,
|
| 136 |
infer_step: int = 8,
|
|
|
|
| 137 |
seed: int = -1,
|
|
|
|
|
|
|
| 138 |
) -> str:
|
| 139 |
-
"""
|
| 140 |
try:
|
| 141 |
wav_path = _generate_gpu(prompt, lyrics, audio_duration, infer_step, seed)
|
| 142 |
with open(wav_path, "rb") as f:
|
|
@@ -148,38 +406,42 @@ def generate(
|
|
| 148 |
raise
|
| 149 |
|
| 150 |
|
| 151 |
-
# ββ API:
|
| 152 |
-
|
| 153 |
-
|
| 154 |
-
|
| 155 |
-
token = os.environ.get("HF_TOKEN", "")
|
| 156 |
-
if not token:
|
| 157 |
-
return "[Error: HF_TOKEN not configured in Space secrets]"
|
| 158 |
|
| 159 |
-
|
| 160 |
-
|
| 161 |
-
|
| 162 |
-
|
| 163 |
-
|
| 164 |
-
|
| 165 |
-
|
| 166 |
-
|
| 167 |
-
|
| 168 |
-
|
| 169 |
-
|
| 170 |
-
|
| 171 |
-
"
|
| 172 |
-
)
|
| 173 |
-
|
| 174 |
-
|
| 175 |
-
|
| 176 |
-
|
| 177 |
-
|
| 178 |
-
|
| 179 |
-
|
| 180 |
-
|
| 181 |
-
|
| 182 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 183 |
|
| 184 |
|
| 185 |
# ββ Serve custom HTML frontend ββββββββββββββββββββββββββββββββββββββββββββββββ
|
|
@@ -189,7 +451,6 @@ async def homepage():
|
|
| 189 |
return f.read()
|
| 190 |
|
| 191 |
|
| 192 |
-
# Required by HF Spaces runtime
|
| 193 |
demo = app
|
| 194 |
|
| 195 |
if __name__ == "__main__":
|
|
|
|
| 1 |
import os
|
| 2 |
import sys
|
| 3 |
+
import re
|
| 4 |
+
import json
|
| 5 |
import base64
|
| 6 |
+
import uuid
|
| 7 |
import tempfile
|
| 8 |
import traceback
|
| 9 |
+
from datetime import datetime, timezone
|
| 10 |
import numpy as np
|
| 11 |
import soundfile as sf
|
| 12 |
|
| 13 |
# ββ CRITICAL: import spaces BEFORE torch and acestep βββββββββββββββββββββββββ
|
|
|
|
| 14 |
try:
|
| 15 |
import spaces
|
| 16 |
HAS_SPACES = True
|
|
|
|
| 23 |
os.environ["GRADIO_ANALYTICS_ENABLED"] = "False"
|
| 24 |
|
| 25 |
# Fix PermissionError on ZeroGPU: /home/user/.cache is not writable.
|
|
|
|
| 26 |
os.environ.setdefault("HF_MODULES_CACHE", "/tmp/hf_modules")
|
| 27 |
os.environ.setdefault("MPLCONFIGDIR", "/tmp/matplotlib")
|
| 28 |
|
| 29 |
+
# Add bundled nano-vllm to path
|
| 30 |
_current_dir = os.path.dirname(os.path.abspath(__file__))
|
| 31 |
_nano_vllm = os.path.join(_current_dir, "acestep", "third_parts", "nano-vllm")
|
| 32 |
if os.path.exists(_nano_vllm):
|
| 33 |
sys.path.insert(0, _nano_vllm)
|
| 34 |
|
| 35 |
+
import io
|
| 36 |
+
import random
|
| 37 |
import torch
|
| 38 |
+
from PIL import Image
|
| 39 |
from acestep.handler import AceStepHandler
|
| 40 |
from gradio import Server
|
| 41 |
from fastapi.responses import HTMLResponse
|
| 42 |
from openai import OpenAI
|
| 43 |
|
| 44 |
# ββ Model Loading βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
|
|
|
|
|
|
|
|
|
| 45 |
|
| 46 |
def _get_storage_path():
|
| 47 |
+
"""Model checkpoints β try to reuse preload_from_hub cache via symlinks."""
|
| 48 |
+
p = os.path.join(_current_dir, "model_cache")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 49 |
os.makedirs(p, exist_ok=True)
|
| 50 |
+
checkpoint_dir = os.path.join(p, "checkpoints")
|
| 51 |
+
os.makedirs(checkpoint_dir, exist_ok=True)
|
| 52 |
+
|
| 53 |
+
# preload_from_hub downloads to HF cache during Docker build.
|
| 54 |
+
# Create symlinks so the handler finds models at the expected paths
|
| 55 |
+
# without re-downloading 20GB on each restart.
|
| 56 |
+
from huggingface_hub import try_to_load_from_cache, scan_cache_dir
|
| 57 |
+
for model_name, repo_id in [
|
| 58 |
+
("acestep-v15-xl-turbo", "ACE-Step/acestep-v15-xl-turbo"),
|
| 59 |
+
]:
|
| 60 |
+
target = os.path.join(checkpoint_dir, model_name)
|
| 61 |
+
if not os.path.exists(target):
|
| 62 |
+
try:
|
| 63 |
+
from huggingface_hub import snapshot_download
|
| 64 |
+
cached = snapshot_download(repo_id, local_files_only=True)
|
| 65 |
+
os.symlink(cached, target)
|
| 66 |
+
print(f"[startup] Linked {model_name} β {cached}")
|
| 67 |
+
except Exception as e:
|
| 68 |
+
print(f"[startup] Cache miss for {model_name}, will download: {e}")
|
| 69 |
+
|
| 70 |
+
# For the unified repo (ACE-Step/Ace-Step1.5), its subdirs (vae, Qwen3-Embedding-0.6B, etc.)
|
| 71 |
+
# need to appear directly in checkpoint_dir
|
| 72 |
+
try:
|
| 73 |
+
from huggingface_hub import snapshot_download
|
| 74 |
+
cached = snapshot_download("ACE-Step/Ace-Step1.5", local_files_only=True)
|
| 75 |
+
for sub in os.listdir(cached):
|
| 76 |
+
src = os.path.join(cached, sub)
|
| 77 |
+
dst = os.path.join(checkpoint_dir, sub)
|
| 78 |
+
if os.path.isdir(src) and not os.path.exists(dst):
|
| 79 |
+
os.symlink(src, dst)
|
| 80 |
+
print(f"[startup] Linked {sub} β {src}")
|
| 81 |
+
except Exception as e:
|
| 82 |
+
print(f"[startup] Cache miss for Ace-Step1.5, will download: {e}")
|
| 83 |
+
|
| 84 |
return p
|
| 85 |
|
| 86 |
_storage = _get_storage_path()
|
| 87 |
+
print(f"[startup] Model storage: {_storage}")
|
| 88 |
+
print(f"[startup] Community bucket: /data (mounted)")
|
| 89 |
|
| 90 |
handler = AceStepHandler(persistent_storage_path=_storage)
|
| 91 |
_status, _ready = handler.initialize_service(
|
|
|
|
| 99 |
)
|
| 100 |
print(f"[startup] Handler: ready={_ready} β {_status}")
|
| 101 |
|
| 102 |
+
# ββ Z-Image-Turbo (thumbnail generation) βββββββββββββββββββββββββββββββββββββ
|
| 103 |
+
try:
|
| 104 |
+
from diffusers import ZImagePipeline, FlowMatchEulerDiscreteScheduler
|
| 105 |
+
_zimage_pipe = ZImagePipeline.from_pretrained(
|
| 106 |
+
"Tongyi-MAI/Z-Image-Turbo",
|
| 107 |
+
torch_dtype=torch.bfloat16,
|
| 108 |
+
)
|
| 109 |
+
_zimage_pipe.to("cuda")
|
| 110 |
+
print("[startup] Z-Image-Turbo loaded for thumbnails")
|
| 111 |
+
except Exception as e:
|
| 112 |
+
_zimage_pipe = None
|
| 113 |
+
print(f"[startup] Z-Image-Turbo not available: {e}")
|
| 114 |
+
|
| 115 |
+
# ββ LLM Compose ββββββββββββββββββββββοΏ½οΏ½οΏ½βββββββββββββββββββββββββββββββββββββββ
|
| 116 |
+
|
| 117 |
+
COMPOSE_SYSTEM = """You are a Grammy-winning songwriter and music producer. The user will describe a song idea in plain English. Your job is to flesh it out into a complete song specification.
|
| 118 |
+
|
| 119 |
+
Return EXACTLY this format β no extra text:
|
| 120 |
+
|
| 121 |
+
---
|
| 122 |
+
title: <short catchy song title>
|
| 123 |
+
tags: <genre and style tags, comma-separated, 3-6 tags>
|
| 124 |
+
bpm: <tempo as integer>
|
| 125 |
+
language: <vocal language: en, zh, ja, ko, or "unknown" for instrumental>
|
| 126 |
+
---
|
| 127 |
+
|
| 128 |
+
<song lyrics with [Verse], [Chorus], [Bridge] markers>
|
| 129 |
+
<use [Instrumental] alone if the song has no vocals>"""
|
| 130 |
+
|
| 131 |
+
BUCKET_ID = "victor/ace-step-community"
|
| 132 |
+
BUCKET_URL = f"https://huggingface.co/buckets/{BUCKET_ID}/resolve"
|
| 133 |
+
|
| 134 |
+
|
| 135 |
+
def _compose(description: str) -> dict:
|
| 136 |
+
"""Call HF Inference Router LLM to generate tags + lyrics from a description."""
|
| 137 |
+
key = os.environ.get("HF_TOKEN", "")
|
| 138 |
+
if not key:
|
| 139 |
+
raise RuntimeError("HF_TOKEN not configured")
|
| 140 |
+
|
| 141 |
+
client = OpenAI(base_url="https://router.huggingface.co/v1", api_key=key)
|
| 142 |
+
resp = client.chat.completions.create(
|
| 143 |
+
model="openai/gpt-oss-120b:groq",
|
| 144 |
+
messages=[
|
| 145 |
+
{"role": "system", "content": COMPOSE_SYSTEM},
|
| 146 |
+
{"role": "user", "content": description},
|
| 147 |
+
],
|
| 148 |
+
max_tokens=2000,
|
| 149 |
+
temperature=0.9,
|
| 150 |
+
)
|
| 151 |
+
raw = resp.choices[0].message.content or ""
|
| 152 |
+
content = re.sub(r"<think>.*?</think>", "", raw, flags=re.DOTALL).strip()
|
| 153 |
+
|
| 154 |
+
# Parse frontmatter
|
| 155 |
+
title, tags, bpm, language = "Untitled", "", 120, "en"
|
| 156 |
+
lyrics = content
|
| 157 |
+
m = re.search(r"---\s*\n(.*?)\n---\s*\n(.*)", content, re.DOTALL)
|
| 158 |
+
if m:
|
| 159 |
+
header, lyrics = m.group(1), m.group(2).strip()
|
| 160 |
+
for line in header.strip().split("\n"):
|
| 161 |
+
if line.startswith("title:"):
|
| 162 |
+
title = line[6:].strip().strip('"\'')
|
| 163 |
+
elif line.startswith("tags:"):
|
| 164 |
+
tags = line[5:].strip()
|
| 165 |
+
elif line.startswith("bpm:"):
|
| 166 |
+
try:
|
| 167 |
+
bpm = int(line[4:].strip())
|
| 168 |
+
except ValueError:
|
| 169 |
+
pass
|
| 170 |
+
elif line.startswith("language:"):
|
| 171 |
+
language = line[9:].strip()
|
| 172 |
+
|
| 173 |
+
return {"title": title, "tags": tags, "lyrics": lyrics, "bpm": bpm, "language": language}
|
| 174 |
+
|
| 175 |
+
|
| 176 |
+
# ββ Thumbnail Generation βββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 177 |
+
|
| 178 |
+
def _get_song_word(title: str, tags: str, lyrics: str, description: str) -> str:
|
| 179 |
+
"""Ask LLM for a single evocative word to represent the song visually."""
|
| 180 |
+
# Fallback: first 2 words of description or title
|
| 181 |
+
fallback = " ".join((description or title or "music").split()[:2])
|
| 182 |
+
key = os.environ.get("HF_TOKEN", "")
|
| 183 |
+
if not key:
|
| 184 |
+
print(f"[thumbnail] no HF_TOKEN, using fallback: {fallback}")
|
| 185 |
+
return fallback
|
| 186 |
+
try:
|
| 187 |
+
client = OpenAI(base_url="https://router.huggingface.co/v1", api_key=key)
|
| 188 |
+
resp = client.chat.completions.create(
|
| 189 |
+
model="openai/gpt-oss-120b:groq",
|
| 190 |
+
messages=[
|
| 191 |
+
{"role": "system", "content": "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."},
|
| 192 |
+
{"role": "user", "content": f"Title: {title}\nTags: {tags}\nLyrics: {lyrics[:300]}"},
|
| 193 |
+
],
|
| 194 |
+
max_tokens=500,
|
| 195 |
+
temperature=0.7,
|
| 196 |
+
)
|
| 197 |
+
raw = resp.choices[0].message.content or ""
|
| 198 |
+
cleaned = re.sub(r"<think>.*?</think>", "", raw, flags=re.DOTALL).strip()
|
| 199 |
+
word = cleaned.split()[0].strip('."\'!,') if cleaned.split() else ""
|
| 200 |
+
if not word:
|
| 201 |
+
print(f"[thumbnail] LLM returned empty, using fallback: {fallback}")
|
| 202 |
+
return fallback
|
| 203 |
+
print(f"[thumbnail] word: {word}")
|
| 204 |
+
return word
|
| 205 |
+
except Exception as e:
|
| 206 |
+
print(f"[thumbnail] word extraction failed: {e}, using fallback: {fallback}")
|
| 207 |
+
return fallback
|
| 208 |
+
|
| 209 |
+
|
| 210 |
+
def _generate_thumbnail_impl(word: str) -> bytes | None:
|
| 211 |
+
"""Generate a thumbnail using Z-Image-Turbo. Returns PNG bytes or None."""
|
| 212 |
+
if _zimage_pipe is None:
|
| 213 |
+
return None
|
| 214 |
+
try:
|
| 215 |
+
prompt = f"{word} studio photography close-up black background"
|
| 216 |
+
print(f"[thumbnail] generating: {prompt}")
|
| 217 |
+
scheduler = FlowMatchEulerDiscreteScheduler(num_train_timesteps=1000, shift=3.0)
|
| 218 |
+
_zimage_pipe.scheduler = scheduler
|
| 219 |
+
image = _zimage_pipe(
|
| 220 |
+
prompt=prompt,
|
| 221 |
+
height=1024, width=1024,
|
| 222 |
+
guidance_scale=0.0,
|
| 223 |
+
num_inference_steps=9,
|
| 224 |
+
generator=torch.Generator("cuda").manual_seed(random.randint(1, 1000000)),
|
| 225 |
+
max_sequence_length=512,
|
| 226 |
+
).images[0]
|
| 227 |
+
buf = io.BytesIO()
|
| 228 |
+
image.save(buf, format="PNG", optimize=True)
|
| 229 |
+
print(f"[thumbnail] done ({len(buf.getvalue()) // 1024}KB)")
|
| 230 |
+
return buf.getvalue()
|
| 231 |
+
except Exception as e:
|
| 232 |
+
print(f"[thumbnail] generation failed: {e}")
|
| 233 |
+
return None
|
| 234 |
+
|
| 235 |
+
if HAS_SPACES:
|
| 236 |
+
@spaces.GPU(duration=30)
|
| 237 |
+
def _generate_thumbnail(word: str) -> bytes | None:
|
| 238 |
+
return _generate_thumbnail_impl(word)
|
| 239 |
+
else:
|
| 240 |
+
def _generate_thumbnail(word: str) -> bytes | None:
|
| 241 |
+
return _generate_thumbnail_impl(word)
|
| 242 |
+
|
| 243 |
+
|
| 244 |
# ββ GPU Inference Function ββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
|
|
|
| 245 |
|
| 246 |
if HAS_SPACES:
|
| 247 |
@spaces.GPU(duration=120)
|
|
|
|
| 273 |
raise RuntimeError(result.get("error", "generation failed"))
|
| 274 |
|
| 275 |
audio_dict = result["audios"][0]
|
| 276 |
+
tensor = audio_dict["tensor"]
|
| 277 |
sr = audio_dict["sample_rate"]
|
| 278 |
|
|
|
|
| 279 |
data = tensor.cpu().float().numpy()
|
| 280 |
if data.ndim == 2:
|
| 281 |
+
data = data.T
|
| 282 |
if data.shape[1] == 1:
|
| 283 |
+
data = data[:, 0]
|
| 284 |
|
|
|
|
| 285 |
peak = np.abs(data).max()
|
| 286 |
if peak > 1e-4:
|
| 287 |
data = (data / peak * 0.95).astype(np.float32)
|
|
|
|
| 292 |
|
| 293 |
|
| 294 |
# ββ gr.Server App βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 295 |
+
app = Server(title="ace-step-jam")
|
| 296 |
+
|
| 297 |
+
|
| 298 |
+
# ββ API: One-box create (compose + generate) βββββββββββββββββββββββββββββββββ
|
| 299 |
+
@app.api(name="create", concurrency_limit=1, time_limit=300)
|
| 300 |
+
def create(
|
| 301 |
+
description: str,
|
| 302 |
+
audio_duration: float = 60.0,
|
| 303 |
+
seed: int = -1,
|
| 304 |
+
community: bool = False,
|
| 305 |
+
) -> str:
|
| 306 |
+
"""One-box: describe a song β LLM composes tags+lyrics β generates audio.
|
| 307 |
+
Returns JSON: {audio, title, tags, lyrics, community_url?}"""
|
| 308 |
+
try:
|
| 309 |
+
# Step 1: LLM compose (no GPU)
|
| 310 |
+
composed = _compose(description)
|
| 311 |
+
title = composed["title"]
|
| 312 |
+
tags = composed["tags"]
|
| 313 |
+
lyrics = composed["lyrics"]
|
| 314 |
+
print(f"[create] title={title} tags={tags[:60]}...")
|
| 315 |
+
|
| 316 |
+
# Step 2: GPU generate music
|
| 317 |
+
wav_path = _generate_gpu(tags, lyrics, audio_duration, 8, seed)
|
| 318 |
+
with open(wav_path, "rb") as f:
|
| 319 |
+
wav_bytes = f.read()
|
| 320 |
+
audio_b64 = f"data:audio/wav;base64,{base64.b64encode(wav_bytes).decode()}"
|
| 321 |
+
|
| 322 |
+
# Step 3: Generate thumbnail (separate GPU session via Z-Image-Turbo)
|
| 323 |
+
thumb_bytes = None
|
| 324 |
+
try:
|
| 325 |
+
word = _get_song_word(title, tags, lyrics, description)
|
| 326 |
+
thumb_bytes = _generate_thumbnail(word)
|
| 327 |
+
except Exception as e:
|
| 328 |
+
print(f"[create] thumbnail failed: {e}")
|
| 329 |
+
|
| 330 |
+
result = {
|
| 331 |
+
"audio": audio_b64,
|
| 332 |
+
"title": title,
|
| 333 |
+
"tags": tags,
|
| 334 |
+
"lyrics": lyrics,
|
| 335 |
+
}
|
| 336 |
+
if thumb_bytes:
|
| 337 |
+
result["thumbnail"] = f"data:image/png;base64,{base64.b64encode(thumb_bytes).decode()}"
|
| 338 |
+
|
| 339 |
+
# Step 3: Community upload (if checked and /data is writable)
|
| 340 |
+
if community:
|
| 341 |
+
try:
|
| 342 |
+
song_id = uuid.uuid4().hex[:12]
|
| 343 |
+
song_dir = f"/data/songs/{song_id}"
|
| 344 |
+
os.makedirs(song_dir, exist_ok=True)
|
| 345 |
|
| 346 |
+
# Save WAV
|
| 347 |
+
wav_name = f"{song_id}.wav"
|
| 348 |
+
with open(f"{song_dir}/{wav_name}", "wb") as f:
|
| 349 |
+
f.write(wav_bytes)
|
| 350 |
|
| 351 |
+
# Save thumbnail
|
| 352 |
+
has_thumb = False
|
| 353 |
+
if thumb_bytes:
|
| 354 |
+
with open(f"{song_dir}/thumb.png", "wb") as f:
|
| 355 |
+
f.write(thumb_bytes)
|
| 356 |
+
has_thumb = True
|
| 357 |
+
|
| 358 |
+
# Save metadata
|
| 359 |
+
meta = {
|
| 360 |
+
"id": song_id,
|
| 361 |
+
"title": title,
|
| 362 |
+
"description": description,
|
| 363 |
+
"tags": tags,
|
| 364 |
+
"lyrics": lyrics,
|
| 365 |
+
"duration": audio_duration,
|
| 366 |
+
"has_thumb": has_thumb,
|
| 367 |
+
"created_at": datetime.now(timezone.utc).isoformat(),
|
| 368 |
+
}
|
| 369 |
+
with open(f"{song_dir}/meta.json", "w") as f:
|
| 370 |
+
json.dump(meta, f, indent=2)
|
| 371 |
+
|
| 372 |
+
result["community_url"] = f"{BUCKET_URL}/songs/{song_id}/{wav_name}"
|
| 373 |
+
print(f"[create] Shared to community: {result['community_url']}")
|
| 374 |
+
_invalidate_feed()
|
| 375 |
+
except Exception as upload_err:
|
| 376 |
+
print(f"[create] Community upload failed: {upload_err}")
|
| 377 |
+
|
| 378 |
+
return json.dumps(result)
|
| 379 |
+
except Exception as e:
|
| 380 |
+
print(f"[create ERROR] {type(e).__name__}: {e}")
|
| 381 |
+
print(traceback.format_exc())
|
| 382 |
+
raise
|
| 383 |
+
|
| 384 |
+
|
| 385 |
+
# ββ API: Direct generate (for advanced/custom mode) ββββββββββββββββββββββββββ
|
| 386 |
@app.api(name="generate", concurrency_limit=1, time_limit=180)
|
| 387 |
def generate(
|
| 388 |
prompt: str,
|
| 389 |
lyrics: str,
|
| 390 |
audio_duration: float = 60.0,
|
| 391 |
infer_step: int = 8,
|
| 392 |
+
guidance_scale: float = 7.0,
|
| 393 |
seed: int = -1,
|
| 394 |
+
lora_name_or_path: str = "",
|
| 395 |
+
lora_weight: float = 0.8,
|
| 396 |
) -> str:
|
| 397 |
+
"""Direct generate from explicit tags + lyrics. Returns base64 WAV data URL."""
|
| 398 |
try:
|
| 399 |
wav_path = _generate_gpu(prompt, lyrics, audio_duration, infer_step, seed)
|
| 400 |
with open(wav_path, "rb") as f:
|
|
|
|
| 406 |
raise
|
| 407 |
|
| 408 |
|
| 409 |
+
# ββ API: Community feed (cached) ββββββββββββββββββββββββββββββββββββββββββββββ
|
| 410 |
+
import time as _time
|
| 411 |
+
_feed_cache = {"data": "[]", "ts": 0}
|
| 412 |
+
_FEED_TTL = 10 # seconds
|
|
|
|
|
|
|
|
|
|
| 413 |
|
| 414 |
+
def _rebuild_feed() -> str:
|
| 415 |
+
songs = []
|
| 416 |
+
songs_dir = "/data/songs"
|
| 417 |
+
if os.path.isdir(songs_dir):
|
| 418 |
+
for song_id in os.listdir(songs_dir):
|
| 419 |
+
meta_path = os.path.join(songs_dir, song_id, "meta.json")
|
| 420 |
+
if os.path.isfile(meta_path):
|
| 421 |
+
try:
|
| 422 |
+
with open(meta_path) as f:
|
| 423 |
+
meta = json.load(f)
|
| 424 |
+
meta["audio_url"] = f"{BUCKET_URL}/songs/{song_id}/{song_id}.wav"
|
| 425 |
+
if meta.get("has_thumb"):
|
| 426 |
+
meta["thumb_url"] = f"{BUCKET_URL}/songs/{song_id}/thumb.png"
|
| 427 |
+
songs.append(meta)
|
| 428 |
+
except Exception:
|
| 429 |
+
pass
|
| 430 |
+
songs.sort(key=lambda s: s.get("created_at", ""), reverse=True)
|
| 431 |
+
return json.dumps(songs[:32])
|
| 432 |
+
|
| 433 |
+
def _invalidate_feed():
|
| 434 |
+
"""Call after uploading a new song to bust the cache."""
|
| 435 |
+
_feed_cache["ts"] = 0
|
| 436 |
+
|
| 437 |
+
@app.api(name="community", concurrency_limit=4)
|
| 438 |
+
def community() -> str:
|
| 439 |
+
"""List community songs from the bucket. Cached for 10s."""
|
| 440 |
+
now = _time.monotonic()
|
| 441 |
+
if now - _feed_cache["ts"] > _FEED_TTL:
|
| 442 |
+
_feed_cache["data"] = _rebuild_feed()
|
| 443 |
+
_feed_cache["ts"] = now
|
| 444 |
+
return _feed_cache["data"]
|
| 445 |
|
| 446 |
|
| 447 |
# ββ Serve custom HTML frontend ββββββββββββββββββββββββββββββββββββββββββββββββ
|
|
|
|
| 451 |
return f.read()
|
| 452 |
|
| 453 |
|
|
|
|
| 454 |
demo = app
|
| 455 |
|
| 456 |
if __name__ == "__main__":
|
index.html
CHANGED
|
@@ -3,813 +3,657 @@
|
|
| 3 |
<head>
|
| 4 |
<meta charset="UTF-8" />
|
| 5 |
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
| 6 |
-
<title>
|
| 7 |
<link rel="preconnect" href="https://fonts.googleapis.com" />
|
| 8 |
-
<link href="https://fonts.googleapis.com/css2?family=
|
| 9 |
<style>
|
| 10 |
-
/* ββ Reset & Variables ββββββββββββββββββββββββββββββββββββββββββββββββββ */
|
| 11 |
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
|
| 12 |
|
| 13 |
:root {
|
| 14 |
-
--bg:
|
| 15 |
-
--surface:
|
| 16 |
-
--surface-2:
|
| 17 |
-
--surface-3:
|
| 18 |
-
--border:
|
| 19 |
-
--accent:
|
| 20 |
-
--accent-dim:
|
| 21 |
-
--accent-glow: rgba(
|
| 22 |
-
--
|
| 23 |
-
--
|
| 24 |
-
--text:
|
| 25 |
-
--text-
|
| 26 |
-
--
|
| 27 |
-
--
|
| 28 |
-
--
|
| 29 |
-
--radius:
|
| 30 |
-
--
|
| 31 |
-
--font: 'Inter', system-ui, sans-serif;
|
| 32 |
}
|
| 33 |
|
| 34 |
html, body {
|
| 35 |
-
height: 100%;
|
| 36 |
-
|
| 37 |
-
color: var(--text);
|
| 38 |
-
font-family: var(--font);
|
| 39 |
-
font-size: 14px;
|
| 40 |
-
line-height: 1.5;
|
| 41 |
-webkit-font-smoothing: antialiased;
|
| 42 |
}
|
| 43 |
|
| 44 |
-
|
| 45 |
-
body {
|
| 46 |
-
display: flex;
|
| 47 |
-
flex-direction: column;
|
| 48 |
-
min-height: 100vh;
|
| 49 |
-
}
|
| 50 |
|
| 51 |
-
|
| 52 |
-
|
| 53 |
-
|
| 54 |
-
|
| 55 |
-
padding: 0 20px;
|
| 56 |
-
}
|
| 57 |
|
| 58 |
/* ββ Header βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ */
|
| 59 |
-
header {
|
| 60 |
-
|
| 61 |
-
|
| 62 |
-
|
| 63 |
-
|
| 64 |
-
.
|
| 65 |
-
|
| 66 |
-
|
| 67 |
-
|
| 68 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 69 |
}
|
| 70 |
-
|
| 71 |
-
.
|
| 72 |
-
|
| 73 |
-
height:
|
| 74 |
-
background: var(--accent);
|
| 75 |
-
border-radius: 8px;
|
| 76 |
-
display: flex;
|
| 77 |
-
align-items: center;
|
| 78 |
-
justify-content: center;
|
| 79 |
-
font-size: 15px;
|
| 80 |
-
box-shadow: 0 0 18px var(--accent-glow);
|
| 81 |
}
|
| 82 |
-
|
| 83 |
-
.
|
| 84 |
-
|
| 85 |
-
|
| 86 |
-
|
| 87 |
-
color: var(--text);
|
| 88 |
}
|
| 89 |
-
|
| 90 |
-
|
| 91 |
-
|
| 92 |
}
|
| 93 |
-
|
| 94 |
-
|
| 95 |
-
|
| 96 |
-
|
| 97 |
-
|
|
|
|
| 98 |
}
|
| 99 |
-
|
| 100 |
-
|
| 101 |
-
|
| 102 |
-
background: var(--surface);
|
| 103 |
-
border: 1px solid var(--border);
|
| 104 |
-
border-radius: var(--radius-lg);
|
| 105 |
-
padding: 18px;
|
| 106 |
-
margin-bottom: 12px;
|
| 107 |
-
}
|
| 108 |
-
|
| 109 |
-
.card-header {
|
| 110 |
-
display: flex;
|
| 111 |
-
align-items: center;
|
| 112 |
-
justify-content: space-between;
|
| 113 |
-
margin-bottom: 10px;
|
| 114 |
-
}
|
| 115 |
-
|
| 116 |
-
.card-label {
|
| 117 |
-
font-size: 11px;
|
| 118 |
-
font-weight: 500;
|
| 119 |
-
letter-spacing: 0.08em;
|
| 120 |
-
text-transform: uppercase;
|
| 121 |
-
color: var(--text-muted);
|
| 122 |
}
|
| 123 |
|
| 124 |
/* ββ Inputs βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ */
|
| 125 |
-
textarea, input[type="text"], input[type="number"] {
|
| 126 |
-
width: 100%;
|
| 127 |
-
|
| 128 |
-
|
| 129 |
-
|
| 130 |
-
|
| 131 |
-
|
| 132 |
-
|
| 133 |
-
|
| 134 |
-
|
| 135 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 136 |
transition: border-color 0.15s;
|
| 137 |
}
|
|
|
|
|
|
|
|
|
|
|
|
|
| 138 |
|
| 139 |
-
|
| 140 |
-
|
| 141 |
-
}
|
| 142 |
-
|
| 143 |
-
textarea::placeholder, input::placeholder {
|
| 144 |
-
color: var(--text-faint);
|
| 145 |
-
}
|
| 146 |
-
|
| 147 |
-
#prompt-input {
|
| 148 |
-
height: 60px;
|
| 149 |
-
font-size: 14px;
|
| 150 |
-
}
|
| 151 |
-
|
| 152 |
-
#lyrics-input {
|
| 153 |
-
height: 180px;
|
| 154 |
-
font-family: 'Courier New', monospace;
|
| 155 |
-
font-size: 13px;
|
| 156 |
-
line-height: 1.7;
|
| 157 |
-
}
|
| 158 |
-
|
| 159 |
-
.lyrics-hint {
|
| 160 |
-
margin-top: 7px;
|
| 161 |
-
font-size: 11px;
|
| 162 |
-
color: var(--text-muted);
|
| 163 |
-
}
|
| 164 |
-
|
| 165 |
-
.lyrics-hint code {
|
| 166 |
-
background: var(--surface-3);
|
| 167 |
-
border-radius: 4px;
|
| 168 |
-
padding: 1px 5px;
|
| 169 |
-
color: var(--accent);
|
| 170 |
-
font-size: 11px;
|
| 171 |
-
}
|
| 172 |
-
|
| 173 |
-
/* ββ Inspire button (inline) ββββββββββββββββββββββββββββββββββββββββββββ */
|
| 174 |
-
.inspire-btn {
|
| 175 |
-
display: inline-flex;
|
| 176 |
-
align-items: center;
|
| 177 |
-
gap: 5px;
|
| 178 |
-
background: transparent;
|
| 179 |
-
border: 1px solid var(--border);
|
| 180 |
-
border-radius: 6px;
|
| 181 |
-
color: var(--text-muted);
|
| 182 |
-
font-family: var(--font);
|
| 183 |
-
font-size: 12px;
|
| 184 |
-
font-weight: 500;
|
| 185 |
-
padding: 5px 10px;
|
| 186 |
-
cursor: pointer;
|
| 187 |
-
transition: color 0.15s, border-color 0.15s;
|
| 188 |
-
white-space: nowrap;
|
| 189 |
-
}
|
| 190 |
-
|
| 191 |
-
.inspire-btn:hover:not(:disabled) {
|
| 192 |
-
color: var(--accent);
|
| 193 |
-
border-color: rgba(124, 106, 255, 0.4);
|
| 194 |
-
}
|
| 195 |
-
|
| 196 |
-
.inspire-btn:disabled {
|
| 197 |
-
opacity: 0.4;
|
| 198 |
-
cursor: not-allowed;
|
| 199 |
-
}
|
| 200 |
|
| 201 |
/* ββ Generate button ββββββββββββββββββββββββββββββββββββββββββββββββββββ */
|
| 202 |
.generate-btn {
|
| 203 |
-
width: 100%;
|
| 204 |
-
|
| 205 |
-
|
| 206 |
-
|
| 207 |
-
|
| 208 |
-
|
| 209 |
-
|
| 210 |
-
|
| 211 |
-
|
| 212 |
-
|
| 213 |
-
|
| 214 |
-
|
| 215 |
-
|
| 216 |
-
|
| 217 |
-
|
| 218 |
-
|
| 219 |
-
.
|
| 220 |
-
|
| 221 |
-
}
|
| 222 |
-
|
| 223 |
-
|
| 224 |
-
transform: scale(0.99);
|
| 225 |
-
}
|
| 226 |
-
|
| 227 |
-
.generate-btn:disabled {
|
| 228 |
-
background: var(--accent-dim);
|
| 229 |
-
cursor: not-allowed;
|
| 230 |
-
box-shadow: none;
|
| 231 |
-
}
|
| 232 |
-
|
| 233 |
-
.generate-btn.loading {
|
| 234 |
-
animation: pulse-glow 1.5s ease-in-out infinite;
|
| 235 |
-
}
|
| 236 |
-
|
| 237 |
-
@keyframes pulse-glow {
|
| 238 |
-
0%, 100% { box-shadow: 0 0 24px var(--accent-glow); }
|
| 239 |
-
50% { box-shadow: 0 0 48px rgba(124, 106, 255, 0.45); }
|
| 240 |
-
}
|
| 241 |
-
|
| 242 |
-
/* ββ Advanced section βββββββββββββββββββββββββββββββββββββββββββββββββββ */
|
| 243 |
-
details {
|
| 244 |
-
margin-bottom: 12px;
|
| 245 |
-
}
|
| 246 |
-
|
| 247 |
-
details summary {
|
| 248 |
-
list-style: none;
|
| 249 |
-
cursor: pointer;
|
| 250 |
-
display: flex;
|
| 251 |
-
align-items: center;
|
| 252 |
-
gap: 6px;
|
| 253 |
-
font-size: 12px;
|
| 254 |
-
font-weight: 500;
|
| 255 |
-
color: var(--text-muted);
|
| 256 |
-
padding: 8px 0;
|
| 257 |
-
user-select: none;
|
| 258 |
-
letter-spacing: 0.03em;
|
| 259 |
-
outline: none;
|
| 260 |
-
}
|
| 261 |
-
|
| 262 |
-
details summary::-webkit-details-marker { display: none; }
|
| 263 |
-
|
| 264 |
-
details summary::before {
|
| 265 |
-
content: 'βΊ';
|
| 266 |
-
font-size: 16px;
|
| 267 |
-
line-height: 1;
|
| 268 |
-
transition: transform 0.2s;
|
| 269 |
-
display: inline-block;
|
| 270 |
-
color: var(--text-faint);
|
| 271 |
-
}
|
| 272 |
-
|
| 273 |
-
details[open] summary::before {
|
| 274 |
-
transform: rotate(90deg);
|
| 275 |
-
}
|
| 276 |
-
|
| 277 |
-
details[open] summary {
|
| 278 |
-
color: var(--text);
|
| 279 |
-
}
|
| 280 |
-
|
| 281 |
-
.advanced-grid {
|
| 282 |
-
display: grid;
|
| 283 |
-
grid-template-columns: 1fr 1fr;
|
| 284 |
-
gap: 14px;
|
| 285 |
-
padding: 14px;
|
| 286 |
-
background: var(--surface);
|
| 287 |
-
border: 1px solid var(--border);
|
| 288 |
-
border-radius: var(--radius-lg);
|
| 289 |
-
}
|
| 290 |
-
|
| 291 |
-
.field-group {
|
| 292 |
-
display: flex;
|
| 293 |
-
flex-direction: column;
|
| 294 |
-
gap: 6px;
|
| 295 |
-
}
|
| 296 |
-
|
| 297 |
-
.field-group.full-width {
|
| 298 |
-
grid-column: 1 / -1;
|
| 299 |
-
}
|
| 300 |
-
|
| 301 |
-
.field-label {
|
| 302 |
-
font-size: 11px;
|
| 303 |
-
font-weight: 500;
|
| 304 |
-
color: var(--text-muted);
|
| 305 |
-
letter-spacing: 0.05em;
|
| 306 |
-
text-transform: uppercase;
|
| 307 |
-
}
|
| 308 |
-
|
| 309 |
-
.field-row {
|
| 310 |
-
display: flex;
|
| 311 |
-
align-items: center;
|
| 312 |
-
gap: 10px;
|
| 313 |
-
}
|
| 314 |
-
|
| 315 |
-
input[type="range"] {
|
| 316 |
-
flex: 1;
|
| 317 |
-
-webkit-appearance: none;
|
| 318 |
-
height: 4px;
|
| 319 |
-
background: var(--surface-3);
|
| 320 |
-
border-radius: 2px;
|
| 321 |
-
outline: none;
|
| 322 |
-
border: none;
|
| 323 |
-
}
|
| 324 |
-
|
| 325 |
-
input[type="range"]::-webkit-slider-thumb {
|
| 326 |
-
-webkit-appearance: none;
|
| 327 |
-
width: 14px;
|
| 328 |
-
height: 14px;
|
| 329 |
-
background: var(--accent);
|
| 330 |
-
border-radius: 50%;
|
| 331 |
-
cursor: pointer;
|
| 332 |
-
box-shadow: 0 0 8px var(--accent-glow);
|
| 333 |
-
}
|
| 334 |
-
|
| 335 |
-
.range-value {
|
| 336 |
-
font-size: 12px;
|
| 337 |
-
color: var(--text);
|
| 338 |
-
min-width: 36px;
|
| 339 |
-
text-align: right;
|
| 340 |
-
font-variant-numeric: tabular-nums;
|
| 341 |
-
}
|
| 342 |
-
|
| 343 |
-
input[type="number"] {
|
| 344 |
-
width: 100%;
|
| 345 |
-
}
|
| 346 |
-
|
| 347 |
-
input[type="text"] {
|
| 348 |
-
width: 100%;
|
| 349 |
-
}
|
| 350 |
-
|
| 351 |
-
/* ββ Output section βββββββββββββββββββββββββββββββββββββββββββββββββββββ */
|
| 352 |
-
.output-section {
|
| 353 |
-
margin-bottom: 32px;
|
| 354 |
-
}
|
| 355 |
-
|
| 356 |
.output-card {
|
| 357 |
-
background: var(--surface);
|
| 358 |
-
border:
|
| 359 |
-
|
| 360 |
-
padding: 20px;
|
| 361 |
-
animation: fade-in 0.3s ease;
|
| 362 |
-
}
|
| 363 |
-
|
| 364 |
-
@keyframes fade-in {
|
| 365 |
-
from { opacity: 0; transform: translateY(8px); }
|
| 366 |
-
to { opacity: 1; transform: translateY(0); }
|
| 367 |
}
|
|
|
|
| 368 |
|
| 369 |
#waveform {
|
| 370 |
-
|
| 371 |
-
border-radius: var(--radius);
|
| 372 |
-
|
| 373 |
-
cursor: pointer;
|
| 374 |
-
margin-bottom: 14px;
|
| 375 |
}
|
| 376 |
-
|
| 377 |
-
|
| 378 |
-
|
| 379 |
-
align-items: center;
|
| 380 |
-
gap: 14px;
|
| 381 |
}
|
| 382 |
-
|
| 383 |
.play-btn {
|
| 384 |
-
width:
|
| 385 |
-
|
| 386 |
-
|
| 387 |
-
|
| 388 |
-
|
| 389 |
-
|
| 390 |
-
|
| 391 |
-
|
| 392 |
-
|
| 393 |
-
align-items: center;
|
| 394 |
-
|
| 395 |
-
|
| 396 |
-
transition:
|
| 397 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 398 |
}
|
|
|
|
| 399 |
|
| 400 |
-
.
|
| 401 |
-
|
| 402 |
-
|
| 403 |
-
|
| 404 |
-
font-size: 12px;
|
| 405 |
-
color: var(--text-muted);
|
| 406 |
-
font-variant-numeric: tabular-nums;
|
| 407 |
-
flex: 1;
|
| 408 |
}
|
|
|
|
|
|
|
| 409 |
|
| 410 |
-
.
|
| 411 |
-
|
| 412 |
-
|
| 413 |
-
|
| 414 |
-
|
| 415 |
-
|
| 416 |
-
border-radius: 6px;
|
| 417 |
-
color: var(--text-muted);
|
| 418 |
-
text-decoration: none;
|
| 419 |
-
font-family: var(--font);
|
| 420 |
-
font-size: 12px;
|
| 421 |
-
font-weight: 500;
|
| 422 |
-
padding: 6px 12px;
|
| 423 |
-
transition: color 0.15s, border-color 0.15s;
|
| 424 |
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 425 |
|
| 426 |
-
|
| 427 |
-
|
| 428 |
-
|
|
|
|
|
|
|
| 429 |
}
|
|
|
|
|
|
|
| 430 |
|
| 431 |
-
|
| 432 |
-
|
| 433 |
-
|
| 434 |
-
color: var(--text-muted);
|
| 435 |
-
text-align: center;
|
| 436 |
-
padding: 8px 0;
|
| 437 |
-
min-height: 30px;
|
| 438 |
-
transition: color 0.2s;
|
| 439 |
}
|
| 440 |
-
|
| 441 |
-
|
| 442 |
-
|
| 443 |
}
|
| 444 |
-
|
| 445 |
-
|
| 446 |
-
|
| 447 |
}
|
| 448 |
|
| 449 |
-
/* ββ
|
| 450 |
-
|
| 451 |
-
margin-top:
|
| 452 |
-
|
| 453 |
-
text-align: center;
|
| 454 |
-
font-size: 11px;
|
| 455 |
-
color: var(--text-faint);
|
| 456 |
}
|
| 457 |
-
|
| 458 |
-
|
| 459 |
-
|
| 460 |
-
text-decoration: none;
|
| 461 |
}
|
| 462 |
-
|
| 463 |
-
|
| 464 |
|
| 465 |
/* ββ Spinner ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ */
|
| 466 |
.spinner {
|
| 467 |
-
display: inline-block;
|
| 468 |
-
|
| 469 |
-
|
| 470 |
-
border: 2px solid rgba(255,255,255,0.15);
|
| 471 |
-
border-top-color: #fff;
|
| 472 |
-
border-radius: 50%;
|
| 473 |
-
animation: spin 0.7s linear infinite;
|
| 474 |
-
vertical-align: middle;
|
| 475 |
}
|
|
|
|
| 476 |
|
| 477 |
-
|
| 478 |
-
|
| 479 |
-
}
|
|
|
|
| 480 |
|
| 481 |
/* ββ Mobile βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ */
|
| 482 |
-
|
| 483 |
-
|
| 484 |
-
.
|
| 485 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 486 |
}
|
| 487 |
</style>
|
| 488 |
</head>
|
| 489 |
<body>
|
| 490 |
|
| 491 |
-
<div class="
|
| 492 |
|
| 493 |
-
<!--
|
| 494 |
-
<
|
| 495 |
-
<div class="wordmark">
|
| 496 |
-
<div class="wordmark-icon">βͺ</div>
|
| 497 |
-
<div class="wordmark-text">ACE<span>-Step</span> Studio</div>
|
| 498 |
-
</div>
|
| 499 |
-
<p>Turbo music generation Β· 8 steps Β· ~2s for 60s of audio</p>
|
| 500 |
-
</header>
|
| 501 |
-
|
| 502 |
-
<!-- Style / Tags card -->
|
| 503 |
-
<div class="card">
|
| 504 |
-
<div class="card-header">
|
| 505 |
-
<span class="card-label">Style & Tags</span>
|
| 506 |
-
<button class="inspire-btn" id="inspire-btn">β¨ Inspire me</button>
|
| 507 |
-
</div>
|
| 508 |
-
<textarea
|
| 509 |
-
id="prompt-input"
|
| 510 |
-
placeholder="lo-fi, chill, jazzy piano, female vocals, rain ambiance..."
|
| 511 |
-
></textarea>
|
| 512 |
-
</div>
|
| 513 |
-
|
| 514 |
-
<!-- Lyrics card -->
|
| 515 |
-
<div class="card">
|
| 516 |
-
<div class="card-header">
|
| 517 |
-
<span class="card-label">Lyrics</span>
|
| 518 |
-
</div>
|
| 519 |
-
<textarea
|
| 520 |
-
id="lyrics-input"
|
| 521 |
-
placeholder="[verse] Write your verse lyrics here... [chorus] Your chorus goes here... [bridge] Optional bridge..."
|
| 522 |
-
></textarea>
|
| 523 |
-
<p class="lyrics-hint">
|
| 524 |
-
Use <code>[verse]</code> <code>[chorus]</code> <code>[bridge]</code> section markers
|
| 525 |
-
</p>
|
| 526 |
-
</div>
|
| 527 |
|
| 528 |
-
|
| 529 |
-
|
| 530 |
-
|
| 531 |
-
|
| 532 |
-
<div class="status-bar" id="status-bar"></div>
|
| 533 |
-
|
| 534 |
-
<!-- Advanced options -->
|
| 535 |
-
<details id="advanced-details">
|
| 536 |
-
<summary>Advanced Options</summary>
|
| 537 |
-
<div class="advanced-grid">
|
| 538 |
-
|
| 539 |
-
<div class="field-group">
|
| 540 |
-
<span class="field-label">Duration (seconds)</span>
|
| 541 |
-
<div class="field-row">
|
| 542 |
-
<input type="range" id="duration-slider" min="10" max="240" step="5" value="60" />
|
| 543 |
-
<span class="range-value" id="duration-value">60s</span>
|
| 544 |
-
</div>
|
| 545 |
</div>
|
| 546 |
-
|
| 547 |
-
|
| 548 |
-
|
| 549 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 550 |
</div>
|
| 551 |
-
|
| 552 |
-
<div class="field-group full-width">
|
| 553 |
-
<span class="field-label">LoRA (HF repo ID or leave blank)</span>
|
| 554 |
-
<input type="text" id="lora-path-input" placeholder="e.g. ACE-Step/ACE-Step-v1-chinese-rap-LoRA" />
|
| 555 |
-
</div>
|
| 556 |
-
|
| 557 |
-
<div class="field-group">
|
| 558 |
-
<span class="field-label">LoRA Weight</span>
|
| 559 |
-
<div class="field-row">
|
| 560 |
-
<input type="range" id="lora-weight-slider" min="0" max="2" step="0.05" value="0.8" />
|
| 561 |
-
<span class="range-value" id="lora-weight-value">0.8</span>
|
| 562 |
-
</div>
|
| 563 |
-
</div>
|
| 564 |
-
|
| 565 |
</div>
|
| 566 |
-
|
| 567 |
-
|
| 568 |
-
|
| 569 |
-
|
| 570 |
-
|
| 571 |
-
|
| 572 |
-
|
| 573 |
-
|
| 574 |
-
|
| 575 |
-
|
| 576 |
-
|
| 577 |
-
</
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 578 |
</div>
|
| 579 |
</div>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 580 |
</div>
|
| 581 |
|
| 582 |
-
</div>
|
| 583 |
|
| 584 |
<footer>
|
| 585 |
-
<
|
| 586 |
-
|
| 587 |
-
Β·
|
| 588 |
-
<a href="https://huggingface.co/ACE-Step/acestep-v15-xl-turbo" target="_blank">Model card</a>
|
| 589 |
-
</div>
|
| 590 |
</footer>
|
| 591 |
|
| 592 |
<script type="module">
|
| 593 |
-
// ββ ZeroGPU auth
|
| 594 |
-
// Without this, requests in the HF iframe are treated as anonymous (2 min GPU
|
| 595 |
-
// quota instead of 25 min for logged-in users), causing generation to fail.
|
| 596 |
const ZEROGPU_HEADERS_MSG = "supports-zerogpu-headers";
|
| 597 |
-
window.addEventListener("message", (
|
| 598 |
-
if (event.data === ZEROGPU_HEADERS_MSG) {
|
| 599 |
-
window.supports_zerogpu_headers = true;
|
| 600 |
-
}
|
| 601 |
-
});
|
| 602 |
const _hn = window.location.hostname;
|
| 603 |
if (_hn.endsWith(".hf.space") || _hn.includes(".dev.")) {
|
| 604 |
-
const
|
| 605 |
-
|
| 606 |
-
: "https://huggingface.co";
|
| 607 |
-
window.parent.postMessage(ZEROGPU_HEADERS_MSG, _origin);
|
| 608 |
}
|
| 609 |
-
// ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 610 |
|
| 611 |
import { Client } from "https://cdn.jsdelivr.net/npm/@gradio/client@1.15.2/dist/index.min.js";
|
| 612 |
-
|
| 613 |
|
| 614 |
-
// ββ DOM
|
| 615 |
-
const
|
| 616 |
-
const
|
|
|
|
|
|
|
| 617 |
const generateBtn = document.getElementById("generate-btn");
|
| 618 |
-
const inspireBtn = document.getElementById("inspire-btn");
|
| 619 |
const statusBar = document.getElementById("status-bar");
|
| 620 |
const outputSection = document.getElementById("output-section");
|
|
|
|
|
|
|
| 621 |
const playBtn = document.getElementById("play-btn");
|
| 622 |
const timeDisplay = document.getElementById("time-display");
|
| 623 |
const downloadBtn = document.getElementById("download-btn");
|
| 624 |
-
const
|
| 625 |
-
const durationValue = document.getElementById("duration-value");
|
| 626 |
-
const loraWeightSlider = document.getElementById("lora-weight-slider");
|
| 627 |
-
const loraWeightValue = document.getElementById("lora-weight-value");
|
| 628 |
-
const loraPathInput = document.getElementById("lora-path-input");
|
| 629 |
-
const seedInput = document.getElementById("seed-input");
|
| 630 |
-
|
| 631 |
-
// ββ Slider live-value display ββββββββββββββββββββββββββββββββββββββββββββ
|
| 632 |
-
durationSlider.addEventListener("input", () => {
|
| 633 |
-
durationValue.textContent = `${durationSlider.value}s`;
|
| 634 |
-
});
|
| 635 |
-
loraWeightSlider.addEventListener("input", () => {
|
| 636 |
-
loraWeightValue.textContent = parseFloat(loraWeightSlider.value).toFixed(2);
|
| 637 |
-
});
|
| 638 |
|
| 639 |
-
// ββ
|
| 640 |
-
const
|
| 641 |
-
|
| 642 |
-
waveColor: "#7c6aff",
|
| 643 |
-
progressColor: "#4dff91",
|
| 644 |
-
cursorColor: "rgba(255,255,255,0.15)",
|
| 645 |
-
barWidth: 2,
|
| 646 |
-
barGap: 1,
|
| 647 |
-
barRadius: 2,
|
| 648 |
-
height: 80,
|
| 649 |
-
normalize: true,
|
| 650 |
-
backend: "WebAudio",
|
| 651 |
-
});
|
| 652 |
-
|
| 653 |
-
ws.on("play", () => { playBtn.textContent = "βΈ"; });
|
| 654 |
-
ws.on("pause", () => { playBtn.textContent = "βΆ"; });
|
| 655 |
-
ws.on("finish",() => { playBtn.textContent = "βΆ"; });
|
| 656 |
-
ws.on("audioprocess", updateTime);
|
| 657 |
-
ws.on("ready", updateTime);
|
| 658 |
-
|
| 659 |
-
playBtn.addEventListener("click", () => ws.playPause());
|
| 660 |
|
|
|
|
| 661 |
function updateTime() {
|
| 662 |
-
const
|
| 663 |
-
|
| 664 |
-
|
| 665 |
-
|
| 666 |
-
|
| 667 |
-
|
| 668 |
-
|
| 669 |
-
|
| 670 |
-
|
| 671 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 672 |
|
| 673 |
-
// ββ Gradio client
|
| 674 |
let gradioClient = null;
|
| 675 |
-
|
| 676 |
async function getClient() {
|
| 677 |
-
if (!gradioClient)
|
| 678 |
-
gradioClient = await Client.connect(window.location.origin);
|
| 679 |
-
}
|
| 680 |
return gradioClient;
|
| 681 |
}
|
| 682 |
|
| 683 |
-
// ββ UI
|
| 684 |
-
function
|
| 685 |
-
|
| 686 |
-
|
| 687 |
-
|
| 688 |
-
generateBtn.innerHTML = '<span class="spinner"></span> Generatingβ¦';
|
| 689 |
-
generateBtn.classList.add("loading");
|
| 690 |
-
inspireBtn.disabled = true;
|
| 691 |
-
} else {
|
| 692 |
-
generateBtn.disabled = false;
|
| 693 |
-
generateBtn.textContent = "Generate";
|
| 694 |
-
generateBtn.classList.remove("loading");
|
| 695 |
-
inspireBtn.disabled = false;
|
| 696 |
-
}
|
| 697 |
}
|
| 698 |
-
|
| 699 |
function setStatus(msg, type = "") {
|
| 700 |
statusBar.textContent = msg;
|
| 701 |
statusBar.className = "status-bar" + (type ? ` ${type}` : "");
|
| 702 |
}
|
| 703 |
|
| 704 |
-
function
|
| 705 |
-
|
| 706 |
-
statusBar.className = "status-bar";
|
| 707 |
-
}
|
| 708 |
-
|
| 709 |
-
// ββ Load audio into WaveSurfer βββββββββββββββββββββββββββββββββββββββββββ
|
| 710 |
-
async function loadAudio(fileData) {
|
| 711 |
-
// Gradio returns { url: "...", ... } or a plain string URL
|
| 712 |
-
const url = typeof fileData === "string" ? fileData
|
| 713 |
-
: (fileData?.url || fileData?.path || String(fileData));
|
| 714 |
-
|
| 715 |
-
const res = await fetch(url);
|
| 716 |
-
if (!res.ok) throw new Error(`Failed to fetch audio: ${res.status}`);
|
| 717 |
const blob = await res.blob();
|
| 718 |
-
const
|
| 719 |
|
| 720 |
-
|
| 721 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 722 |
outputSection.style.display = "block";
|
| 723 |
-
|
| 724 |
-
|
| 725 |
}
|
| 726 |
|
| 727 |
-
// ββ
|
| 728 |
-
|
| 729 |
-
|
| 730 |
-
const lyrics = lyricsInput.value.trim();
|
| 731 |
|
| 732 |
-
|
| 733 |
-
|
| 734 |
-
|
| 735 |
-
|
|
|
|
|
|
|
|
|
|
| 736 |
}
|
| 737 |
-
|
| 738 |
-
|
| 739 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 740 |
return;
|
| 741 |
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 742 |
|
| 743 |
-
|
| 744 |
-
|
| 745 |
-
|
| 746 |
try {
|
| 747 |
const client = await getClient();
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 748 |
|
| 749 |
-
|
| 750 |
-
|
| 751 |
-
|
| 752 |
-
|
| 753 |
-
|
| 754 |
-
|
| 755 |
-
|
| 756 |
-
lora_name_or_path: loraPathInput.value.trim(),
|
| 757 |
-
lora_weight: parseFloat(loraWeightSlider.value),
|
| 758 |
});
|
| 759 |
|
| 760 |
for await (const msg of job) {
|
| 761 |
if (msg.type === "status") {
|
| 762 |
-
|
| 763 |
-
|
| 764 |
-
|
| 765 |
-
setStatus(`Queue position ${s.position}/${s.queue_size}${eta}`);
|
| 766 |
-
} else if (s.stage === "generating" || s.status === "generating") {
|
| 767 |
-
setStatus("Generatingβ¦");
|
| 768 |
}
|
| 769 |
} else if (msg.type === "data") {
|
| 770 |
-
const
|
| 771 |
-
if (!
|
| 772 |
-
|
| 773 |
-
|
| 774 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 775 |
}
|
| 776 |
}
|
| 777 |
} catch (err) {
|
| 778 |
-
console.error(
|
| 779 |
setStatus(`Error: ${err.message}`, "error");
|
| 780 |
} finally {
|
| 781 |
-
|
|
|
|
| 782 |
}
|
| 783 |
});
|
| 784 |
|
| 785 |
-
|
| 786 |
-
|
| 787 |
-
inspireBtn.disabled = true;
|
| 788 |
-
inspireBtn.textContent = "β¦";
|
| 789 |
-
|
| 790 |
-
try {
|
| 791 |
-
const client = await getClient();
|
| 792 |
-
const result = await client.predict("/inspire", {
|
| 793 |
-
genre_hint: promptInput.value.trim(),
|
| 794 |
-
});
|
| 795 |
-
const lyrics = result?.data?.[0] || "";
|
| 796 |
-
if (lyrics.startsWith("[Error")) {
|
| 797 |
-
setStatus(lyrics, "error");
|
| 798 |
-
} else {
|
| 799 |
-
lyricsInput.value = lyrics;
|
| 800 |
-
clearStatus();
|
| 801 |
-
}
|
| 802 |
-
} catch (err) {
|
| 803 |
-
setStatus(`Inspire failed: ${err.message}`, "error");
|
| 804 |
-
} finally {
|
| 805 |
-
inspireBtn.disabled = false;
|
| 806 |
-
inspireBtn.textContent = "β¨ Inspire me";
|
| 807 |
-
}
|
| 808 |
});
|
| 809 |
|
| 810 |
-
// ββ
|
| 811 |
-
|
| 812 |
-
getClient().catch(console.error);
|
| 813 |
</script>
|
| 814 |
|
| 815 |
</body>
|
|
|
|
| 3 |
<head>
|
| 4 |
<meta charset="UTF-8" />
|
| 5 |
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
| 6 |
+
<title>ace-step-jam</title>
|
| 7 |
<link rel="preconnect" href="https://fonts.googleapis.com" />
|
| 8 |
+
<link href="https://fonts.googleapis.com/css2?family=Hanken+Grotesk:wght@300;400;500;600&display=swap" rel="stylesheet" />
|
| 9 |
<style>
|
|
|
|
| 10 |
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
|
| 11 |
|
| 12 |
:root {
|
| 13 |
+
--bg: oklch(0.13 0.006 260);
|
| 14 |
+
--surface: rgba(255, 255, 255, 0.04);
|
| 15 |
+
--surface-2: rgba(255, 255, 255, 0.06);
|
| 16 |
+
--surface-3: rgba(255, 255, 255, 0.09);
|
| 17 |
+
--border: rgba(255, 255, 255, 0.08);
|
| 18 |
+
--accent: oklch(0.90 0.005 260);
|
| 19 |
+
--accent-dim: oklch(0.40 0.005 260);
|
| 20 |
+
--accent-glow: rgba(255, 255, 255, 0.08);
|
| 21 |
+
--progress: oklch(0.72 0.14 155);
|
| 22 |
+
--text: rgba(255, 255, 255, 0.87);
|
| 23 |
+
--text-muted: rgba(255, 255, 255, 0.40);
|
| 24 |
+
--text-faint: rgba(255, 255, 255, 0.25);
|
| 25 |
+
--error: oklch(0.68 0.19 22);
|
| 26 |
+
--success: oklch(0.72 0.14 155);
|
| 27 |
+
--radius: 8px;
|
| 28 |
+
--radius-lg: 10px;
|
| 29 |
+
--font: 'Hanken Grotesk', system-ui, sans-serif;
|
|
|
|
| 30 |
}
|
| 31 |
|
| 32 |
html, body {
|
| 33 |
+
height: 100%; background: var(--bg); color: var(--text);
|
| 34 |
+
font-family: var(--font); font-size: 13.5px; line-height: 1.5;
|
|
|
|
|
|
|
|
|
|
|
|
|
| 35 |
-webkit-font-smoothing: antialiased;
|
| 36 |
}
|
| 37 |
|
| 38 |
+
body { display: flex; flex-direction: column; min-height: 100vh; }
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 39 |
|
| 40 |
+
/* ββ Layout βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ */
|
| 41 |
+
.page { display: flex; flex: 1; max-width: 1100px; width: 100%; margin: 0 auto; padding: 28px 20px 0; gap: 20px; align-items: flex-start; }
|
| 42 |
+
.col-left { flex: 1; min-width: 0; max-width: 480px; position: sticky; top: 28px; background: var(--bg); }
|
| 43 |
+
.col-right { flex: 1; min-width: 0; }
|
|
|
|
|
|
|
| 44 |
|
| 45 |
/* ββ Header βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ */
|
| 46 |
+
header { padding: 0 0 20px; text-align: left; }
|
| 47 |
+
.wordmark { display: inline-flex; align-items: center; gap: 8px; margin-bottom: 4px; }
|
| 48 |
+
.wordmark-logo {
|
| 49 |
+
height: 21px; width: auto; color: var(--text); flex-shrink: 0;
|
| 50 |
+
}
|
| 51 |
+
.version {
|
| 52 |
+
font-family: ui-monospace, 'SF Mono', monospace;
|
| 53 |
+
font-size: 10px; font-weight: 500; letter-spacing: 0.03em;
|
| 54 |
+
color: var(--text); border: 1px solid var(--border);
|
| 55 |
+
border-radius: 4px; padding: 1px 5px;
|
| 56 |
+
text-decoration: none; transition: border-color 0.15s, color 0.15s;
|
| 57 |
+
}
|
| 58 |
+
.version:hover { color: var(--text); border-color: rgba(255, 255, 255, 0.15); }
|
| 59 |
+
header p { font-size: 14px; color: var(--text-muted); }
|
| 60 |
+
|
| 61 |
+
/* ββ Compose box βββββββββββββββββββββββββββββββββββββββββββββββββββββββ */
|
| 62 |
+
.compose-box {
|
| 63 |
+
background: var(--surface); border: 1px solid var(--border);
|
| 64 |
+
border-radius: var(--radius-lg); margin-bottom: 10px;
|
| 65 |
+
transition: border-color 0.15s;
|
| 66 |
}
|
| 67 |
+
.compose-box:focus-within { border-color: rgba(255, 255, 255, 0.14); }
|
| 68 |
+
.compose-box #description-input {
|
| 69 |
+
background: transparent; border: none; border-radius: var(--radius-lg) var(--radius-lg) 0 0;
|
| 70 |
+
padding: 14px 14px 6px; height: 80px;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 71 |
}
|
| 72 |
+
.compose-box #description-input:focus,
|
| 73 |
+
.compose-box #description-input:focus-visible { border: none; outline: none; }
|
| 74 |
+
.compose-controls {
|
| 75 |
+
display: flex; gap: 4px; padding: 4px 10px 10px;
|
| 76 |
+
align-items: center; flex-wrap: wrap;
|
|
|
|
| 77 |
}
|
| 78 |
+
.compose-controls .pill {
|
| 79 |
+
background: transparent; border: none; padding: 4px 8px;
|
| 80 |
+
font-size: 11px; border-radius: 6px;
|
| 81 |
}
|
| 82 |
+
.compose-controls .pill:hover { background: var(--surface-2); }
|
| 83 |
+
.compose-controls .pill select { font-size: 11px; min-width: 44px; background: transparent; border: none; }
|
| 84 |
+
.compose-controls .pill input[type="checkbox"] { width: 11px; height: 11px; }
|
| 85 |
+
.compose-controls .seed-input {
|
| 86 |
+
width: 48px !important; font-size: 11px !important; padding: 3px 6px !important;
|
| 87 |
+
background: transparent !important; border-radius: 6px !important;
|
| 88 |
}
|
| 89 |
+
.compose-controls .generate-btn {
|
| 90 |
+
width: auto; padding: 5px 14px; font-size: 12px;
|
| 91 |
+
border-radius: 6px; letter-spacing: 0;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 92 |
}
|
| 93 |
|
| 94 |
/* ββ Inputs βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ */
|
| 95 |
+
textarea, input[type="text"], input[type="number"], select {
|
| 96 |
+
width: 100%; background: var(--surface-2); border: 1px solid var(--border);
|
| 97 |
+
border-radius: var(--radius); color: var(--text); font-family: var(--font);
|
| 98 |
+
font-size: 13.5px; padding: 9px 11px; outline: none; transition: border-color 0.15s;
|
| 99 |
+
}
|
| 100 |
+
textarea:focus, input:focus, select:focus { border-color: rgba(255, 255, 255, 0.18); }
|
| 101 |
+
textarea:focus-visible, input:focus-visible, select:focus-visible { outline: 1px solid rgba(255, 255, 255, 0.08); outline-offset: 1px; }
|
| 102 |
+
textarea::placeholder, input::placeholder { color: var(--text-faint); }
|
| 103 |
+
textarea { resize: none; }
|
| 104 |
+
|
| 105 |
+
#description-input { height: 76px; font-size: 14px; }
|
| 106 |
+
.prompt-hint { margin-top: 6px; font-size: 11px; color: var(--text-faint); }
|
| 107 |
+
|
| 108 |
+
/* ββ Controls row βββββββββββββββββββββββββββββββββββββββββββββββββββββββ */
|
| 109 |
+
.controls-row { display: flex; gap: 6px; margin-bottom: 10px; align-items: center; flex-wrap: wrap; }
|
| 110 |
+
|
| 111 |
+
.pill {
|
| 112 |
+
display: inline-flex; align-items: center; gap: 5px;
|
| 113 |
+
font-size: 12px; color: var(--text-muted); cursor: pointer; user-select: none;
|
| 114 |
+
background: var(--surface); border: 1px solid var(--border);
|
| 115 |
+
border-radius: var(--radius); padding: 6px 10px; white-space: nowrap;
|
| 116 |
transition: border-color 0.15s;
|
| 117 |
}
|
| 118 |
+
.pill:hover { border-color: rgba(255, 255, 255, 0.12); }
|
| 119 |
+
.pill input[type="checkbox"] { width: 12px; height: 12px; accent-color: var(--accent); cursor: pointer; }
|
| 120 |
+
.pill:has(input:checked) span { color: var(--text); }
|
| 121 |
+
.pill select { background: transparent; border: none; color: var(--text); font-size: 12px; padding: 0; min-width: 56px; }
|
| 122 |
|
| 123 |
+
.seed-input { width: 72px !important; font-size: 12px !important; padding: 6px 8px !important; }
|
| 124 |
+
.spacer { flex: 1; }
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 125 |
|
| 126 |
/* ββ Generate button ββββββββββββββββββββββββββββββββββββββββββββββββββββ */
|
| 127 |
.generate-btn {
|
| 128 |
+
width: 100%; background: var(--accent); border: none;
|
| 129 |
+
border-radius: var(--radius); color: oklch(0.13 0.006 260); font-family: var(--font);
|
| 130 |
+
font-size: 14px; font-weight: 600; padding: 11px; cursor: pointer;
|
| 131 |
+
transition: opacity 0.15s, transform 0.1s; letter-spacing: -0.01em;
|
| 132 |
+
}
|
| 133 |
+
.generate-btn:hover:not(:disabled) { opacity: 0.88; }
|
| 134 |
+
.generate-btn:active:not(:disabled) { transform: scale(0.99); }
|
| 135 |
+
.generate-btn:disabled { background: var(--accent-dim); cursor: not-allowed; opacity: 0.5; }
|
| 136 |
+
.generate-btn:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }
|
| 137 |
+
.generate-btn.loading { animation: btn-pulse 2s ease-in-out infinite; }
|
| 138 |
+
@keyframes btn-pulse {
|
| 139 |
+
0%, 100% { opacity: 1; }
|
| 140 |
+
50% { opacity: 0.7; }
|
| 141 |
+
}
|
| 142 |
+
|
| 143 |
+
/* ββ Status βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ */
|
| 144 |
+
.status-bar { font-size: 11.5px; color: var(--text-muted); text-align: center; padding: 0; min-height: 0; display: none; }
|
| 145 |
+
.status-bar.error { display: block; color: var(--error); padding: 6px 0; }
|
| 146 |
+
.status-bar.success { display: none; }
|
| 147 |
+
|
| 148 |
+
/* ββ Output card ββββββββββββββββββββββββββββββββββββββββββββββββββββββββ */
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 149 |
.output-card {
|
| 150 |
+
background: var(--surface); border: 1px solid var(--border);
|
| 151 |
+
border-radius: var(--radius-lg); padding: 14px; margin-bottom: 10px;
|
| 152 |
+
animation: fade-in 0.25s ease;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 153 |
}
|
| 154 |
+
@keyframes fade-in { from { opacity: 0; transform: translateY(4px); } to { opacity: 1; transform: translateY(0); } }
|
| 155 |
|
| 156 |
#waveform {
|
| 157 |
+
display: flex; align-items: end; gap: 1px; height: 56px;
|
| 158 |
+
background: var(--surface-2); border-radius: var(--radius);
|
| 159 |
+
padding: 8px 10px; cursor: pointer; margin-bottom: 10px;
|
|
|
|
|
|
|
| 160 |
}
|
| 161 |
+
#waveform .bar {
|
| 162 |
+
flex: 1; background: var(--text-faint); border-radius: 1px;
|
| 163 |
+
transition: background 0.15s;
|
|
|
|
|
|
|
| 164 |
}
|
| 165 |
+
.playback-controls { display: flex; align-items: center; gap: 10px; }
|
| 166 |
.play-btn {
|
| 167 |
+
width: 30px; height: 30px; background: var(--accent); border: none;
|
| 168 |
+
border-radius: 50%; color: oklch(0.13 0.006 260); font-size: 12px; cursor: pointer;
|
| 169 |
+
display: flex; align-items: center; justify-content: center; flex-shrink: 0;
|
| 170 |
+
transition: opacity 0.15s;
|
| 171 |
+
}
|
| 172 |
+
.play-btn:hover { opacity: 0.85; }
|
| 173 |
+
.play-btn:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }
|
| 174 |
+
.time-display { font-size: 11.5px; color: var(--text-muted); font-variant-numeric: tabular-nums; flex: 1; }
|
| 175 |
+
.download-btn {
|
| 176 |
+
display: inline-flex; align-items: center; gap: 4px; background: transparent;
|
| 177 |
+
border: 1px solid var(--border); border-radius: 6px; color: var(--text-muted);
|
| 178 |
+
text-decoration: none; font-family: var(--font); font-size: 11px;
|
| 179 |
+
font-weight: 500; padding: 4px 9px; transition: border-color 0.15s, color 0.15s;
|
| 180 |
+
}
|
| 181 |
+
.download-btn:hover { color: var(--text); border-color: rgba(255, 255, 255, 0.15); }
|
| 182 |
+
.download-btn:focus-visible { outline: 2px solid var(--accent); outline-offset: 1px; }
|
| 183 |
+
|
| 184 |
+
/* ββ Community feed βββββββββββββββββββββββββββββββββββββββββββββββββββββ */
|
| 185 |
+
.feed-header {
|
| 186 |
+
font-size: 11px; font-weight: 600; color: var(--text-muted);
|
| 187 |
+
text-transform: uppercase; letter-spacing: 0.06em;
|
| 188 |
+
margin-bottom: 10px;
|
| 189 |
}
|
| 190 |
+
.feed-empty { font-size: 12px; color: var(--text-faint); text-align: center; padding: 32px 0; }
|
| 191 |
|
| 192 |
+
.song-card {
|
| 193 |
+
background: transparent; border: 1px solid transparent;
|
| 194 |
+
border-radius: var(--radius); padding: 8px 10px; margin-bottom: 6px;
|
| 195 |
+
cursor: pointer; transition: background 0.15s;
|
|
|
|
|
|
|
|
|
|
|
|
|
| 196 |
}
|
| 197 |
+
.song-card:hover { background: var(--surface); }
|
| 198 |
+
.song-card.playing { background: var(--surface); }
|
| 199 |
|
| 200 |
+
.song-card-top { display: flex; align-items: center; gap: 8px; margin-bottom: 3px; }
|
| 201 |
+
.song-play-btn {
|
| 202 |
+
width: 26px; height: 26px; background: var(--surface-3); border: 1px solid var(--border);
|
| 203 |
+
border-radius: 50%; color: var(--text-muted); font-size: 10px; cursor: pointer;
|
| 204 |
+
display: flex; align-items: center; justify-content: center; flex-shrink: 0;
|
| 205 |
+
transition: background 0.15s, color 0.15s, border-color 0.15s;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 206 |
}
|
| 207 |
+
.song-play-btn:focus-visible { outline: 2px solid var(--accent); outline-offset: 1px; }
|
| 208 |
+
.song-card:hover .song-play-btn { background: rgba(255, 255, 255, 0.08); color: var(--text); border-color: rgba(255, 255, 255, 0.12); }
|
| 209 |
+
.song-card.playing .song-play-btn { background: var(--accent); color: oklch(0.13 0.006 260); border-color: var(--accent); }
|
| 210 |
+
.song-title { font-size: 13px; font-weight: 500; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
| 211 |
+
.song-tags { font-size: 11px; color: var(--text-muted); flex: 1; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; text-align: right; }
|
| 212 |
+
.song-duration { font-size: 11px; color: var(--text-muted); flex-shrink: 0; font-variant-numeric: tabular-nums; margin-left: 8px; }
|
| 213 |
|
| 214 |
+
/* ββ Placeholder (loading state) ββββββββββββββββββββββββββββββββββββββββ */
|
| 215 |
+
.placeholder-card {
|
| 216 |
+
background: var(--surface); border: 1px solid rgba(255, 255, 255, 0.12);
|
| 217 |
+
border-radius: var(--radius-lg); padding: 14px;
|
| 218 |
+
margin-bottom: 10px; animation: fade-in 0.25s ease;
|
| 219 |
}
|
| 220 |
+
.placeholder-title { font-size: 13px; font-weight: 600; color: var(--text); margin-bottom: 3px; }
|
| 221 |
+
.placeholder-sub { font-size: 11px; color: var(--text-muted); margin-bottom: 10px; }
|
| 222 |
|
| 223 |
+
.fake-waveform {
|
| 224 |
+
display: flex; align-items: end; gap: 2px; height: 44px;
|
| 225 |
+
background: var(--surface-2); border-radius: var(--radius); padding: 8px 10px;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 226 |
}
|
| 227 |
+
.fake-waveform .bar {
|
| 228 |
+
flex: 1; background: var(--accent); border-radius: 1px; opacity: 0.3;
|
| 229 |
+
animation: wave-pulse 1.2s ease-in-out infinite;
|
| 230 |
}
|
| 231 |
+
@keyframes wave-pulse {
|
| 232 |
+
0%, 100% { transform: scaleY(0.3); opacity: 0.15; }
|
| 233 |
+
50% { transform: scaleY(1); opacity: 0.4; }
|
| 234 |
}
|
| 235 |
|
| 236 |
+
/* ββ Mini waveform ββββββββββββββββββββββββββββββββββββββββββββββββββββββ */
|
| 237 |
+
.mini-waveform {
|
| 238 |
+
display: flex; align-items: end; gap: 1px; height: 22px; margin-top: 5px;
|
| 239 |
+
cursor: pointer;
|
|
|
|
|
|
|
|
|
|
| 240 |
}
|
| 241 |
+
.mini-waveform .bar {
|
| 242 |
+
flex: 1; background: var(--text-faint); border-radius: 1px;
|
| 243 |
+
transition: background 0.2s;
|
|
|
|
| 244 |
}
|
| 245 |
+
.song-card.playing .mini-waveform .bar { background: var(--accent); }
|
| 246 |
+
.song-card:hover .mini-waveform .bar { background: rgba(255, 255, 255, 0.22); }
|
| 247 |
|
| 248 |
/* ββ Spinner ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ */
|
| 249 |
.spinner {
|
| 250 |
+
display: inline-block; width: 12px; height: 12px;
|
| 251 |
+
border: 2px solid rgba(0, 0, 0, 0.12); border-top-color: oklch(0.13 0.006 260);
|
| 252 |
+
border-radius: 50%; animation: spin 0.7s linear infinite; vertical-align: middle;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 253 |
}
|
| 254 |
+
@keyframes spin { to { transform: rotate(360deg); } }
|
| 255 |
|
| 256 |
+
/* ββ Footer βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ */
|
| 257 |
+
footer { padding: 20px; text-align: center; font-size: 11px; color: var(--text-faint); }
|
| 258 |
+
footer a { color: var(--text-muted); text-decoration: none; }
|
| 259 |
+
footer a:hover { color: var(--text); }
|
| 260 |
|
| 261 |
/* ββ Mobile βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ */
|
| 262 |
+
.share-label-short { display: none; }
|
| 263 |
+
@media (max-width: 800px) {
|
| 264 |
+
.page { flex-direction: column; padding: 20px 16px 0; gap: 16px; }
|
| 265 |
+
.col-left, .col-right { max-width: none; width: 100%; }
|
| 266 |
+
.song-card-top .song-tags { display: none; }
|
| 267 |
+
.controls-row { gap: 5px; }
|
| 268 |
+
.seed-input { width: 60px !important; }
|
| 269 |
+
.share-label-full { display: none; }
|
| 270 |
+
.share-label-short { display: inline; }
|
| 271 |
}
|
| 272 |
</style>
|
| 273 |
</head>
|
| 274 |
<body>
|
| 275 |
|
| 276 |
+
<div class="page">
|
| 277 |
|
| 278 |
+
<!-- ββββ LEFT: Create ββββ -->
|
| 279 |
+
<div class="col-left">
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 280 |
|
| 281 |
+
<header>
|
| 282 |
+
<div class="wordmark">
|
| 283 |
+
<svg class="wordmark-logo" viewBox="0 0 1357 390" fill="none" xmlns="http://www.w3.org/2000/svg"><path fill="currentColor" d="M429.554 121.056c25.685-2.701 56.59 8.361 65.05 35.015 6.4 20.237-1.839 45.271-23.479 51.909-10.535 3.243-26.295 1.729-29.56-10.892-1.465-7.657 4.81-15.286 7.43-22.246 2.915-7.759 2.54-20.084-5.83-24.234-12.865-6.234-25.751 10.698-30.446 20.741-11.515 24.623-18.194 66.318-9.654 92.045 15.96 48.099 79.485 21.441 93.715-13.469 1.85-4.537.905-13.069 1.885-18.054 4.24-45.958 30.43-90.954 75.74-106.514 34.455-11.835 78.87-.084 91.26 37.146 4.43 13.542 2.515 29.945-3.84 42.61-14.065 26.676-46.055 36.676-72.945 45.039-6.48 2.225-18.736 5.131-24.076 7.796q.107 1.334.326 2.656c5.275 31.502 38.935 36.362 64.115 25.899 23.91-9.935 38.415-24.933 53.5-45.365 5.055-5.088 11.455-3.266 11.695 4.253.285 5.284-2.34 10.701-4.915 15.32-17.715 31.762-47.551 61.527-83.131 71.447-37.425 10.435-79.034-3.97-97.779-38.678-1.09-2.018-3.645-8.147-5.085-9.36-3.25 2.494-10.835 11.736-14.61 15.398-18.56 17.995-38.876 31.53-64.996 34.715a81.2 81.2 0 0 1-60.209-16.81c-17.921-14.293-27.46-36.077-29.707-58.502-6.195-61.85 28.272-131.158 95.546-137.865m181.456 70.616c5.125-13.489 7.57-43.481-14.165-42.209-24.88 6.91-34.32 59.526-33.31 81.763 5.11-1.34 8.85-2.705 13.775-4.704 15.59-7.135 27.52-18.594 33.7-34.85"/><path fill="currentColor" d="M252.566 31.328c13.489.15 28.638-1.107 40.747 5.829 14.378 8.235 13.358 26.043 14.32 40.355l2.323 33.361 7.744 101.628c1.854 26.297 3.277 52.889 7.327 78.938 1.466 9.43 5.535 20.237 13.799 25.7 2.658 1.75 6.034 4.031 6.681 7.386 2.157 11.18-15.813 8.85-23.014 8.805l-27.869-.166q-14.718-.014-29.435.166c-5.378.055-19.803 1.139-23.314-1.986-6.732-6 9.234-15.974 10.313-24.989 2.273-19.001.231-40.745-1.328-59.509-7.726-2.792-16.639-4.369-24.684-6.138-13.149-2.89-26.221-5.506-39.576-7.317-26.129 49.575-50.203 122.044-116.567 125.219-30.024-.73-51.661-17.32-43.553-49.833 2.48-9.949 16.708-26.894 26.499-15.203 3.194 3.815.678 17.184 3.565 22.031 5.484 10.919 20.75 10.089 29.943 5.274 26.071-13.664 41.425-42.9 54.985-67.806 3.709-6.846 8.069-15.671 10.602-22.698q-2.555-.012-5.109.052c-19.691.564-40.684 9.808-55.07 23.351-4.01 3.776-11.3 15.404-16.445 15.651-6.447.31-8.223-6.662-8.054-12.163.398-13.022 6.724-26.658 16.043-35.764 23.204-21.692 54.274-24.541 84.477-24.073 13.373-27.212 25.61-57.864 37.877-85.805 6.466-14.728 13.866-31.519 18.076-47.027 1.006-3.702.114-6.804-.832-10.394-4.37-4.675-25.115-13.02-5.264-19.847 6.059-2.084 17.892-2.365 24.562-2.782 3.149-.28 7.046-.282 10.231-.246m-11.933 74.358c-2.056 2.836-4.866 10.582-6.357 14.227l-12.001 29.498c-7.084 17.143-15.606 35.261-22.304 52.253 4.745.982 9.285 1.633 14.067 2.339 10.719 1.694 23.779 4.18 34.381 4.855-.456-12.059-4.748-99.339-7.786-103.172M751.392 71.823c-.34-9.52 19.635-7.66 25.921-7.67l26.78-.04c12.231-.03 25.492-.374 37.717.53 1.879.316 5.482 1.365 6.766 2.88 6.272 7.415-6.264 12.926-9.123 19.746-2.577 6.15-3.914 8.925-4.523 15.455-1.488 15.925-1.144 31.855-1.169 47.79l-.324 68.059c-.121 34.375 1.189 67.59-24.984 94.135-36.711 37.23-117.356 35.785-141.56-15.145-4.124-8.675-4.715-18.09-4.655-27.495a47.1 47.1 0 0 1 10.969-26.95c12.532-14.62 37.444-18.11 52.064-4.965a27.12 27.12 0 0 1 8.988 19.43c.401 14.01-6.404 17.145-14.45 25.775-12.126 13.465 3.685 27.82 18.108 25.8 34.753-4.875 30.803-53.81 30.832-79.675l.022-51.27-.083-45.14c-.062-14.645 1.538-35.665-6.982-47.905-2.659-3.745-10.142-8.515-10.314-13.345m195.843-9.245c12.179.095 24.785 2.39 33.048 12.225 8.54 10.165 15.095 43.36 18.885 57.315a4784 4784 0 0 0 33.182 114.901c4.95 16.344 9.74 35.189 16.79 50.634 2.01 4.405 6.36 8.5 10.52 11.01 10.66-9.845 9.61-34.86 9.84-48.38.27-16.265.11-32.69.37-48.98.38-21.295.57-42.59.58-63.89.14-16.01 1.85-34.3-1.62-50.005-1.39-6.29-3.63-11.715-8.4-16.22-1.79-1.68-3.86-3.26-5.09-5.405-.93-1.605-1.16-3.165-.56-4.955.73-2.195 2.16-3.65 4.24-4.62 2.31-1.075 4.81-1.395 7.32-1.61 9.48-.82 64.09-1.63 70.82 1.13 3.59 1.476 7.11 3.465 9.04 6.985 4.07 7.45 7.19 22.705 9.93 31.675 6.89 22.14 13.89 44.245 21.01 66.305 1.7 5.266 9.36 32.903 12.28 34.155 3.93-2.421 13.66-31.838 16.01-38.289l22.7-63.086c2.97-8.45 8.02-25.404 12.63-32.664 6.54-10.29 38.43-5.77 50.05-6.66 7.74-.595 17.31.319 25.1.439 1.84.03 5.8 1.49 7.28 2.775 6.32 6.505-8.06 15.936-10.21 24.03-4.25 16.02-4 33.681-4.07 50.001-.21 46.014-.95 91.855.43 137.839.23 7.98.77 17.025 3.85 24.4 1.8 5.35 11.31 10.78 11.29 16.415-.04 8.814-19.17 6.56-23.92 6.53l-24.93-.05c-11.57.01-23.48.55-34.81.225-13.23-.37-10.68-8.34-3.9-15.13 6.19-6.2 7.38-12.235 8.82-20.59 1.49-12.225 1.48-27.015 1.52-39.31l.08-43.34c.01-4.711.82-35.563-1.29-36.875-3.11 1.92-5.99 12.945-7.48 16.735-2.81 7.115-5.56 14.42-8.26 21.59l-28.89 77.345c-3.79 10.19-7.2 20.715-11.81 30.535-4.6 9.945-21.68 7.62-26.25-.93-6.24-11.665-10.05-27.12-14.24-39.845l-25.75-78.195c-1.55-4.689-8.26-28.116-11.64-28.965-1.32 1.15-1.96 4.495-1.99 6.17-.75 34.63-.79 69.4-.67 104.045.03 11.2 1.09 22.405 9.76 30.435 1.59 1.63 5.91 5.895 6.31 8.04 2.11 11.404-22.14 8.28-29.52 8.205l-35.05-.27-61.452-.04c-7.77.02-15.615.57-23.372.37-2.238-.06-6.194.01-8.107-1.214-1.381-.885-2.384-2.471-2.7-4.071-.959-4.86 5.28-9.225 7.673-12.98 6.088-9.545.193-28.165-2.33-38.38-.971-3.935-2.246-9.53-6.087-11.63-6.617-3.62-51.072-4.499-59.105-2.185-2.513.725-3.717 2.395-4.872 4.63-4.31 8.35-10.534 31.55-9.323 40.77.252 1.92 1.094 3.74 2.121 5.365 2.362 3.735 7.774 6.501 8.939 10.775.49 1.8.036 3.635-.991 5.16-1.309 1.939-2.962 2.64-5.191 3.11-7.781 1.635-27.562.18-36.554.245-6.542.045-13.637 1.1-20.092.515-2.462-.225-5.183-.915-7.202-2.4-1.51-1.11-2.693-2.71-2.759-4.645-.174-5.1 6.625-9.225 9.585-12.93 3.854-4.82 6.477-10.545 8.774-16.23 13.148-32.555 23.034-66.76 34.557-99.94l20.711-60.995c3.423-10.25 7.955-21.985 9.888-32.605 3.471-8.28-9.124-18.955-9.128-22.945-.009-9.225 38.877-11.295 43.682-11.575m-9.769 79.41c-2.886 3.83-24.901 73.496-24.944 78.31 3.007 2.44 15.374 1.64 19.785 1.575 4.813 0 21.568 1.005 24.674-1.604.802-.676.618-3.005.565-3.965-.305-5.51-15.823-71.054-17.593-72.785-.732-.715-1.577-1.106-2.487-1.531"/></svg>
|
| 284 |
+
<a href="https://huggingface.co/ACE-Step/acestep-v15-xl-turbo" target="_blank" class="version">ACE-Step v1.5</a>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 285 |
</div>
|
| 286 |
+
<p>Describe any song. AI writes & produces it.</p>
|
| 287 |
+
</header>
|
| 288 |
+
|
| 289 |
+
<div class="compose-box">
|
| 290 |
+
<textarea id="description-input"
|
| 291 |
+
placeholder="A chill lo-fi track for studying, ninja battle anthem, love song about AI..."
|
| 292 |
+
></textarea>
|
| 293 |
+
<div class="compose-controls">
|
| 294 |
+
<label class="pill">
|
| 295 |
+
<select id="duration-select">
|
| 296 |
+
<option value="30">30s</option>
|
| 297 |
+
<option value="60" selected>1 min</option>
|
| 298 |
+
<option value="120">2 min</option>
|
| 299 |
+
<option value="180">3 min</option>
|
| 300 |
+
</select>
|
| 301 |
+
</label>
|
| 302 |
+
<label class="pill"><input type="checkbox" id="instrumental-check" /><span>Instrumental</span></label>
|
| 303 |
+
<label class="pill"><input type="checkbox" id="community-check" checked /><span class="share-label-full">Share in community</span><span class="share-label-short">Share</span></label>
|
| 304 |
+
<span class="spacer"></span>
|
| 305 |
+
<button class="generate-btn" id="generate-btn">Generate</button>
|
| 306 |
</div>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 307 |
</div>
|
| 308 |
+
<div class="status-bar" id="status-bar"></div>
|
| 309 |
+
|
| 310 |
+
<!-- Output -->
|
| 311 |
+
<div id="output-section" style="display:none">
|
| 312 |
+
<div class="output-card">
|
| 313 |
+
<div style="display:flex;gap:10px;margin-bottom:8px;">
|
| 314 |
+
<img id="song-thumb" style="width:48px;height:48px;border-radius:6px;object-fit:cover;display:none;" />
|
| 315 |
+
<div style="min-width:0;flex:1;">
|
| 316 |
+
<div id="song-title" style="font-size:13px;font-weight:600;margin-bottom:3px;"></div>
|
| 317 |
+
<div id="song-tags" style="font-size:11px;color:var(--text-muted);"></div>
|
| 318 |
+
</div>
|
| 319 |
+
</div>
|
| 320 |
+
<div id="waveform"></div>
|
| 321 |
+
<div class="playback-controls">
|
| 322 |
+
<button class="play-btn" id="play-btn" title="Play / Pause">βΆ</button>
|
| 323 |
+
<span class="time-display" id="time-display">0:00 / 0:00</span>
|
| 324 |
+
<a class="download-btn" id="download-btn" download="output.wav">β¬ Download</a>
|
| 325 |
+
</div>
|
| 326 |
</div>
|
| 327 |
</div>
|
| 328 |
+
|
| 329 |
+
</div>
|
| 330 |
+
|
| 331 |
+
<!-- ββββ RIGHT: Community feed ββββ -->
|
| 332 |
+
<div class="col-right">
|
| 333 |
+
<div class="feed-header">Community Songs</div>
|
| 334 |
+
<div id="feed"></div>
|
| 335 |
</div>
|
| 336 |
|
| 337 |
+
</div>
|
| 338 |
|
| 339 |
<footer>
|
| 340 |
+
Powered by <a href="https://github.com/ace-step/ACE-Step" target="_blank">ACE-Step</a> Β·
|
| 341 |
+
<a href="https://huggingface.co/ACE-Step/acestep-v15-xl-turbo" target="_blank">Model card</a>
|
|
|
|
|
|
|
|
|
|
| 342 |
</footer>
|
| 343 |
|
| 344 |
<script type="module">
|
| 345 |
+
// ββ ZeroGPU auth ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
|
|
|
|
|
|
| 346 |
const ZEROGPU_HEADERS_MSG = "supports-zerogpu-headers";
|
| 347 |
+
window.addEventListener("message", (e) => { if (e.data === ZEROGPU_HEADERS_MSG) window.supports_zerogpu_headers = true; });
|
|
|
|
|
|
|
|
|
|
|
|
|
| 348 |
const _hn = window.location.hostname;
|
| 349 |
if (_hn.endsWith(".hf.space") || _hn.includes(".dev.")) {
|
| 350 |
+
const _o = _hn.includes(".dev.") ? `https://moon-${_hn.split(".")[1]}.dev.spaces.huggingface.tech` : "https://huggingface.co";
|
| 351 |
+
window.parent.postMessage(ZEROGPU_HEADERS_MSG, _o);
|
|
|
|
|
|
|
| 352 |
}
|
|
|
|
| 353 |
|
| 354 |
import { Client } from "https://cdn.jsdelivr.net/npm/@gradio/client@1.15.2/dist/index.min.js";
|
| 355 |
+
// WaveSurfer removed β using lightweight bar waveform with real audio peaks
|
| 356 |
|
| 357 |
+
// ββ DOM ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 358 |
+
const descInput = document.getElementById("description-input");
|
| 359 |
+
const durationSelect = document.getElementById("duration-select");
|
| 360 |
+
const instrumentalChk = document.getElementById("instrumental-check");
|
| 361 |
+
const communityChk = document.getElementById("community-check");
|
| 362 |
const generateBtn = document.getElementById("generate-btn");
|
|
|
|
| 363 |
const statusBar = document.getElementById("status-bar");
|
| 364 |
const outputSection = document.getElementById("output-section");
|
| 365 |
+
const songTitleEl = document.getElementById("song-title");
|
| 366 |
+
const songTagsEl = document.getElementById("song-tags");
|
| 367 |
const playBtn = document.getElementById("play-btn");
|
| 368 |
const timeDisplay = document.getElementById("time-display");
|
| 369 |
const downloadBtn = document.getElementById("download-btn");
|
| 370 |
+
const feedEl = document.getElementById("feed");
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 371 |
|
| 372 |
+
// ββ Main player (plain Audio + bar waveform) βββββββββββββββββββββββββββββ
|
| 373 |
+
const mainAudio = new Audio();
|
| 374 |
+
const waveformEl = document.getElementById("waveform");
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 375 |
|
| 376 |
+
function fmt(s) { return `${Math.floor(s/60)}:${Math.floor(s%60).toString().padStart(2,"0")}`; }
|
| 377 |
function updateTime() {
|
| 378 |
+
const c = mainAudio.currentTime || 0, d = mainAudio.duration || 0;
|
| 379 |
+
timeDisplay.textContent = `${fmt(c)} / ${fmt(d)}`;
|
| 380 |
+
}
|
| 381 |
+
function updateMainProgress() {
|
| 382 |
+
if (mainAudio.paused && mainAudio.currentTime === 0) return;
|
| 383 |
+
const pct = mainAudio.duration ? mainAudio.currentTime / mainAudio.duration : 0;
|
| 384 |
+
waveformEl.querySelectorAll(".bar").forEach((b, i, arr) => {
|
| 385 |
+
b.style.background = (i / arr.length) <= pct ? "var(--accent)" : "";
|
| 386 |
+
});
|
| 387 |
+
updateTime();
|
| 388 |
+
if (!mainAudio.paused) requestAnimationFrame(updateMainProgress);
|
| 389 |
+
}
|
| 390 |
+
mainAudio.addEventListener("play", () => { playBtn.textContent = "βΈ"; requestAnimationFrame(updateMainProgress); });
|
| 391 |
+
mainAudio.addEventListener("pause", () => { playBtn.textContent = "βΆ"; });
|
| 392 |
+
mainAudio.addEventListener("ended", () => { playBtn.textContent = "βΆ"; waveformEl.querySelectorAll(".bar").forEach(b => b.style.background = ""); });
|
| 393 |
+
mainAudio.addEventListener("timeupdate", updateTime);
|
| 394 |
+
|
| 395 |
+
playBtn.addEventListener("click", () => { stopFeedAudio(); mainAudio.paused ? mainAudio.play() : mainAudio.pause(); });
|
| 396 |
+
|
| 397 |
+
waveformEl.addEventListener("click", (e) => {
|
| 398 |
+
const rect = waveformEl.getBoundingClientRect();
|
| 399 |
+
const ratio = Math.max(0, Math.min(1, (e.clientX - rect.left) / rect.width));
|
| 400 |
+
if (mainAudio.duration) {
|
| 401 |
+
stopFeedAudio();
|
| 402 |
+
mainAudio.currentTime = mainAudio.duration * ratio;
|
| 403 |
+
if (mainAudio.paused) mainAudio.play();
|
| 404 |
+
requestAnimationFrame(updateMainProgress);
|
| 405 |
+
}
|
| 406 |
+
});
|
| 407 |
|
| 408 |
+
// ββ Gradio client βββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 409 |
let gradioClient = null;
|
|
|
|
| 410 |
async function getClient() {
|
| 411 |
+
if (!gradioClient) gradioClient = await Client.connect(window.location.origin);
|
|
|
|
|
|
|
| 412 |
return gradioClient;
|
| 413 |
}
|
| 414 |
|
| 415 |
+
// ββ UI helpers ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 416 |
+
function setLoading(on) {
|
| 417 |
+
generateBtn.disabled = on;
|
| 418 |
+
if (on) { generateBtn.innerHTML = '<span class="spinner"></span> Creatingβ¦'; generateBtn.classList.add("loading"); }
|
| 419 |
+
else { generateBtn.textContent = "Generate"; generateBtn.classList.remove("loading"); }
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 420 |
}
|
|
|
|
| 421 |
function setStatus(msg, type = "") {
|
| 422 |
statusBar.textContent = msg;
|
| 423 |
statusBar.className = "status-bar" + (type ? ` ${type}` : "");
|
| 424 |
}
|
| 425 |
|
| 426 |
+
async function loadAudio(dataUrl) {
|
| 427 |
+
const res = await fetch(dataUrl);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 428 |
const blob = await res.blob();
|
| 429 |
+
const url = URL.createObjectURL(blob);
|
| 430 |
|
| 431 |
+
// Extract real peaks for bar waveform
|
| 432 |
+
const numBars = 80;
|
| 433 |
+
try {
|
| 434 |
+
const buf = await blob.arrayBuffer();
|
| 435 |
+
const ctx = new (window.AudioContext || window.webkitAudioContext)();
|
| 436 |
+
const decoded = await ctx.decodeAudioData(buf);
|
| 437 |
+
const raw = decoded.getChannelData(0);
|
| 438 |
+
const block = Math.floor(raw.length / numBars);
|
| 439 |
+
const peaks = [];
|
| 440 |
+
for (let i = 0; i < numBars; i++) {
|
| 441 |
+
let sum = 0;
|
| 442 |
+
for (let j = 0; j < block; j++) sum += Math.abs(raw[i * block + j]);
|
| 443 |
+
peaks.push(sum / block);
|
| 444 |
+
}
|
| 445 |
+
const mx = Math.max(...peaks);
|
| 446 |
+
waveformEl.innerHTML = peaks.map(p => {
|
| 447 |
+
const h = mx > 0 ? 15 + (p / mx) * 85 : 50;
|
| 448 |
+
return `<div class="bar" style="height:${h}%"></div>`;
|
| 449 |
+
}).join("");
|
| 450 |
+
} catch {
|
| 451 |
+
waveformEl.innerHTML = Array.from({length: numBars}, () =>
|
| 452 |
+
`<div class="bar" style="height:${15 + Math.random() * 85}%"></div>`
|
| 453 |
+
).join("");
|
| 454 |
+
}
|
| 455 |
+
|
| 456 |
+
mainAudio.src = url;
|
| 457 |
+
downloadBtn.href = url;
|
| 458 |
outputSection.style.display = "block";
|
| 459 |
+
stopFeedAudio();
|
| 460 |
+
mainAudio.play();
|
| 461 |
}
|
| 462 |
|
| 463 |
+
// ββ Community feed player βββββββββββββββββββββββββββββββββββββββββββββββ
|
| 464 |
+
let feedAudio = null;
|
| 465 |
+
let playingCard = null;
|
|
|
|
| 466 |
|
| 467 |
+
function stopFeedAudio() {
|
| 468 |
+
if (feedAudio) { feedAudio.pause(); feedAudio = null; }
|
| 469 |
+
if (playingCard) {
|
| 470 |
+
playingCard.querySelectorAll(".mini-waveform .bar").forEach(b => b.style.background = "");
|
| 471 |
+
playingCard.classList.remove("playing");
|
| 472 |
+
playingCard.querySelector(".song-play-btn").textContent = "βΆ";
|
| 473 |
+
playingCard = null;
|
| 474 |
}
|
| 475 |
+
}
|
| 476 |
+
|
| 477 |
+
function playFeedSong(card, audioUrl, seekRatio) {
|
| 478 |
+
// If same card and already playing, just seek (or pause if clicking play btn)
|
| 479 |
+
if (playingCard === card && feedAudio) {
|
| 480 |
+
if (seekRatio != null) {
|
| 481 |
+
feedAudio.currentTime = feedAudio.duration * seekRatio;
|
| 482 |
+
if (feedAudio.paused) feedAudio.play();
|
| 483 |
+
} else {
|
| 484 |
+
// Toggle play/pause (clicked the play button)
|
| 485 |
+
if (feedAudio.paused) { feedAudio.play(); }
|
| 486 |
+
else { feedAudio.pause(); }
|
| 487 |
+
}
|
| 488 |
return;
|
| 489 |
}
|
| 490 |
+
stopFeedAudio();
|
| 491 |
+
mainAudio.pause();
|
| 492 |
+
feedAudio = new Audio(audioUrl);
|
| 493 |
+
playingCard = card;
|
| 494 |
+
card.classList.add("playing");
|
| 495 |
+
card.querySelector(".song-play-btn").textContent = "βΈ";
|
| 496 |
+
feedAudio.play();
|
| 497 |
+
if (seekRatio != null) {
|
| 498 |
+
feedAudio.addEventListener("loadedmetadata", () => {
|
| 499 |
+
feedAudio.currentTime = feedAudio.duration * seekRatio;
|
| 500 |
+
}, { once: true });
|
| 501 |
+
}
|
| 502 |
+
// Update waveform progress during playback
|
| 503 |
+
const waveEl = card.querySelector(".mini-waveform");
|
| 504 |
+
const bars = waveEl ? waveEl.querySelectorAll(".bar") : [];
|
| 505 |
+
function updateProgress() {
|
| 506 |
+
if (playingCard !== card || !feedAudio || feedAudio.paused) return;
|
| 507 |
+
const pct = feedAudio.currentTime / feedAudio.duration;
|
| 508 |
+
bars.forEach((b, i) => {
|
| 509 |
+
b.style.background = (i / bars.length) <= pct ? "var(--accent)" : "var(--text-faint)";
|
| 510 |
+
});
|
| 511 |
+
requestAnimationFrame(updateProgress);
|
| 512 |
+
}
|
| 513 |
+
if (bars.length) requestAnimationFrame(updateProgress);
|
| 514 |
+
feedAudio.addEventListener("ended", () => {
|
| 515 |
+
bars.forEach(b => b.style.background = "");
|
| 516 |
+
stopFeedAudio();
|
| 517 |
+
});
|
| 518 |
+
feedAudio.addEventListener("error", () => stopFeedAudio());
|
| 519 |
+
feedAudio.addEventListener("pause", () => {
|
| 520 |
+
if (playingCard === card) card.querySelector(".song-play-btn").textContent = "βΆ";
|
| 521 |
+
});
|
| 522 |
+
feedAudio.addEventListener("play", () => {
|
| 523 |
+
if (playingCard === card) {
|
| 524 |
+
card.querySelector(".song-play-btn").textContent = "βΈ";
|
| 525 |
+
requestAnimationFrame(updateProgress);
|
| 526 |
+
}
|
| 527 |
+
});
|
| 528 |
+
}
|
| 529 |
|
| 530 |
+
// ββ Load community feed βββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 531 |
+
async function loadFeed() {
|
|
|
|
| 532 |
try {
|
| 533 |
const client = await getClient();
|
| 534 |
+
const res = await client.predict("/community");
|
| 535 |
+
// Gradio JS client may return {data: [...]} or the value directly
|
| 536 |
+
const raw = res?.data?.[0] ?? res?.data ?? res;
|
| 537 |
+
const songs = JSON.parse(typeof raw === "string" ? raw : JSON.stringify(raw) || "[]");
|
| 538 |
+
if (songs.length === 0) {
|
| 539 |
+
feedEl.innerHTML = '<div class="feed-empty">No community songs yet.<br>Be the first β check "Share" and generate!</div>';
|
| 540 |
+
return;
|
| 541 |
+
}
|
| 542 |
+
feedEl.innerHTML = "";
|
| 543 |
+
for (const song of songs) {
|
| 544 |
+
const card = document.createElement("div");
|
| 545 |
+
card.className = "song-card";
|
| 546 |
+
const dur = song.duration ? `${Math.round(song.duration)}s` : "";
|
| 547 |
+
// Generate random mini waveform bars (seeded from title for consistency)
|
| 548 |
+
const miniBars = Array.from({length: 80}, () =>
|
| 549 |
+
`<div class="bar" style="height:${15 + Math.random() * 85}%"></div>`
|
| 550 |
+
).join("");
|
| 551 |
+
const thumbHtml = song.thumb_url
|
| 552 |
+
? `<img src="${esc(song.thumb_url)}" style="width:24px;height:24px;border-radius:4px;object-fit:cover;flex-shrink:0;" loading="lazy" />`
|
| 553 |
+
: '';
|
| 554 |
+
card.innerHTML = `
|
| 555 |
+
<div class="song-card-top">
|
| 556 |
+
<button class="song-play-btn">βΆ</button>
|
| 557 |
+
${thumbHtml}
|
| 558 |
+
<div class="song-title">${esc(song.title || "Untitled")}</div>
|
| 559 |
+
<div class="song-tags">${esc(song.tags || "")}</div>
|
| 560 |
+
<div class="song-duration">${dur}</div>
|
| 561 |
+
</div>
|
| 562 |
+
<div class="mini-waveform">${miniBars}</div>
|
| 563 |
+
`;
|
| 564 |
+
// Play button: toggle play/pause
|
| 565 |
+
card.querySelector(".song-play-btn").addEventListener("click", (e) => {
|
| 566 |
+
e.stopPropagation();
|
| 567 |
+
playFeedSong(card, song.audio_url);
|
| 568 |
+
});
|
| 569 |
+
// Waveform: seek to click position
|
| 570 |
+
card.querySelector(".mini-waveform").addEventListener("click", (e) => {
|
| 571 |
+
e.stopPropagation();
|
| 572 |
+
const rect = e.currentTarget.getBoundingClientRect();
|
| 573 |
+
const ratio = Math.max(0, Math.min(1, (e.clientX - rect.left) / rect.width));
|
| 574 |
+
playFeedSong(card, song.audio_url, ratio);
|
| 575 |
+
});
|
| 576 |
+
feedEl.appendChild(card);
|
| 577 |
+
}
|
| 578 |
+
} catch (e) {
|
| 579 |
+
console.error("Feed load error:", e);
|
| 580 |
+
feedEl.innerHTML = '<div class="feed-empty">Could not load community songs</div>';
|
| 581 |
+
}
|
| 582 |
+
}
|
| 583 |
+
|
| 584 |
+
function esc(s) { const d = document.createElement("div"); d.textContent = s; return d.innerHTML; }
|
| 585 |
+
|
| 586 |
+
// ββ Generate βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 587 |
+
generateBtn.addEventListener("click", async () => {
|
| 588 |
+
let desc = descInput.value.trim();
|
| 589 |
+
if (!desc) { setStatus("Describe the song you want to create", "error"); descInput.focus(); return; }
|
| 590 |
+
if (instrumentalChk.checked) desc += " (instrumental, no vocals)";
|
| 591 |
+
|
| 592 |
+
stopFeedAudio();
|
| 593 |
+
setLoading(true);
|
| 594 |
+
setStatus("");
|
| 595 |
+
|
| 596 |
+
// Show animated placeholder on the left, above output
|
| 597 |
+
outputSection.style.display = "none";
|
| 598 |
+
const placeholder = document.createElement("div");
|
| 599 |
+
placeholder.className = "placeholder-card";
|
| 600 |
+
placeholder.id = "gen-placeholder";
|
| 601 |
+
const bars = Array.from({length: 40}, (_, i) =>
|
| 602 |
+
`<div class="bar" style="height:100%;animation-delay:${(i * 0.06).toFixed(2)}s"></div>`
|
| 603 |
+
).join("");
|
| 604 |
+
placeholder.innerHTML = `
|
| 605 |
+
<div class="placeholder-title">${esc(desc.slice(0, 80))}</div>
|
| 606 |
+
<div class="placeholder-sub">Composing & generatingβ¦</div>
|
| 607 |
+
<div class="fake-waveform">${bars}</div>
|
| 608 |
+
`;
|
| 609 |
+
outputSection.parentNode.insertBefore(placeholder, outputSection);
|
| 610 |
|
| 611 |
+
try {
|
| 612 |
+
const client = await getClient();
|
| 613 |
+
const job = client.submit("/create", {
|
| 614 |
+
description: desc,
|
| 615 |
+
audio_duration: parseFloat(durationSelect.value),
|
| 616 |
+
seed: -1,
|
| 617 |
+
community: communityChk.checked,
|
|
|
|
|
|
|
| 618 |
});
|
| 619 |
|
| 620 |
for await (const msg of job) {
|
| 621 |
if (msg.type === "status") {
|
| 622 |
+
if (msg.queue_size > 0) {
|
| 623 |
+
const eta = msg.eta != null ? ` Β· ~${Math.round(msg.eta)}s` : "";
|
| 624 |
+
setStatus(`Queue ${msg.position}/${msg.queue_size}${eta}`);
|
|
|
|
|
|
|
|
|
|
| 625 |
}
|
| 626 |
} else if (msg.type === "data") {
|
| 627 |
+
const raw = msg.data?.[0];
|
| 628 |
+
if (!raw) throw new Error("No data returned");
|
| 629 |
+
const result = JSON.parse(raw);
|
| 630 |
+
songTitleEl.textContent = result.title || "";
|
| 631 |
+
songTagsEl.textContent = result.tags || "";
|
| 632 |
+
const thumbEl = document.getElementById("song-thumb");
|
| 633 |
+
if (result.thumbnail) { thumbEl.src = result.thumbnail; thumbEl.style.display = "block"; }
|
| 634 |
+
else { thumbEl.style.display = "none"; }
|
| 635 |
+
const safeName = (result.title || "output").replace(/[^a-zA-Z0-9 -]/g, "").trim().replace(/\s+/g, "-");
|
| 636 |
+
downloadBtn.download = `${safeName}.wav`;
|
| 637 |
+
await loadAudio(result.audio);
|
| 638 |
+
// Refresh feed if shared
|
| 639 |
+
if (communityChk.checked) loadFeed();
|
| 640 |
}
|
| 641 |
}
|
| 642 |
} catch (err) {
|
| 643 |
+
console.error(err);
|
| 644 |
setStatus(`Error: ${err.message}`, "error");
|
| 645 |
} finally {
|
| 646 |
+
placeholder.remove();
|
| 647 |
+
setLoading(false);
|
| 648 |
}
|
| 649 |
});
|
| 650 |
|
| 651 |
+
descInput.addEventListener("keydown", (e) => {
|
| 652 |
+
if ((e.metaKey || e.ctrlKey) && e.key === "Enter") { e.preventDefault(); generateBtn.click(); }
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 653 |
});
|
| 654 |
|
| 655 |
+
// ββ Init βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 656 |
+
getClient().then(() => loadFeed()).catch(console.error);
|
|
|
|
| 657 |
</script>
|
| 658 |
|
| 659 |
</body>
|
requirements.txt
CHANGED
|
@@ -2,7 +2,7 @@ torch==2.9.1
|
|
| 2 |
torchaudio==2.9.1
|
| 3 |
torchvision==0.24.1
|
| 4 |
transformers>=4.51.0,<4.58.0
|
| 5 |
-
diffusers
|
| 6 |
gradio==6.12.0
|
| 7 |
matplotlib>=3.7.5
|
| 8 |
scipy>=1.10.1
|
|
|
|
| 2 |
torchaudio==2.9.1
|
| 3 |
torchvision==0.24.1
|
| 4 |
transformers>=4.51.0,<4.58.0
|
| 5 |
+
diffusers>=0.37.0
|
| 6 |
gradio==6.12.0
|
| 7 |
matplotlib>=3.7.5
|
| 8 |
scipy>=1.10.1
|