Nekochu commited on
Commit
2dc2899
·
1 Parent(s): 567a93e

rewrite app.py for ace-server HTTP API, no torch

Browse files
Files changed (1) hide show
  1. app.py +100 -439
app.py CHANGED
@@ -1,461 +1,122 @@
1
- """
2
- ACE-Step 1.5 Music Generation + LoRA Training (CPU)
3
- Runs on HuggingFace Spaces free CPU tier.
4
- """
5
 
6
  import os
7
- import sys
8
- import gc
9
  import time
10
  import tempfile
11
- import shutil
12
- from pathlib import Path
13
-
14
- # Force CPU, no CUDA
15
- os.environ["CUDA_VISIBLE_DEVICES"] = ""
16
- os.environ["TOKENIZERS_PARALLELISM"] = "false"
17
- os.environ["TORCHAUDIO_USE_BACKEND"] = "ffmpeg"
18
- os.environ["ACESTEP_DISABLE_TQDM"] = "1"
19
-
20
- import torch
21
- torch.set_default_dtype(torch.float32)
22
-
23
- import numpy as np
24
  import gradio as gr
25
- import soundfile as sf
26
-
27
- # ---------------------------------------------------------------------------
28
- # Clone ACE-Step repo if not present
29
- # ---------------------------------------------------------------------------
30
- REPO_DIR = os.path.join(os.path.dirname(os.path.abspath(__file__)), "ace-step-source")
31
- if not os.path.isdir(REPO_DIR):
32
- print("[Setup] Cloning ACE-Step 1.5 repository...")
33
- os.system(f"git clone --depth 1 https://github.com/ace-step/ACE-Step-1.5 {REPO_DIR}")
34
-
35
- # Add repo to path
36
- if REPO_DIR not in sys.path:
37
- sys.path.insert(0, REPO_DIR)
38
-
39
- # ---------------------------------------------------------------------------
40
- # Lazy-load handler (downloads model on first use)
41
- # ---------------------------------------------------------------------------
42
- _dit_handler = None
43
- _init_status = None
44
-
45
- CHECKPOINT_DIR = os.path.join(os.path.dirname(os.path.abspath(__file__)), "checkpoints")
46
- LORA_OUTPUT_DIR = os.path.join(os.path.dirname(os.path.abspath(__file__)), "lora_output")
47
- CURRENT_LM_SIZE = "1.7B" # Track current LM size
48
-
49
-
50
- def get_handler():
51
- """Get or initialize the ACE-Step handler (lazy, first call downloads model)."""
52
- global _dit_handler, _init_status
53
-
54
- if _dit_handler is not None and _dit_handler.model is not None:
55
- return _dit_handler, _init_status
56
-
57
- from acestep.handler import AceStepHandler
58
- from acestep.model_downloader import ensure_main_model
59
-
60
- print("[Init] Ensuring model is downloaded...")
61
- success, msg = ensure_main_model(
62
- checkpoints_dir=Path(CHECKPOINT_DIR),
63
- prefer_source="huggingface",
64
- )
65
- print(f"[Init] Model download: {msg}")
66
-
67
- if not success:
68
- _init_status = f"Model download failed: {msg}"
69
- return None, _init_status
70
-
71
- _dit_handler = AceStepHandler()
72
- project_root = os.path.dirname(os.path.abspath(__file__))
73
-
74
- os.environ["ACESTEP_PROJECT_ROOT"] = project_root
75
-
76
- status, ok = _dit_handler.initialize_service(
77
- project_root=project_root,
78
- config_path="acestep-v15-turbo",
79
- device="cpu",
80
- use_flash_attention=False,
81
- compile_model=False,
82
- offload_to_cpu=False,
83
- offload_dit_to_cpu=False,
84
- quantization=None,
85
- use_mlx_dit=False,
86
- )
87
-
88
- _init_status = status
89
- if not ok:
90
- print(f"[Init] FAILED: {status}")
91
- _dit_handler = None
92
- return None, _init_status
93
-
94
- # Force float32 on everything
95
- _dit_handler.dtype = torch.float32
96
- if _dit_handler.model is not None:
97
- _dit_handler.model = _dit_handler.model.float().to("cpu")
98
- if _dit_handler.vae is not None:
99
- _dit_handler.vae = _dit_handler.vae.float().to("cpu")
100
- if _dit_handler.text_encoder is not None:
101
- _dit_handler.text_encoder = _dit_handler.text_encoder.float().to("cpu")
102
 
103
- print(f"[Init] OK: {status}")
104
- return _dit_handler, _init_status
105
-
106
-
107
- def get_trained_loras():
108
- """List available trained LoRAs."""
109
- loras = ["None (no LoRA)"]
110
- if os.path.isdir(LORA_OUTPUT_DIR):
111
- for name in sorted(os.listdir(LORA_OUTPUT_DIR)):
112
- lora_dir = os.path.join(LORA_OUTPUT_DIR, name)
113
- if os.path.isdir(lora_dir):
114
- # Check for any .safetensors or .pt files
115
- for f in os.listdir(lora_dir):
116
- if f.endswith((".safetensors", ".pt", ".bin")):
117
- loras.append(name)
118
- break
119
- return loras
120
 
 
 
 
 
 
121
 
122
- # ---------------------------------------------------------------------------
123
- # Generate Tab
124
- # ---------------------------------------------------------------------------
125
- def generate_music(
126
- caption,
127
- lyrics,
128
- instrumental,
129
- bpm,
130
- duration,
131
- seed,
132
- inference_steps,
133
- lm_size,
134
- lora_choice,
135
- progress=gr.Progress(track_tqdm=True),
136
- ):
137
- """Generate music from text prompt on CPU."""
138
  t0 = time.time()
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
139
 
140
- handler, status = get_handler()
141
- if handler is None:
142
- return None, f"Model not ready: {status}"
143
-
144
- # Apply trained LoRA if selected
145
- if lora_choice and lora_choice != "None (no LoRA)":
146
- lora_dir = os.path.join(LORA_OUTPUT_DIR, lora_choice)
147
- if os.path.isdir(lora_dir):
148
- try:
149
- handler.load_lora(lora_dir)
150
- print(f"[Gen] Loaded LoRA: {lora_choice}")
151
- except Exception as e:
152
- print(f"[Gen] LoRA load failed: {e}")
153
-
154
- # TODO: LM size switching requires re-downloading the LM model
155
- # For now, log the selected size
156
- if lm_size != CURRENT_LM_SIZE:
157
- print(f"[Gen] LM size {lm_size} requested (current: {CURRENT_LM_SIZE})")
158
 
159
- # Clamp values
160
- duration = max(10, min(float(duration), 120)) # cap at 120s for CPU
161
- inference_steps = max(1, min(int(inference_steps), 32))
162
- bpm_val = int(bpm) if bpm and int(bpm) > 0 else None
163
- seed_val = int(seed) if seed and int(seed) >= 0 else -1
164
 
165
  try:
166
- result = handler.generate_music(
167
- captions=caption or "upbeat electronic dance music",
168
- lyrics=lyrics or "[Instrumental]",
169
- bpm=bpm_val,
170
- audio_duration=duration,
171
- inference_steps=inference_steps,
172
- guidance_scale=1.0, # turbo model, no CFG needed
173
- use_random_seed=(seed_val < 0),
174
- seed=str(seed_val) if seed_val >= 0 else "",
175
- batch_size=1,
176
- task_type="text2music",
177
- vocal_language="en",
178
- shift=1.0,
179
- infer_method="ode",
180
- progress=None,
181
- )
182
-
183
- elapsed = time.time() - t0
184
-
185
- if not result.get("success", False):
186
- error = result.get("error", result.get("status_message", "Unknown error"))
187
- return None, f"Generation failed: {error}"
188
-
189
- audios = result.get("audios", [])
190
- if not audios:
191
- return None, "No audio generated"
192
-
193
- audio_tensor = audios[0].get("tensor")
194
- sample_rate = audios[0].get("sample_rate", 48000)
195
-
196
- if audio_tensor is None:
197
- return None, "Audio tensor is None"
198
-
199
- # Convert to numpy
200
- if isinstance(audio_tensor, torch.Tensor):
201
- audio_np = audio_tensor.cpu().float().numpy()
202
- else:
203
- audio_np = np.array(audio_tensor, dtype=np.float32)
204
-
205
- # Save to temp file
206
- tmp = tempfile.NamedTemporaryFile(suffix=".wav", delete=False)
207
- # soundfile expects (samples, channels)
208
- if audio_np.ndim == 2:
209
- audio_np = audio_np.T # (channels, samples) -> (samples, channels)
210
- sf.write(tmp.name, audio_np, sample_rate)
211
-
212
- status_msg = (
213
- f"Generated in {elapsed:.1f}s | "
214
- f"Duration: {duration}s | Steps: {inference_steps} | "
215
- f"Seed: {seed_val}"
216
- )
217
- return tmp.name, status_msg
218
-
219
  except Exception as e:
220
- import traceback
221
- return None, f"Error: {e}\n{traceback.format_exc()}"
222
- finally:
223
- gc.collect()
224
-
225
 
226
- # ---------------------------------------------------------------------------
227
- # Train LoRA Tab
228
- # ---------------------------------------------------------------------------
229
- def train_lora(
230
- audio_files,
231
- lora_name,
232
- epochs,
233
- learning_rate,
234
- lora_rank,
235
- progress=gr.Progress(track_tqdm=True),
236
- ):
237
- """Train a LoRA adapter from uploaded audio files on CPU."""
238
- if not audio_files:
239
- return "No audio files uploaded."
240
-
241
- handler, status = get_handler()
242
- if handler is None:
243
- return f"Model not ready: {status}"
244
-
245
- lora_name = lora_name.strip() or "my_lora"
246
- epochs = max(1, min(int(epochs), 10))
247
- lr = float(learning_rate)
248
- rank = max(1, min(int(lora_rank), 64))
249
-
250
- output_dir = os.path.join(
251
- os.path.dirname(os.path.abspath(__file__)), "lora_output", lora_name
252
- )
253
- os.makedirs(output_dir, exist_ok=True)
254
-
255
- # Create a temp directory for audio files
256
- audio_dir = os.path.join(output_dir, "audio_input")
257
- os.makedirs(audio_dir, exist_ok=True)
258
-
259
- # Copy uploaded files
260
- for f in audio_files:
261
- src = f.name if hasattr(f, "name") else str(f)
262
- dst = os.path.join(audio_dir, os.path.basename(src))
263
- shutil.copy2(src, dst)
264
-
265
- log_lines = []
266
- log_lines.append(f"LoRA Training: '{lora_name}'")
267
- log_lines.append(f"Audio files: {len(audio_files)}")
268
- log_lines.append(f"Epochs: {epochs}, LR: {lr}, Rank: {rank}")
269
- log_lines.append(f"Output: {output_dir}")
270
- log_lines.append("")
271
 
272
  try:
273
- # Preprocessing step: encode audio files to tensors
274
- log_lines.append("[Step 1/2] Preprocessing audio files...")
275
-
276
- tensor_dir = os.path.join(output_dir, "preprocessed_tensors")
277
- os.makedirs(tensor_dir, exist_ok=True)
278
-
279
- from acestep.training_v2.preprocess import preprocess_audio_files
280
-
281
- preprocess_result = preprocess_audio_files(
282
- audio_dir=audio_dir,
283
- output_dir=tensor_dir,
284
- checkpoint_dir=CHECKPOINT_DIR,
285
- variant="turbo",
286
- max_duration=60.0,
287
- device="cpu",
288
- precision="float32",
289
- )
290
-
291
- processed = preprocess_result.get("processed", 0)
292
- total = preprocess_result.get("total", 0)
293
- failed = preprocess_result.get("failed", 0)
294
- log_lines.append(f" Preprocessed: {processed}/{total} (failed: {failed})")
295
-
296
- if processed == 0:
297
- log_lines.append("ERROR: No files were preprocessed successfully.")
298
- return "\n".join(log_lines)
299
-
300
- # Training step
301
- log_lines.append("[Step 2/2] Training LoRA adapter...")
302
-
303
- from acestep.training_v2.model_loader import load_decoder_for_training
304
- from acestep.training_v2.trainer_fixed import FixedLoRATrainer
305
- from acestep.training_v2.configs import TrainingConfigV2, LoRAConfigV2
306
-
307
- # Load model for training (force float32 for CPU)
308
- model = load_decoder_for_training(
309
- checkpoint_dir=CHECKPOINT_DIR,
310
- variant="turbo",
311
- device="cpu",
312
- precision="float32",
313
- )
314
- model = model.float()
315
-
316
- adapter_cfg = LoRAConfigV2(
317
- r=rank,
318
- alpha=rank,
319
- dropout=0.0,
320
- )
321
-
322
- train_cfg = TrainingConfigV2(
323
- checkpoint_dir=CHECKPOINT_DIR,
324
- model_variant="turbo",
325
- dataset_dir=tensor_dir,
326
- output_dir=output_dir,
327
- max_epochs=epochs,
328
- batch_size=1,
329
- learning_rate=lr,
330
- device="cpu",
331
- precision="float32",
332
- seed=42,
333
- num_workers=0,
334
- pin_memory=False,
335
- )
336
-
337
- trainer = FixedLoRATrainer(model, adapter_cfg, train_cfg)
338
-
339
- step_count = 0
340
- last_loss = 0.0
341
- for update in trainer.train():
342
- if hasattr(update, "step"):
343
- step_count = update.step
344
- last_loss = update.loss
345
- if step_count % 5 == 0:
346
- log_lines.append(f" Step {step_count}: loss={last_loss:.4f}")
347
- elif isinstance(update, tuple) and len(update) >= 2:
348
- step_count = update[0]
349
- last_loss = update[1]
350
- if step_count % 5 == 0:
351
- log_lines.append(f" Step {step_count}: loss={last_loss:.4f}")
352
-
353
- log_lines.append(f"Training complete! Final step: {step_count}, loss: {last_loss:.4f}")
354
- log_lines.append(f"LoRA saved to: {output_dir}")
355
-
356
- # Cleanup
357
- del model, trainer
358
- gc.collect()
359
-
360
  except Exception as e:
361
- import traceback
362
- log_lines.append(f"ERROR: {e}")
363
- log_lines.append(traceback.format_exc())
364
-
365
- return "\n".join(log_lines)
366
-
367
 
368
- # ---------------------------------------------------------------------------
369
- # Gradio UI
370
- # ---------------------------------------------------------------------------
371
- def build_ui():
372
- theme = gr.themes.Default()
373
  try:
374
- theme = gr.Theme.from_hub("NoCrypt/miku")
375
- except Exception:
376
- pass
377
-
378
- with gr.Blocks(
379
- theme=theme,
380
- title="ACE-Step 1.5 CPU",
381
- css="""
382
- .main-title { text-align: center; margin-bottom: 0.5em; }
383
- .status-box { font-family: monospace; font-size: 0.85em; }
384
- """,
385
- ) as demo:
386
- gr.Markdown("**[ACE-Step 1.5 (CPU)](https://github.com/ace-step/ACE-Step-1.5)**")
387
-
388
- with gr.Tabs():
389
- # ---- Generate Tab ----
390
- with gr.Tab("Generate Music"):
391
- with gr.Row():
392
- with gr.Column(scale=2):
393
- caption_input = gr.Textbox(
394
- label="Music Description",
395
- placeholder="e.g. upbeat electronic dance music, 120 BPM",
396
- lines=2,
397
- value="upbeat electronic dance music, energetic synth leads, driving bassline",
398
- )
399
- lyrics_input = gr.Textbox(
400
- label="Lyrics ([Instrumental] for no vocals)",
401
- lines=2,
402
- value="[Instrumental]",
403
- )
404
- with gr.Column(scale=1):
405
- audio_output = gr.Audio(label="Output", type="filepath")
406
- gen_status = gr.Textbox(label="Status", interactive=False, lines=1, elem_classes="status-box")
407
- with gr.Row():
408
- instrumental_cb = gr.Checkbox(label="Instrumental", value=True, scale=1)
409
- bpm_input = gr.Number(label="BPM", value=120, minimum=0, maximum=300, scale=1)
410
- duration_input = gr.Slider(label="Duration (s)", minimum=10, maximum=120, value=10, step=5, scale=1)
411
- steps_input = gr.Slider(label="Steps", minimum=1, maximum=32, value=8, step=1, scale=1)
412
- with gr.Row():
413
- seed_input = gr.Number(label="Seed", value=-1, scale=1)
414
- lm_size_input = gr.Dropdown(label="LM Size", choices=["0.6B (fast)", "1.7B (balanced)", "4B (best quality)"], value="4B (best quality)", scale=1)
415
- lora_select = gr.Dropdown(label="LoRA", choices=get_trained_loras(), value="None (no LoRA)", scale=1)
416
- generate_btn = gr.Button("Generate Music", variant="primary")
417
-
418
- generate_btn.click(
419
- fn=generate_music,
420
- inputs=[caption_input, lyrics_input, instrumental_cb, bpm_input, duration_input, seed_input, steps_input, lm_size_input, lora_select],
421
- outputs=[audio_output, gen_status],
422
- )
423
-
424
- # ---- Train LoRA Tab ----
425
- with gr.Tab("Train LoRA"):
426
- gr.Markdown("Upload audio files to train a LoRA adapter. Training on CPU, keep epochs low.")
427
- with gr.Row():
428
- audio_upload = gr.File(label="Audio Files", file_count="multiple", file_types=["audio"], scale=2)
429
- with gr.Column(scale=1):
430
- lora_name_input = gr.Textbox(label="LoRA Name", value="my_lora")
431
- train_model_info = gr.Textbox(label="Training Model", value="acestep-v15-turbo (DiT decoder)", interactive=False)
432
- with gr.Row():
433
- epochs_input = gr.Slider(label="Epochs", minimum=1, maximum=10, value=1, step=1, scale=1)
434
- lr_input = gr.Number(label="LR", value=1e-4, scale=1)
435
- rank_input = gr.Slider(label="LoRA Rank", minimum=1, maximum=64, value=8, step=1, scale=1)
436
- train_btn = gr.Button("Start Training", variant="primary")
437
- train_log = gr.Textbox(label="Training Log", interactive=False, lines=10, elem_classes="status-box")
438
-
439
- def train_and_refresh(*args):
440
- log = train_lora(*args)
441
- new_loras = get_trained_loras()
442
- return log, gr.update(choices=new_loras, value=new_loras[-1] if len(new_loras) > 1 else "None (no LoRA)")
443
-
444
- train_btn.click(
445
- fn=train_and_refresh,
446
- inputs=[audio_upload, lora_name_input, epochs_input, lr_input, rank_input],
447
- outputs=[train_log, lora_select],
448
- )
449
-
450
- return demo
451
-
452
 
453
- if __name__ == "__main__":
454
- demo = build_ui()
455
- demo.launch(
456
- server_name="0.0.0.0",
457
- server_port=7860,
458
- show_error=True,
459
- ssr_mode=False,
460
- )
461
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """ACE-Step 1.5 XL (CPU) - Gradio frontend for ace-server GGUF inference"""
 
 
 
2
 
3
  import os
 
 
4
  import time
5
  import tempfile
6
+ import requests
 
 
 
 
 
 
 
 
 
 
 
 
7
  import gradio as gr
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
8
 
9
+ ACE_SERVER = "http://127.0.0.1:8085"
10
+ OUTPUT_DIR = "/app/outputs"
11
+ os.makedirs(OUTPUT_DIR, exist_ok=True)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
12
 
13
+ def _server_ok():
14
+ try:
15
+ return requests.get(f"{ACE_SERVER}/health", timeout=5).status_code == 200
16
+ except Exception:
17
+ return False
18
 
19
+ def _poll_job(job_id, timeout=600):
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
20
  t0 = time.time()
21
+ while time.time() - t0 < timeout:
22
+ try:
23
+ r = requests.get(f"{ACE_SERVER}/job", params={"id": job_id}, timeout=10)
24
+ status = r.json().get("status", "unknown")
25
+ if status in ("done", "error"):
26
+ return status, time.time() - t0
27
+ except Exception:
28
+ pass
29
+ time.sleep(2)
30
+ return "timeout", time.time() - t0
31
+
32
+ def generate_music(caption, lyrics, instrumental, bpm, duration, seed, steps, progress=gr.Progress(track_tqdm=True)):
33
+ t0 = time.time()
34
+ if not _server_ok():
35
+ return None, "ace-server not running"
36
 
37
+ req = {"caption": caption or "upbeat electronic dance music"}
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
38
 
39
+ if instrumental or not lyrics or lyrics.strip() == "":
40
+ req["lyrics"] = "[Instrumental]"
41
+ else:
42
+ req["lyrics"] = lyrics
 
43
 
44
  try:
45
+ if bpm and int(bpm) > 0: req["bpm"] = int(bpm)
46
+ if duration and float(duration) > 0: req["duration"] = min(float(duration), 300)
47
+ if seed is not None and int(seed) >= 0: req["seed"] = int(seed)
48
+ if steps and int(steps) > 0: req["inference_steps"] = int(steps)
49
+ except (ValueError, TypeError) as e:
50
+ return None, f"Bad param: {e}"
51
+
52
+ progress(0.05, desc="Submitting LM job...")
53
+ try:
54
+ r = requests.post(f"{ACE_SERVER}/lm", json=req, timeout=30)
55
+ if r.status_code != 200:
56
+ return None, f"LM failed: {r.status_code} {r.text}"
57
+ lm_job_id = r.json().get("id")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
58
  except Exception as e:
59
+ return None, f"LM error: {e}"
 
 
 
 
60
 
61
+ progress(0.1, desc=f"LM generating (job {lm_job_id})...")
62
+ lm_status, lm_elapsed = _poll_job(lm_job_id, timeout=300)
63
+ if lm_status != "done":
64
+ return None, f"LM {lm_status} after {lm_elapsed:.0f}s"
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
65
 
66
  try:
67
+ r = requests.get(f"{ACE_SERVER}/job", params={"id": lm_job_id, "result": 1}, timeout=30)
68
+ lm_results = r.json()
69
+ if not isinstance(lm_results, list) or len(lm_results) == 0:
70
+ return None, f"LM no results: {lm_results}"
71
+ synth_request = lm_results[0]
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
72
  except Exception as e:
73
+ return None, f"LM result error: {e}"
 
 
 
 
 
74
 
75
+ progress(0.4, desc="Submitting synth job...")
76
+ synth_request["output_format"] = "wav16"
 
 
 
77
  try:
78
+ r = requests.post(f"{ACE_SERVER}/synth", json=synth_request, timeout=30)
79
+ if r.status_code != 200:
80
+ return None, f"Synth failed: {r.status_code} {r.text}"
81
+ synth_job_id = r.json().get("id")
82
+ except Exception as e:
83
+ return None, f"Synth error: {e}"
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
84
 
85
+ progress(0.5, desc=f"Synthesizing (job {synth_job_id})...")
86
+ synth_status, synth_elapsed = _poll_job(synth_job_id, timeout=600)
87
+ if synth_status != "done":
88
+ return None, f"Synth {synth_status} after {synth_elapsed:.0f}s"
 
 
 
 
89
 
90
+ progress(0.9, desc="Fetching audio...")
91
+ try:
92
+ r = requests.get(f"{ACE_SERVER}/job", params={"id": synth_job_id, "result": 1}, timeout=60)
93
+ if r.status_code != 200:
94
+ return None, f"Audio fetch failed: {r.status_code}"
95
+ tmp = tempfile.NamedTemporaryFile(suffix=".wav", dir=OUTPUT_DIR, delete=False)
96
+ tmp.write(r.content)
97
+ tmp.close()
98
+ except Exception as e:
99
+ return None, f"Audio error: {e}"
100
+
101
+ elapsed = time.time() - t0
102
+ return tmp.name, f"Done in {elapsed:.0f}s | {duration}s audio, {steps} steps"
103
+
104
+ with gr.Blocks(title="ACE-Step 1.5 XL (CPU)") as demo:
105
+ gr.Markdown("**[ACE-Step 1.5 XL (CPU)](https://github.com/ace-step/ACE-Step-1.5)** GGUF Q4_K_M via [acestep.cpp](https://github.com/ServeurpersoCom/acestep.cpp)")
106
+ with gr.Row():
107
+ with gr.Column(scale=2):
108
+ caption = gr.Textbox(label="Music Description", lines=2, value="upbeat electronic dance music, energetic synth leads")
109
+ lyrics = gr.Textbox(label="Lyrics ([Instrumental] for no vocals)", lines=2, value="[Instrumental]")
110
+ with gr.Column(scale=1):
111
+ audio_out = gr.Audio(label="Output", type="filepath")
112
+ status = gr.Textbox(label="Status", interactive=False, lines=1)
113
+ with gr.Row():
114
+ instrumental = gr.Checkbox(label="Instrumental", value=True, scale=1)
115
+ bpm = gr.Number(label="BPM", value=120, minimum=0, maximum=300, scale=1)
116
+ duration = gr.Slider(label="Duration (s)", minimum=10, maximum=120, value=10, step=5, scale=1)
117
+ steps = gr.Slider(label="Steps", minimum=1, maximum=32, value=8, step=1, scale=1)
118
+ seed = gr.Number(label="Seed", value=-1, scale=1)
119
+ gen_btn = gr.Button("Generate Music", variant="primary")
120
+ gen_btn.click(fn=generate_music, inputs=[caption, lyrics, instrumental, bpm, duration, seed, steps], outputs=[audio_out, status])
121
+
122
+ demo.launch(server_name="0.0.0.0", server_port=7860)