Opera8 commited on
Commit
ff865c5
·
verified ·
1 Parent(s): 7465c26

Update acestep/resume_utils.py

Browse files
Files changed (1) hide show
  1. acestep/resume_utils.py +307 -1254
acestep/resume_utils.py CHANGED
@@ -1,1272 +1,325 @@
1
  """
2
- ACE-Step Inference API Module
3
-
4
- This module provides a standardized inference interface for music generation,
5
- designed for third-party integration. It offers both a simplified API and
6
- backward-compatible Gradio UI support.
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
7
  """
8
 
9
- import math
10
  import os
11
- import tempfile
12
- from typing import Optional, Union, List, Dict, Any, Tuple
13
- from dataclasses import dataclass, field, asdict
 
 
 
 
 
14
  from loguru import logger
15
 
16
- from acestep.audio_utils import AudioSaver, generate_uuid_from_params
17
- from acestep.resume_utils import (
18
- generate_task_id,
19
- DEFAULT_TIMEOUT_SECONDS,
20
- save_lm_audio_codes,
21
- load_lm_audio_codes,
22
- )
23
-
24
- # HuggingFace Space environment detection
25
- IS_HUGGINGFACE_SPACE = os.environ.get("SPACE_ID") is not None
26
-
27
-
28
- @dataclass
29
- class GenerationParams:
30
- """Configuration for music generation parameters.
31
-
32
- Attributes:
33
- # Text Inputs
34
- caption: A short text prompt describing the desired music (main prompt). < 512 characters
35
- lyrics: Lyrics for the music. Use "[Instrumental]" for instrumental songs. < 4096 characters
36
- instrumental: If True, generate instrumental music regardless of lyrics.
37
-
38
- # Music Metadata
39
- bpm: BPM (beats per minute), e.g., 120. Set to None for automatic estimation. 30 ~ 300
40
- keyscale: Musical key (e.g., "C Major", "Am"). Leave empty for auto-detection. A-G, #/♭, major/minor
41
- timesignature: Time signature (2 for '2/4', 3 for '3/4', 4 for '4/4', 6 for '6/8'). Leave empty for auto-detection.
42
- vocal_language: Language code for vocals, e.g., "en", "zh", "ja", or "unknown". see acestep/constants.py:VALID_LANGUAGES
43
- duration: Target audio length in seconds. If <0 or None, model chooses automatically. 10 ~ 600
44
-
45
- # Generation Parameters
46
- inference_steps: Number of diffusion steps (e.g., 8 for turbo, 32–100 for base model).
47
- guidance_scale: CFG (classifier-free guidance) strength. Higher means following the prompt more strictly. Only support for non-turbo model.
48
- seed: Integer seed for reproducibility. -1 means use random seed each time.
49
-
50
- # Advanced DiT Parameters
51
- use_adg: Whether to use Adaptive Dual Guidance (only works for base model).
52
- cfg_interval_start: Start ratio (0.0–1.0) to apply CFG.
53
- cfg_interval_end: End ratio (0.0–1.0) to apply CFG.
54
- shift: Timestep shift factor (default 1.0). When != 1.0, applies t = shift * t / (1 + (shift - 1) * t) to timesteps.
55
-
56
- # Task-Specific Parameters
57
- task_type: Type of generation task. One of: "text2music", "cover", "repaint", "lego", "extract", "complete".
58
- reference_audio: Path to a reference audio file for style transfer or cover tasks.
59
- src_audio: Path to a source audio file for audio-to-audio tasks.
60
- audio_codes: Audio semantic codes as a string (advanced use, for code-control generation).
61
- repainting_start: For repaint/lego tasks: start time in seconds for region to repaint.
62
- repainting_end: For repaint/lego tasks: end time in seconds for region to repaint (-1 for until end).
63
- audio_cover_strength: Strength of reference audio/codes influence (range 0.0–1.0). set smaller (0.2) for style transfer tasks.
64
- instruction: Optional task instruction prompt. If empty, auto-generated by system.
65
-
66
- # 5Hz Language Model Parameters for CoT reasoning
67
- thinking: If True, enable 5Hz Language Model "Chain-of-Thought" reasoning for semantic/music metadata and codes.
68
- lm_temperature: Sampling temperature for the LLM (0.0–2.0). Higher = more creative/varied results.
69
- lm_cfg_scale: Classifier-free guidance scale for the LLM.
70
- lm_top_k: LLM top-k sampling (0 = disabled).
71
- lm_top_p: LLM top-p nucleus sampling (1.0 = disabled).
72
- lm_negative_prompt: Negative prompt to use for LLM (for control).
73
- use_cot_metas: Whether to let LLM generate music metadata via CoT reasoning.
74
- use_cot_caption: Whether to let LLM rewrite or format the input caption via CoT reasoning.
75
- use_cot_language: Whether to let LLM detect vocal language via CoT.
76
- """
77
- # Required Inputs
78
- task_type: str = "text2music"
79
- instruction: str = "Fill the audio semantic mask based on the given conditions:"
80
-
81
- # Audio Uploads
82
- reference_audio: Optional[str] = None
83
- src_audio: Optional[str] = None
84
-
85
- # LM Codes Hints
86
- audio_codes: str = ""
87
-
88
- # Text Inputs
89
- caption: str = ""
90
- lyrics: str = ""
91
- instrumental: bool = False
92
-
93
- # Metadata
94
- vocal_language: str = "unknown"
95
- bpm: Optional[int] = None
96
- keyscale: str = ""
97
- timesignature: str = ""
98
- duration: float = -1.0
99
-
100
- # Advanced Settings
101
- inference_steps: int = 8
102
- seed: int = -1
103
- guidance_scale: float = 7.0
104
- use_adg: bool = False
105
- cfg_interval_start: float = 0.0
106
- cfg_interval_end: float = 1.0
107
- shift: float = 1.0
108
- infer_method: str = "ode" # "ode" or "sde" - diffusion inference method
109
- # Custom timesteps (parsed from string like "0.97,0.76,0.615,0.5,0.395,0.28,0.18,0.085,0")
110
- # If provided, overrides inference_steps and shift
111
- timesteps: Optional[List[float]] = None
112
-
113
- repainting_start: float = 0.0
114
- repainting_end: float = -1
115
- audio_cover_strength: float = 1.0
116
-
117
- # 5Hz Language Model Parameters
118
- thinking: bool = True
119
- lm_temperature: float = 0.85
120
- lm_cfg_scale: float = 2.0
121
- lm_top_k: int = 0
122
- lm_top_p: float = 0.9
123
- lm_negative_prompt: str = "NO USER INPUT"
124
- use_cot_metas: bool = True
125
- use_cot_caption: bool = True
126
- use_cot_lyrics: bool = False # TODO: not used yet
127
- use_cot_language: bool = True
128
- use_constrained_decoding: bool = True
129
-
130
- cot_bpm: Optional[int] = None
131
- cot_keyscale: str = ""
132
- cot_timesignature: str = ""
133
- cot_duration: Optional[float] = None
134
- cot_vocal_language: str = "unknown"
135
- cot_caption: str = ""
136
- cot_lyrics: str = ""
137
-
138
- def to_dict(self) -> Dict[str, Any]:
139
- """Convert config to dictionary for JSON serialization."""
140
- return asdict(self)
141
-
142
-
143
- @dataclass
144
- class GenerationConfig:
145
- """Configuration for music generation.
146
-
147
- Attributes:
148
- batch_size: Number of audio samples to generate
149
- allow_lm_batch: Whether to allow batch processing in LM
150
- use_random_seed: Whether to use random seed
151
- seeds: Seed(s) for batch generation. Can be:
152
- - None: Use random seeds (when use_random_seed=True) or params.seed (when use_random_seed=False)
153
- - List[int]: List of seeds, will be padded with random seeds if fewer than batch_size
154
- - int: Single seed value (will be converted to list and padded)
155
- lm_batch_chunk_size: Batch chunk size for LM processing
156
- constrained_decoding_debug: Whether to enable constrained decoding debug
157
- audio_format: Output audio format, one of "mp3", "wav", "flac". Default: "flac"
158
- """
159
- batch_size: int = 2
160
- allow_lm_batch: bool = False
161
- use_random_seed: bool = True
162
- seeds: Optional[List[int]] = None
163
- lm_batch_chunk_size: int = 8
164
- constrained_decoding_debug: bool = False
165
- audio_format: str = "flac" # Default to FLAC for fast saving
166
-
167
- def to_dict(self) -> Dict[str, Any]:
168
- """Convert config to dictionary for JSON serialization."""
169
- return asdict(self)
170
-
171
-
172
- @dataclass
173
- class GenerationResult:
174
- """Result of music generation.
175
-
176
- Attributes:
177
- # Audio Outputs
178
- audios: List of audio dictionaries with paths, keys, params
179
- status_message: Status message from generation
180
- extra_outputs: Extra outputs from generation
181
- success: Whether generation completed successfully
182
- error: Error message if generation failed
183
- """
184
 
185
- # Audio Outputs
186
- audios: List[Dict[str, Any]] = field(default_factory=list)
187
- # Generation Information
188
- status_message: str = ""
189
- extra_outputs: Dict[str, Any] = field(default_factory=dict)
190
- # Success Status
191
- success: bool = True
192
- error: Optional[str] = None
193
-
194
- def to_dict(self) -> Dict[str, Any]:
195
- """Convert result to dictionary for JSON serialization."""
196
- return asdict(self)
197
-
198
-
199
- @dataclass
200
- class UnderstandResult:
201
- """Result of music understanding from audio codes.
202
-
203
- Attributes:
204
- # Metadata Fields
205
- caption: Generated caption describing the music
206
- lyrics: Generated or extracted lyrics
207
- bpm: Beats per minute (None if not detected)
208
- duration: Duration in seconds (None if not detected)
209
- keyscale: Musical key (e.g., "C Major")
210
- language: Vocal language code (e.g., "en", "zh")
211
- timesignature: Time signature (e.g., "4/4")
212
-
213
- # Status
214
- status_message: Status message from understanding
215
- success: Whether understanding completed successfully
216
- error: Error message if understanding failed
217
- """
218
- # Metadata Fields
219
- caption: str = ""
220
- lyrics: str = ""
221
- bpm: Optional[int] = None
222
- duration: Optional[float] = None
223
- keyscale: str = ""
224
- language: str = ""
225
- timesignature: str = ""
226
-
227
- # Status
228
- status_message: str = ""
229
- success: bool = True
230
- error: Optional[str] = None
231
-
232
- def to_dict(self) -> Dict[str, Any]:
233
- """Convert result to dictionary for JSON serialization."""
234
- return asdict(self)
235
-
236
-
237
- def _update_metadata_from_lm(
238
- metadata: Dict[str, Any],
239
- bpm: Optional[int],
240
- key_scale: str,
241
- time_signature: str,
242
- audio_duration: Optional[float],
243
- vocal_language: str,
244
- caption: str,
245
- lyrics: str,
246
- ) -> Tuple[Optional[int], str, str, Optional[float]]:
247
- """Update metadata fields from LM output if not provided by user."""
248
-
249
- if bpm is None and metadata.get('bpm'):
250
- bpm_value = metadata.get('bpm')
251
- if bpm_value not in ["N/A", ""]:
252
- try:
253
- bpm = int(bpm_value)
254
- except (ValueError, TypeError):
255
- pass
256
-
257
- if not key_scale and metadata.get('keyscale'):
258
- key_scale_value = metadata.get('keyscale', metadata.get('key_scale', ""))
259
- if key_scale_value != "N/A":
260
- key_scale = key_scale_value
261
-
262
- if not time_signature and metadata.get('timesignature'):
263
- time_signature_value = metadata.get('timesignature', metadata.get('time_signature', ""))
264
- if time_signature_value != "N/A":
265
- time_signature = time_signature_value
266
-
267
- if audio_duration is None or audio_duration <= 0:
268
- audio_duration_value = metadata.get('duration', -1)
269
- if audio_duration_value not in ["N/A", ""]:
270
- try:
271
- audio_duration = float(audio_duration_value)
272
- except (ValueError, TypeError):
273
- pass
274
-
275
- if not vocal_language and metadata.get('vocal_language'):
276
- vocal_language = metadata.get('vocal_language')
277
- if not caption and metadata.get('caption'):
278
- caption = metadata.get('caption')
279
- if not lyrics and metadata.get('lyrics'):
280
- lyrics = metadata.get('lyrics')
281
- return bpm, key_scale, time_signature, audio_duration, vocal_language, caption, lyrics
282
-
283
-
284
- def generate_music(
285
- dit_handler,
286
- llm_handler,
287
- params: GenerationParams,
288
- config: GenerationConfig,
289
- save_dir: Optional[str] = None,
290
- progress=None,
291
- task_id: Optional[str] = None,
292
- timeout: float = DEFAULT_TIMEOUT_SECONDS,
293
- ) -> "GenerationResult":
294
- """Generate music using ACE-Step model with optional LM reasoning.
295
-
296
- Args:
297
- dit_handler: Initialized DiT model handler (AceStepHandler instance)
298
- llm_handler: Initialized LLM handler (LLMHandler instance)
299
- params: Generation parameters (GenerationParams instance)
300
- config: Generation configuration (GenerationConfig instance)
301
- task_id: Optional tracking code for Save & Resume across ZeroGPU's
302
- 120-second hard timeout. Pass None on a fresh request — a new
303
- task_id will be generated automatically and returned in
304
- extra_outputs["task_id"] on every call (whether it finished or
305
- not), so the UI can always show it. Pass the SAME task_id back
306
- in to continue a previously timed-out generation (works across
307
- separate calls today after quota resets, or tomorrow — it just
308
- needs to be the same Space / same persistent disk).
309
- timeout: Seconds of GPU time before the LM phase checkpoints itself
310
- and returns early. Keep a safety margin under ZeroGPU's 120s
311
- hard kill (default 110s leaves 10s for the save + cleanup).
312
-
313
- Returns:
314
- GenerationResult with generated audio files and metadata. If the
315
- LM phase timed out and saved a checkpoint, success=False and
316
- extra_outputs["resume_pending"]=True with extra_outputs["task_id"]
317
- set — show this code to the user so they can resume.
318
  """
319
- # Always have a task_id so we can checkpoint and report it, even if
320
- # the caller didn't think to ask for resume support up front.
321
- if not task_id:
322
- task_id = generate_task_id()
323
 
324
- try:
325
- # Phase 1: LM-based metadata and code generation (if enabled)
326
- audio_code_string_to_use = params.audio_codes
327
- lm_generated_metadata = None
328
- lm_generated_audio_codes_list = []
329
- lm_total_time_costs = {
330
- "phase1_time": 0.0,
331
- "phase2_time": 0.0,
332
- "total_time": 0.0,
333
- }
334
-
335
- # Extract mutable copies of metadata (will be updated by LM if needed)
336
- bpm = params.bpm
337
- key_scale = params.keyscale
338
- time_signature = params.timesignature
339
- audio_duration = params.duration
340
- dit_input_caption = params.caption
341
- dit_input_vocal_language = params.vocal_language
342
- dit_input_lyrics = params.lyrics
343
- # Determine if we need to generate audio codes
344
- # If user has provided audio_codes, we don't need to generate them
345
- # Otherwise, check if we need audio codes (lm_dit mode) or just metas (dit mode)
346
- user_provided_audio_codes = bool(params.audio_codes and str(params.audio_codes).strip())
347
-
348
- # Determine infer_type: use "llm_dit" if we need audio codes, "dit" if only metas needed
349
- # For now, we use "llm_dit" if batch mode or if user hasn't provided codes
350
- # Use "dit" if user has provided codes (only need metas) or if explicitly only need metas
351
- # Note: This logic can be refined based on specific requirements
352
- need_audio_codes = not user_provided_audio_codes
353
-
354
- # Determine if we should use chunk-based LM generation (always use chunks for consistency)
355
- # Determine actual batch size for chunk processing
356
- actual_batch_size = config.batch_size if config.batch_size is not None else 1
357
-
358
- # Prepare seeds for batch generation
359
- # Use config.seed if provided, otherwise fallback to params.seed
360
- # Convert config.seed (None, int, or List[int]) to format that prepare_seeds accepts
361
- seed_for_generation = ""
362
- if config.seeds is not None and len(config.seeds) > 0:
363
- if isinstance(config.seeds, list):
364
- # Convert List[int] to comma-separated string
365
- seed_for_generation = ",".join(str(s) for s in config.seeds)
366
-
367
- # Use dit_handler.prepare_seeds to handle seed list generation and padding
368
- # This will handle all the logic: padding with random seeds if needed, etc.
369
- actual_seed_list, _ = dit_handler.prepare_seeds(actual_batch_size, seed_for_generation, config.use_random_seed)
370
-
371
- # LM-based Chain-of-Thought reasoning
372
- # Skip LM for cover/repaint tasks - these tasks use reference/src audio directly
373
- # and don't need LM to generate audio codes
374
- skip_lm_tasks = {"cover", "repaint"}
375
-
376
- # Determine if we should use LLM
377
- # LLM is needed for:
378
- # 1. thinking=True: generate audio codes via LM
379
- # 2. use_cot_caption=True: enhance/generate caption via CoT
380
- # 3. use_cot_language=True: detect vocal language via CoT
381
- # 4. use_cot_metas=True: fill missing metadata via CoT
382
- need_lm_for_cot = params.use_cot_caption or params.use_cot_language or params.use_cot_metas
383
- use_lm = (params.thinking or need_lm_for_cot) and llm_handler.llm_initialized and params.task_type not in skip_lm_tasks
384
- lm_status = []
385
-
386
- if params.task_type in skip_lm_tasks:
387
- logger.info(f"Skipping LM for task_type='{params.task_type}' - using DiT directly")
388
-
389
- logger.info(f"[generate_music] LLM usage decision: thinking={params.thinking}, "
390
- f"use_cot_caption={params.use_cot_caption}, use_cot_language={params.use_cot_language}, "
391
- f"use_cot_metas={params.use_cot_metas}, need_lm_for_cot={need_lm_for_cot}, "
392
- f"llm_initialized={llm_handler.llm_initialized if llm_handler else False}, use_lm={use_lm}")
393
-
394
- if use_lm:
395
- # ---- Save & Resume: skip straight to DiT if this task_id already
396
- # finished its LM phase in a previous (possibly timed-out) call ----
397
- saved_codes = load_lm_audio_codes(llm_handler.persistent_storage_path, task_id) if task_id else None
398
- if saved_codes is not None:
399
- logger.info(f"[resume] Found completed LM phase for task {task_id}, skipping straight to DiT")
400
- lm_generated_metadata = saved_codes["metadata"]
401
- lm_generated_audio_codes_list = [saved_codes["audio_codes"]]
402
- audio_code_string_to_use = saved_codes["audio_codes"]
403
- bpm, key_scale, time_signature, audio_duration, dit_input_vocal_language, dit_input_caption, dit_input_lyrics = _update_metadata_from_lm(
404
- metadata=lm_generated_metadata,
405
- bpm=bpm,
406
- key_scale=key_scale,
407
- time_signature=time_signature,
408
- audio_duration=audio_duration,
409
- vocal_language=dit_input_vocal_language,
410
- caption=dit_input_caption,
411
- lyrics=dit_input_lyrics)
412
- lm_status.append(f"✅ LM phase restored from tracking code {task_id} (resumed)")
413
- use_lm = False # Skip the LM block below entirely; fall through to Phase 2 (DiT)
414
-
415
- if use_lm:
416
- # Convert sampling parameters - handle None values safely
417
- top_k_value = None if not params.lm_top_k or params.lm_top_k == 0 else int(params.lm_top_k)
418
- top_p_value = None if not params.lm_top_p or params.lm_top_p >= 1.0 else params.lm_top_p
419
-
420
- # Build user_metadata from user-provided values
421
- user_metadata = {}
422
- if bpm is not None:
423
- try:
424
- bpm_value = float(bpm)
425
- if bpm_value > 0:
426
- user_metadata['bpm'] = int(bpm_value)
427
- except (ValueError, TypeError):
428
- pass
429
-
430
- if key_scale and key_scale.strip():
431
- key_scale_clean = key_scale.strip()
432
- if key_scale_clean.lower() not in ["n/a", ""]:
433
- user_metadata['keyscale'] = key_scale_clean
434
-
435
- if time_signature and time_signature.strip():
436
- time_sig_clean = time_signature.strip()
437
- if time_sig_clean.lower() not in ["n/a", ""]:
438
- user_metadata['timesignature'] = time_sig_clean
439
-
440
- if audio_duration is not None:
441
- try:
442
- duration_value = float(audio_duration)
443
- if duration_value > 0:
444
- user_metadata['duration'] = int(duration_value)
445
- except (ValueError, TypeError):
446
- pass
447
-
448
- user_metadata_to_pass = user_metadata if user_metadata else None
449
-
450
- # Determine infer_type based on whether we need audio codes
451
- # - "llm_dit": generates both metas and audio codes (two-phase internally)
452
- # - "dit": generates only metas (single phase)
453
- infer_type = "llm_dit" if need_audio_codes and params.thinking else "dit"
454
-
455
- # Use chunk size from config, or default to batch_size if not set
456
- max_inference_batch_size = int(config.lm_batch_chunk_size) if config.lm_batch_chunk_size > 0 else actual_batch_size
457
- num_chunks = math.ceil(actual_batch_size / max_inference_batch_size)
458
-
459
- # Resume (task_id-based checkpointing) is only supported for a
460
- # single chunk of size 1 — i.e. generating one song at a time,
461
- # which is the normal "free tier" usage pattern this is built for.
462
- resume_eligible = (num_chunks == 1 and actual_batch_size == 1)
463
- if task_id and not resume_eligible:
464
- logger.warning(f"[resume] task_id={task_id} given but batch/chunk size > 1 — resume disabled for this call")
465
-
466
- all_metadata_list = []
467
- all_audio_codes_list = []
468
-
469
- for chunk_idx in range(num_chunks):
470
- chunk_start = chunk_idx * max_inference_batch_size
471
- chunk_end = min(chunk_start + max_inference_batch_size, actual_batch_size)
472
- chunk_size = chunk_end - chunk_start
473
- chunk_seeds = actual_seed_list[chunk_start:chunk_end] if chunk_start < len(actual_seed_list) else None
474
-
475
- logger.info(f"LM chunk {chunk_idx+1}/{num_chunks} (infer_type={infer_type}) "
476
- f"(size: {chunk_size}, seeds: {chunk_seeds})")
477
-
478
- # Use the determined infer_type
479
- # - "llm_dit" will internally run two phases (metas + codes)
480
- # - "dit" will only run phase 1 (metas only)
481
- result = llm_handler.generate_with_stop_condition(
482
- caption=params.caption or "",
483
- lyrics=params.lyrics or "",
484
- infer_type=infer_type,
485
- temperature=params.lm_temperature,
486
- cfg_scale=params.lm_cfg_scale,
487
- negative_prompt=params.lm_negative_prompt,
488
- top_k=top_k_value,
489
- top_p=top_p_value,
490
- target_duration=audio_duration, # Pass duration to limit audio codes generation
491
- user_metadata=user_metadata_to_pass,
492
- use_cot_caption=params.use_cot_caption,
493
- use_cot_language=params.use_cot_language,
494
- use_cot_metas=params.use_cot_metas,
495
- use_constrained_decoding=params.use_constrained_decoding,
496
- constrained_decoding_debug=config.constrained_decoding_debug,
497
- batch_size=chunk_size,
498
- seeds=chunk_seeds,
499
- progress=progress,
500
- task_id=task_id if resume_eligible else None,
501
- timeout=timeout,
502
- )
503
-
504
- # Save & Resume: LM phase ran out of GPU time and checkpointed.
505
- # Surface the tracking code instead of treating this as a hard failure.
506
- error_val = result.get("error") or ""
507
- if isinstance(error_val, str) and error_val.startswith("TIMEOUT_SAVED"):
508
- return GenerationResult(
509
- audios=[],
510
- status_message=(
511
- f"⏱️ زمان رایگان (ZeroGPU) تمام شد، اما پیشرفت ذخیره شد.\n"
512
- f"کد پیگیری شما: {task_id}\n"
513
- f"این کد را ذخیره کنید. بعداً (امروز پس از ریست سهمیه، یا فردا) با همین کد ادامه دهید."
514
- ),
515
- extra_outputs={
516
- "task_id": task_id,
517
- "resume_pending": True,
518
- "resume_phase": result.get("extra_outputs", {}).get("resume_phase"),
519
- "time_costs": result.get("extra_outputs", {}).get("time_costs", {}),
520
- },
521
- success=False,
522
- error=f"TIMEOUT_SAVED:{task_id}",
523
- )
524
-
525
- # Check if LM generation failed
526
- if not result.get("success", False):
527
- error_msg = result.get("error", "Unknown LM error")
528
- lm_status.append(f"❌ LM Error: {error_msg}")
529
- # Return early with error
530
- return GenerationResult(
531
- audios=[],
532
- status_message=f"❌ LM generation failed: {error_msg}",
533
- extra_outputs={},
534
- success=False,
535
- error=error_msg,
536
- )
537
-
538
- # Extract metadata and audio_codes from result dict
539
- if chunk_size > 1:
540
- metadata_list = result.get("metadata", [])
541
- audio_codes_list = result.get("audio_codes", [])
542
- all_metadata_list.extend(metadata_list)
543
- all_audio_codes_list.extend(audio_codes_list)
544
- else:
545
- metadata = result.get("metadata", {})
546
- audio_codes = result.get("audio_codes", "")
547
- all_metadata_list.append(metadata)
548
- all_audio_codes_list.append(audio_codes)
549
-
550
- # Collect time costs from LM extra_outputs
551
- lm_extra = result.get("extra_outputs", {})
552
- lm_chunk_time_costs = lm_extra.get("time_costs", {})
553
- if lm_chunk_time_costs:
554
- # Accumulate time costs from all chunks
555
- for key in ["phase1_time", "phase2_time", "total_time"]:
556
- if key in lm_chunk_time_costs:
557
- lm_total_time_costs[key] += lm_chunk_time_costs[key]
558
-
559
- time_str = ", ".join([f"{k}: {v:.2f}s" for k, v in lm_chunk_time_costs.items()])
560
- lm_status.append(f"✅ LM chunk {chunk_idx+1}: {time_str}")
561
-
562
- lm_generated_metadata = all_metadata_list[0] if all_metadata_list else None
563
- lm_generated_audio_codes_list = all_audio_codes_list
564
-
565
- # Set audio_code_string_to_use based on infer_type
566
- if infer_type == "llm_dit":
567
- # If batch mode, use list; otherwise use single string
568
- if actual_batch_size > 1:
569
- audio_code_string_to_use = all_audio_codes_list
570
- else:
571
- audio_code_string_to_use = all_audio_codes_list[0] if all_audio_codes_list else ""
572
- else:
573
- # For "dit" mode, keep user-provided codes or empty
574
- audio_code_string_to_use = params.audio_codes
575
-
576
- # Update metadata from LM if not provided by user
577
- if lm_generated_metadata:
578
- bpm, key_scale, time_signature, audio_duration, vocal_language, caption, lyrics = _update_metadata_from_lm(
579
- metadata=lm_generated_metadata,
580
- bpm=bpm,
581
- key_scale=key_scale,
582
- time_signature=time_signature,
583
- audio_duration=audio_duration,
584
- vocal_language=dit_input_vocal_language,
585
- caption=dit_input_caption,
586
- lyrics=dit_input_lyrics)
587
- if not params.bpm:
588
- params.cot_bpm = bpm
589
- if not params.keyscale:
590
- params.cot_keyscale = key_scale
591
- if not params.timesignature:
592
- params.cot_timesignature = time_signature
593
- if not params.duration:
594
- params.cot_duration = audio_duration
595
- if not params.vocal_language:
596
- params.cot_vocal_language = vocal_language
597
- if not params.caption:
598
- params.cot_caption = caption
599
- if not params.lyrics:
600
- params.cot_lyrics = lyrics
601
-
602
- # set cot caption and language if needed
603
- if params.use_cot_caption:
604
- dit_input_caption = lm_generated_metadata.get("caption", dit_input_caption)
605
- if params.use_cot_language:
606
- dit_input_vocal_language = lm_generated_metadata.get("vocal_language", dit_input_vocal_language)
607
-
608
- # Phase 2: DiT music generation
609
- # Use seed_for_generation (from config.seed or params.seed) instead of params.seed for actual generation
610
- result = dit_handler.generate_music(
611
- captions=dit_input_caption,
612
- lyrics=dit_input_lyrics,
613
- bpm=bpm,
614
- key_scale=key_scale,
615
- time_signature=time_signature,
616
- vocal_language=dit_input_vocal_language,
617
- inference_steps=params.inference_steps,
618
- guidance_scale=params.guidance_scale,
619
- use_random_seed=config.use_random_seed,
620
- seed=seed_for_generation, # Use config.seed (or params.seed fallback) instead of params.seed directly
621
- reference_audio=params.reference_audio,
622
- audio_duration=audio_duration,
623
- batch_size=config.batch_size if config.batch_size is not None else 1,
624
- src_audio=params.src_audio,
625
- audio_code_string=audio_code_string_to_use,
626
- repainting_start=params.repainting_start,
627
- repainting_end=params.repainting_end,
628
- instruction=params.instruction,
629
- audio_cover_strength=params.audio_cover_strength,
630
- task_type=params.task_type,
631
- use_adg=params.use_adg,
632
- cfg_interval_start=params.cfg_interval_start,
633
- cfg_interval_end=params.cfg_interval_end,
634
- shift=params.shift,
635
- infer_method=params.infer_method,
636
- timesteps=params.timesteps,
637
- progress=progress,
638
- )
639
-
640
- # Check if generation failed
641
- if not result.get("success", False):
642
- return GenerationResult(
643
- audios=[],
644
- status_message=result.get("status_message", ""),
645
- extra_outputs={},
646
- success=False,
647
- error=result.get("error"),
648
- )
649
-
650
- # Extract results from dit_handler.generate_music dict
651
- dit_audios = result.get("audios", [])
652
- status_message = result.get("status_message", "")
653
- dit_extra_outputs = result.get("extra_outputs", {})
654
-
655
- # Use the seed list already prepared above (from config.seed or params.seed fallback)
656
- # actual_seed_list was computed earlier using dit_handler.prepare_seeds
657
- seed_list = actual_seed_list
658
-
659
- # Get base params dictionary
660
- base_params_dict = params.to_dict()
661
-
662
- # Save audio files using AudioSaver (format from config)
663
- audio_format = config.audio_format if config.audio_format else "flac"
664
- audio_saver = AudioSaver(default_format=audio_format)
665
-
666
- # Use handler's temp_dir for saving files
667
- if save_dir is not None:
668
- os.makedirs(save_dir, exist_ok=True)
669
-
670
- # Build audios list for GenerationResult with params and save files
671
- # Audio saving and UUID generation handled here, outside of handler
672
- audios = []
673
- for idx, dit_audio in enumerate(dit_audios):
674
- # Create a copy of params dict for this audio
675
- audio_params = base_params_dict.copy()
676
-
677
- # Update audio-specific values
678
- audio_params["seed"] = seed_list[idx] if idx < len(seed_list) else None
679
-
680
- # Add audio codes if batch mode
681
- if lm_generated_audio_codes_list and idx < len(lm_generated_audio_codes_list):
682
- audio_params["audio_codes"] = lm_generated_audio_codes_list[idx]
683
-
684
- # Get audio tensor and metadata
685
- audio_tensor = dit_audio.get("tensor")
686
- sample_rate = dit_audio.get("sample_rate", 48000)
687
-
688
- # Generate UUID for this audio (moved from handler)
689
- batch_seed = seed_list[idx] if idx < len(seed_list) else seed_list[0] if seed_list else -1
690
- audio_code_str = lm_generated_audio_codes_list[idx] if (
691
- lm_generated_audio_codes_list and idx < len(lm_generated_audio_codes_list)) else audio_code_string_to_use
692
- if isinstance(audio_code_str, list):
693
- audio_code_str = audio_code_str[idx] if idx < len(audio_code_str) else ""
694
-
695
- audio_key = generate_uuid_from_params(audio_params)
696
-
697
- # Save audio file (handled outside handler)
698
- audio_path = None
699
- if audio_tensor is not None and save_dir is not None:
700
- try:
701
- audio_file = os.path.join(save_dir, f"{audio_key}.{audio_format}")
702
- audio_path = audio_saver.save_audio(audio_tensor,
703
- audio_file,
704
- sample_rate=sample_rate,
705
- format=audio_format,
706
- channels_first=True)
707
- except Exception as e:
708
- logger.error(f"[generate_music] Failed to save audio file: {e}")
709
- audio_path = "" # Fallback to empty path
710
-
711
- audio_dict = {
712
- "path": audio_path or "", # File path (saved here, not in handler)
713
- "tensor": audio_tensor, # Audio tensor [channels, samples], CPU, float32
714
- "key": audio_key,
715
- "sample_rate": sample_rate,
716
- "params": audio_params,
717
- }
718
-
719
- audios.append(audio_dict)
720
-
721
- # Merge extra_outputs: include dit_extra_outputs (latents, masks) and add LM metadata
722
- extra_outputs = dit_extra_outputs.copy()
723
- extra_outputs["lm_metadata"] = lm_generated_metadata
724
- extra_outputs["task_id"] = task_id
725
-
726
- # Merge time_costs from both LM and DiT into a unified dictionary
727
- unified_time_costs = {}
728
-
729
- # Add LM time costs (if LM was used)
730
- if use_lm and lm_total_time_costs:
731
- for key, value in lm_total_time_costs.items():
732
- unified_time_costs[f"lm_{key}"] = value
733
-
734
- # Add DiT time costs (if available)
735
- dit_time_costs = dit_extra_outputs.get("time_costs", {})
736
- if dit_time_costs:
737
- for key, value in dit_time_costs.items():
738
- unified_time_costs[f"dit_{key}"] = value
739
-
740
- # Calculate total pipeline time
741
- if unified_time_costs:
742
- lm_total = unified_time_costs.get("lm_total_time", 0.0)
743
- dit_total = unified_time_costs.get("dit_total_time_cost", 0.0)
744
- unified_time_costs["pipeline_total_time"] = lm_total + dit_total
745
-
746
- # Update extra_outputs with unified time_costs
747
- extra_outputs["time_costs"] = unified_time_costs
748
-
749
- if lm_status:
750
- status_message = "\n".join(lm_status) + "\n" + status_message
751
- else:
752
- status_message = status_message
753
-
754
- # Save & Resume: generation finished successfully end-to-end, so the
755
- # checkpoint files for this task_id are no longer needed.
756
- if task_id:
757
- try:
758
- from acestep.resume_utils import save_final_marker, cleanup_task
759
- final_audio_path = audios[0]["path"] if audios else None
760
- save_final_marker(llm_handler.persistent_storage_path, task_id, audio_path=final_audio_path)
761
- cleanup_task(llm_handler.persistent_storage_path, task_id)
762
- except Exception as e:
763
- logger.warning(f"[resume] Failed to clean up completed task {task_id}: {e}")
764
-
765
- # Create and return GenerationResult
766
- return GenerationResult(
767
- audios=audios,
768
- status_message=status_message,
769
- extra_outputs=extra_outputs,
770
- success=True,
771
- error=None,
772
- )
773
 
774
- except Exception as e:
775
- logger.exception("Music generation failed")
776
- return GenerationResult(
777
- audios=[],
778
- status_message=f"Error: {str(e)}",
779
- extra_outputs={},
780
- success=False,
781
- error=str(e),
782
- )
783
-
784
-
785
- def understand_music(
786
- llm_handler,
787
- audio_codes: str,
788
- temperature: float = 0.85,
789
- top_k: Optional[int] = None,
790
- top_p: Optional[float] = None,
791
- repetition_penalty: float = 1.0,
792
- use_constrained_decoding: bool = True,
793
- constrained_decoding_debug: bool = False,
794
- ) -> UnderstandResult:
795
- """Understand music from audio codes using the 5Hz Language Model.
796
-
797
- This function analyzes audio semantic codes and generates metadata about the music,
798
- including caption, lyrics, BPM, duration, key scale, language, and time signature.
799
-
800
- If audio_codes is empty or "NO USER INPUT", the LM will generate a sample example
801
- instead of analyzing existing codes.
802
-
803
- Note: cfg_scale and negative_prompt are not supported in understand mode.
804
-
805
- Args:
806
- llm_handler: Initialized LLM handler (LLMHandler instance)
807
- audio_codes: String of audio code tokens (e.g., "<|audio_code_123|><|audio_code_456|>...")
808
- Use empty string or "NO USER INPUT" to generate a sample example.
809
- temperature: Sampling temperature for generation (0.0-2.0). Higher = more creative.
810
- top_k: Top-K sampling (None or 0 = disabled)
811
- top_p: Top-P (nucleus) sampling (None or 1.0 = disabled)
812
- repetition_penalty: Repetition penalty (1.0 = no penalty)
813
- use_constrained_decoding: Whether to use FSM-based constrained decoding for metadata
814
- constrained_decoding_debug: Whether to enable debug logging for constrained decoding
815
-
816
- Returns:
817
- UnderstandResult with parsed metadata fields and status
818
-
819
- Example:
820
- >>> result = understand_music(llm_handler, audio_codes="<|audio_code_123|>...")
821
- >>> if result.success:
822
- ... print(f"Caption: {result.caption}")
823
- ... print(f"BPM: {result.bpm}")
824
- ... print(f"Lyrics: {result.lyrics}")
825
  """
826
- # Check if LLM is initialized
827
- if not llm_handler.llm_initialized:
828
- return UnderstandResult(
829
- status_message="5Hz LM not initialized. Please initialize it first.",
830
- success=False,
831
- error="LLM not initialized",
832
- )
833
-
834
- # If codes are empty, use "NO USER INPUT" to generate a sample example
835
- if not audio_codes or not audio_codes.strip():
836
- audio_codes = "NO USER INPUT"
837
-
 
 
838
  try:
839
- # Call LLM understanding
840
- metadata, status = llm_handler.understand_audio_from_codes(
841
- audio_codes=audio_codes,
842
- temperature=temperature,
843
- top_k=top_k,
844
- top_p=top_p,
845
- repetition_penalty=repetition_penalty,
846
- use_constrained_decoding=use_constrained_decoding,
847
- constrained_decoding_debug=constrained_decoding_debug,
848
- )
849
-
850
- # Check if LLM returned empty metadata (error case)
851
- if not metadata:
852
- return UnderstandResult(
853
- status_message=status or "Failed to understand audio codes",
854
- success=False,
855
- error=status or "Empty metadata returned",
856
- )
857
-
858
- # Extract and convert fields
859
- caption = metadata.get('caption', '')
860
- lyrics = metadata.get('lyrics', '')
861
- keyscale = metadata.get('keyscale', '')
862
- language = metadata.get('language', metadata.get('vocal_language', ''))
863
- timesignature = metadata.get('timesignature', '')
864
-
865
- # Convert BPM to int
866
- bpm = None
867
- bpm_value = metadata.get('bpm')
868
- if bpm_value is not None and bpm_value != 'N/A' and bpm_value != '':
869
- try:
870
- bpm = int(bpm_value)
871
- except (ValueError, TypeError):
872
- pass
873
-
874
- # Convert duration to float
875
- duration = None
876
- duration_value = metadata.get('duration')
877
- if duration_value is not None and duration_value != 'N/A' and duration_value != '':
878
- try:
879
- duration = float(duration_value)
880
- except (ValueError, TypeError):
881
- pass
882
-
883
- # Clean up N/A values
884
- if keyscale == 'N/A':
885
- keyscale = ''
886
- if language == 'N/A':
887
- language = ''
888
- if timesignature == 'N/A':
889
- timesignature = ''
890
-
891
- return UnderstandResult(
892
- caption=caption,
893
- lyrics=lyrics,
894
- bpm=bpm,
895
- duration=duration,
896
- keyscale=keyscale,
897
- language=language,
898
- timesignature=timesignature,
899
- status_message=status,
900
- success=True,
901
- error=None,
902
- )
903
-
904
  except Exception as e:
905
- logger.exception("Music understanding failed")
906
- return UnderstandResult(
907
- status_message=f"Error: {str(e)}",
908
- success=False,
909
- error=str(e),
910
- )
911
-
912
-
913
- @dataclass
914
- class CreateSampleResult:
915
- """Result of creating a music sample from a natural language query.
916
-
917
- This is used by the "Simple Mode" / "Inspiration Mode" feature where users
918
- provide a natural language description and the LLM generates a complete
919
- sample with caption, lyrics, and metadata.
920
-
921
- Attributes:
922
- # Metadata Fields
923
- caption: Generated detailed music description/caption
924
- lyrics: Generated lyrics (or "[Instrumental]" for instrumental music)
925
- bpm: Beats per minute (None if not generated)
926
- duration: Duration in seconds (None if not generated)
927
- keyscale: Musical key (e.g., "C Major")
928
- language: Vocal language code (e.g., "en", "zh")
929
- timesignature: Time signature (e.g., "4")
930
- instrumental: Whether this is an instrumental piece
931
-
932
- # Status
933
- status_message: Status message from sample creation
934
- success: Whether sample creation completed successfully
935
- error: Error message if sample creation failed
936
- """
937
- # Metadata Fields
938
- caption: str = ""
939
- lyrics: str = ""
940
- bpm: Optional[int] = None
941
- duration: Optional[float] = None
942
- keyscale: str = ""
943
- language: str = ""
944
- timesignature: str = ""
945
- instrumental: bool = False
946
-
947
- # Status
948
- status_message: str = ""
949
- success: bool = True
950
- error: Optional[str] = None
951
-
952
- def to_dict(self) -> Dict[str, Any]:
953
- """Convert result to dictionary for JSON serialization."""
954
- return asdict(self)
955
-
956
-
957
- def create_sample(
958
- llm_handler,
959
- query: str,
960
- instrumental: bool = False,
961
- vocal_language: Optional[str] = None,
962
- temperature: float = 0.85,
963
- top_k: Optional[int] = None,
964
- top_p: Optional[float] = None,
965
- repetition_penalty: float = 1.0,
966
- use_constrained_decoding: bool = True,
967
- constrained_decoding_debug: bool = False,
968
- ) -> CreateSampleResult:
969
- """Create a music sample from a natural language query using the 5Hz Language Model.
970
-
971
- This is the "Simple Mode" / "Inspiration Mode" feature that takes a user's natural
972
- language description of music and generates a complete sample including:
973
- - Detailed caption/description
974
- - Lyrics (unless instrumental)
975
- - Metadata (BPM, duration, key, language, time signature)
976
-
977
- Note: cfg_scale and negative_prompt are not supported in create_sample mode.
978
-
979
- Args:
980
- llm_handler: Initialized LLM handler (LLMHandler instance)
981
- query: User's natural language music description (e.g., "a soft Bengali love song")
982
- instrumental: Whether to generate instrumental music (no vocals)
983
- vocal_language: Allowed vocal language for constrained decoding (e.g., "en", "zh").
984
- If provided, the model will be constrained to generate lyrics in this language.
985
- If None or "unknown", no language constraint is applied.
986
- temperature: Sampling temperature for generation (0.0-2.0). Higher = more creative.
987
- top_k: Top-K sampling (None or 0 = disabled)
988
- top_p: Top-P (nucleus) sampling (None or 1.0 = disabled)
989
- repetition_penalty: Repetition penalty (1.0 = no penalty)
990
- use_constrained_decoding: Whether to use FSM-based constrained decoding
991
- constrained_decoding_debug: Whether to enable debug logging
992
-
993
- Returns:
994
- CreateSampleResult with generated sample fields and status
995
-
996
- Example:
997
- >>> result = create_sample(llm_handler, "a soft Bengali love song for a quiet evening", vocal_language="bn")
998
- >>> if result.success:
999
- ... print(f"Caption: {result.caption}")
1000
- ... print(f"Lyrics: {result.lyrics}")
1001
- ... print(f"BPM: {result.bpm}")
1002
  """
1003
- import torch
1004
- # Debug logging for ZeroGPU diagnosis
1005
- logger.info(f"[create_sample Debug] Entry: IS_HUGGINGFACE_SPACE={IS_HUGGINGFACE_SPACE}")
1006
- logger.info(f"[create_sample Debug] torch.cuda.is_available()={torch.cuda.is_available()}")
1007
- if torch.cuda.is_available():
1008
- logger.info(f"[create_sample Debug] torch.cuda.current_device()={torch.cuda.current_device()}")
1009
- logger.info(f"[create_sample Debug] llm_handler.device={llm_handler.device}, llm_handler.offload_to_cpu={llm_handler.offload_to_cpu}")
1010
- if llm_handler.llm is not None:
1011
- try:
1012
- logger.info(f"[create_sample Debug] Model device: {next(llm_handler.llm.parameters()).device}")
1013
- except Exception as e:
1014
- logger.info(f"[create_sample Debug] Could not get model device: {e}")
1015
-
1016
- # Check if LLM is initialized
1017
- if not llm_handler.llm_initialized:
1018
- return CreateSampleResult(
1019
- status_message="5Hz LM not initialized. Please initialize it first.",
1020
- success=False,
1021
- error="LLM not initialized",
1022
- )
1023
-
1024
  try:
1025
- # Call LLM to create sample
1026
- metadata, status = llm_handler.create_sample_from_query(
1027
- query=query,
1028
- instrumental=instrumental,
1029
- vocal_language=vocal_language,
1030
- temperature=temperature,
1031
- top_k=top_k,
1032
- top_p=top_p,
1033
- repetition_penalty=repetition_penalty,
1034
- use_constrained_decoding=use_constrained_decoding,
1035
- constrained_decoding_debug=constrained_decoding_debug,
1036
- )
1037
-
1038
- # Check if LLM returned empty metadata (error case)
1039
- if not metadata:
1040
- return CreateSampleResult(
1041
- status_message=status or "Failed to create sample",
1042
- success=False,
1043
- error=status or "Empty metadata returned",
1044
- )
1045
-
1046
- # Extract and convert fields
1047
- caption = metadata.get('caption', '')
1048
- lyrics = metadata.get('lyrics', '')
1049
- keyscale = metadata.get('keyscale', '')
1050
- language = metadata.get('language', metadata.get('vocal_language', ''))
1051
- timesignature = metadata.get('timesignature', '')
1052
- is_instrumental = metadata.get('instrumental', instrumental)
1053
-
1054
- # Convert BPM to int
1055
- bpm = None
1056
- bpm_value = metadata.get('bpm')
1057
- if bpm_value is not None and bpm_value != 'N/A' and bpm_value != '':
1058
- try:
1059
- bpm = int(bpm_value)
1060
- except (ValueError, TypeError):
1061
- pass
1062
-
1063
- # Convert duration to float
1064
- duration = None
1065
- duration_value = metadata.get('duration')
1066
- if duration_value is not None and duration_value != 'N/A' and duration_value != '':
1067
  try:
1068
- duration = float(duration_value)
1069
- except (ValueError, TypeError):
1070
- pass
1071
-
1072
- # Clean up N/A values
1073
- if keyscale == 'N/A':
1074
- keyscale = ''
1075
- if language == 'N/A':
1076
- language = ''
1077
- if timesignature == 'N/A':
1078
- timesignature = ''
1079
-
1080
- return CreateSampleResult(
1081
- caption=caption,
1082
- lyrics=lyrics,
1083
- bpm=bpm,
1084
- duration=duration,
1085
- keyscale=keyscale,
1086
- language=language,
1087
- timesignature=timesignature,
1088
- instrumental=is_instrumental,
1089
- status_message=status,
1090
- success=True,
1091
- error=None,
1092
- )
1093
-
1094
- except Exception as e:
1095
- logger.exception("Sample creation failed")
1096
- return CreateSampleResult(
1097
- status_message=f"Error: {str(e)}",
1098
- success=False,
1099
- error=str(e),
1100
- )
1101
-
1102
-
1103
- @dataclass
1104
- class FormatSampleResult:
1105
- """Result of formatting user-provided caption and lyrics.
1106
-
1107
- This is used by the "Format" feature where users provide caption and lyrics,
1108
- and the LLM formats them into structured music metadata and an enhanced description.
1109
-
1110
- Attributes:
1111
- # Metadata Fields
1112
- caption: Enhanced/formatted music description/caption
1113
- lyrics: Formatted lyrics (may be same as input or reformatted)
1114
- bpm: Beats per minute (None if not detected)
1115
- duration: Duration in seconds (None if not detected)
1116
- keyscale: Musical key (e.g., "C Major")
1117
- language: Vocal language code (e.g., "en", "zh")
1118
- timesignature: Time signature (e.g., "4")
1119
-
1120
- # Status
1121
- status_message: Status message from formatting
1122
- success: Whether formatting completed successfully
1123
- error: Error message if formatting failed
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1124
  """
1125
- # Metadata Fields
1126
- caption: str = ""
1127
- lyrics: str = ""
1128
- bpm: Optional[int] = None
1129
- duration: Optional[float] = None
1130
- keyscale: str = ""
1131
- language: str = ""
1132
- timesignature: str = ""
1133
-
1134
- # Status
1135
- status_message: str = ""
1136
- success: bool = True
1137
- error: Optional[str] = None
1138
-
1139
- def to_dict(self) -> Dict[str, Any]:
1140
- """Convert result to dictionary for JSON serialization."""
1141
- return asdict(self)
1142
-
1143
-
1144
- def format_sample(
1145
- llm_handler,
1146
- caption: str,
1147
- lyrics: str,
1148
- user_metadata: Optional[Dict[str, Any]] = None,
1149
- temperature: float = 0.85,
1150
- top_k: Optional[int] = None,
1151
- top_p: Optional[float] = None,
1152
- repetition_penalty: float = 1.0,
1153
- use_constrained_decoding: bool = True,
1154
- constrained_decoding_debug: bool = False,
1155
- ) -> FormatSampleResult:
1156
- """Format user-provided caption and lyrics using the 5Hz Language Model.
1157
-
1158
- This function takes user input (caption and lyrics) and generates structured
1159
- music metadata including an enhanced caption, BPM, duration, key, language,
1160
- and time signature.
1161
-
1162
- If user_metadata is provided, those values will be used to constrain the
1163
- decoding, ensuring the output matches user-specified values.
1164
-
1165
- Note: cfg_scale and negative_prompt are not supported in format mode.
1166
-
1167
- Args:
1168
- llm_handler: Initialized LLM handler (LLMHandler instance)
1169
- caption: User's caption/description (e.g., "Latin pop, reggaeton")
1170
- lyrics: User's lyrics with structure tags
1171
- user_metadata: Optional dict with user-provided metadata to constrain decoding.
1172
- Supported keys: bpm, duration, keyscale, timesignature, language
1173
- temperature: Sampling temperature for generation (0.0-2.0). Higher = more creative.
1174
- top_k: Top-K sampling (None or 0 = disabled)
1175
- top_p: Top-P (nucleus) sampling (None or 1.0 = disabled)
1176
- repetition_penalty: Repetition penalty (1.0 = no penalty)
1177
- use_constrained_decoding: Whether to use FSM-based constrained decoding for metadata
1178
- constrained_decoding_debug: Whether to enable debug logging for constrained decoding
1179
-
1180
- Returns:
1181
- FormatSampleResult with formatted metadata fields and status
1182
-
1183
- Example:
1184
- >>> result = format_sample(llm_handler, "Latin pop, reggaeton", "[Verse 1]\\nHola mundo...")
1185
- >>> if result.success:
1186
- ... print(f"Caption: {result.caption}")
1187
- ... print(f"BPM: {result.bpm}")
1188
- ... print(f"Lyrics: {result.lyrics}")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1189
  """
1190
- # Check if LLM is initialized
1191
- if not llm_handler.llm_initialized:
1192
- return FormatSampleResult(
1193
- status_message="5Hz LM not initialized. Please initialize it first.",
1194
- success=False,
1195
- error="LLM not initialized",
1196
- )
1197
-
1198
- try:
1199
- # Call LLM formatting
1200
- metadata, status = llm_handler.format_sample_from_input(
1201
- caption=caption,
1202
- lyrics=lyrics,
1203
- user_metadata=user_metadata,
1204
- temperature=temperature,
1205
- top_k=top_k,
1206
- top_p=top_p,
1207
- repetition_penalty=repetition_penalty,
1208
- use_constrained_decoding=use_constrained_decoding,
1209
- constrained_decoding_debug=constrained_decoding_debug,
1210
- )
1211
-
1212
- # Check if LLM returned empty metadata (error case)
1213
- if not metadata:
1214
- return FormatSampleResult(
1215
- status_message=status or "Failed to format input",
1216
- success=False,
1217
- error=status or "Empty metadata returned",
1218
- )
1219
-
1220
- # Extract and convert fields
1221
- result_caption = metadata.get('caption', '')
1222
- result_lyrics = metadata.get('lyrics', lyrics) # Fall back to input lyrics
1223
- keyscale = metadata.get('keyscale', '')
1224
- language = metadata.get('language', metadata.get('vocal_language', ''))
1225
- timesignature = metadata.get('timesignature', '')
1226
-
1227
- # Convert BPM to int
1228
- bpm = None
1229
- bpm_value = metadata.get('bpm')
1230
- if bpm_value is not None and bpm_value != 'N/A' and bpm_value != '':
1231
- try:
1232
- bpm = int(bpm_value)
1233
- except (ValueError, TypeError):
1234
- pass
1235
-
1236
- # Convert duration to float
1237
- duration = None
1238
- duration_value = metadata.get('duration')
1239
- if duration_value is not None and duration_value != 'N/A' and duration_value != '':
1240
- try:
1241
- duration = float(duration_value)
1242
- except (ValueError, TypeError):
1243
- pass
1244
-
1245
- # Clean up N/A values
1246
- if keyscale == 'N/A':
1247
- keyscale = ''
1248
- if language == 'N/A':
1249
- language = ''
1250
- if timesignature == 'N/A':
1251
- timesignature = ''
1252
-
1253
- return FormatSampleResult(
1254
- caption=result_caption,
1255
- lyrics=result_lyrics,
1256
- bpm=bpm,
1257
- duration=duration,
1258
- keyscale=keyscale,
1259
- language=language,
1260
- timesignature=timesignature,
1261
- status_message=status,
1262
- success=True,
1263
- error=None,
1264
- )
1265
-
1266
- except Exception as e:
1267
- logger.exception("Format sample failed")
1268
- return FormatSampleResult(
1269
- status_message=f"Error: {str(e)}",
1270
- success=False,
1271
- error=str(e),
1272
- )
 
1
  """
2
+ resume_utils.py
3
+ ================
4
+ Save & Resume utility for long-running ACE-Step generations on HuggingFace
5
+ ZeroGPU Spaces (120-second hard GPU timeout per call).
6
+
7
+ DESIGN NOTE (important):
8
+ This module is built for the LEGITIMATE resume scenario only:
9
+ - Same user, same Space, same account.
10
+ - Today's free ZeroGPU quota runs out before generation finishes.
11
+ - User comes back tomorrow (quota resets) and continues from where
12
+ they left off, using the SAME IP / SAME browser session.
13
+ There is no IP-rotation, identity-spoofing, or quota-evasion logic here.
14
+ All this code does is persist intermediate tensors to the Space's
15
+ persistent disk (/data) under a task_id, so a later call (today or
16
+ tomorrow, same account) can pick the work back up instead of starting
17
+ from token 0 / step 0 again.
18
+
19
+ Storage layout (under persistent_storage_path, e.g. /data/resume_state):
20
+ /data/resume_state/<task_id>/lm_tokens.pt -> generated LM token ids (CPU tensor)
21
+ /data/resume_state/<task_id>/lm_meta.json -> phase + params + timestamps
22
+ /data/resume_state/<task_id>/dit_latents.pt -> latents at last checkpointed step
23
+ /data/resume_state/<task_id>/dit_meta.json -> step index + DiT params + timestamps
24
+ /data/resume_state/<task_id>/audio_codes.json -> final LM phase-2 output (string codes)
25
+
26
+ task_id is short, human-typeable (e.g. "AB3K9F"), so a person can write it
27
+ down and paste it into a textbox the next day.
28
  """
29
 
 
30
  import os
31
+ import json
32
+ import time
33
+ import uuid
34
+ import string
35
+ import random
36
+ from typing import Optional, Dict, Any
37
+
38
+ import torch
39
  from loguru import logger
40
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
41
 
42
+ # ZeroGPU gives ~120s per call. We checkpoint a safety margin before the
43
+ # hard kill so the save itself (disk I/O) has time to complete.
44
+ DEFAULT_TIMEOUT_SECONDS = 110
45
+
46
+ # How long an unfinished task is kept on disk before we consider it stale
47
+ # and eligible for cleanup. 7 days comfortably covers "finish tomorrow".
48
+ MAX_TASK_AGE_SECONDS = 7 * 24 * 3600
49
+
50
+
51
+ def _resume_root(persistent_storage_path: Optional[str]) -> str:
52
+ """Resolve the root directory for resume state.
53
+
54
+ Falls back to a local ./resume_state directory if no persistent path
55
+ is configured (e.g. local dev), so the code never crashes — it just
56
+ won't survive a process restart in that case.
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
57
  """
58
+ base = persistent_storage_path or "."
59
+ root = os.path.join(base, "resume_state")
60
+ os.makedirs(root, exist_ok=True)
61
+ return root
62
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
63
 
64
+ def generate_task_id() -> str:
65
+ """Generate a short, human-typeable tracking code, e.g. 'AB3K9F'.
66
+
67
+ Avoids ambiguous characters (0/O, 1/I/L) so users can read it back
68
+ off a screen and retype it the next day without errors.
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
69
  """
70
+ alphabet = "ABCDEFGHJKMNPQRSTUVWXYZ23456789"
71
+ return "".join(random.choice(alphabet) for _ in range(6))
72
+
73
+
74
+ def _task_dir(persistent_storage_path: Optional[str], task_id: str) -> str:
75
+ safe_id = "".join(c for c in task_id.strip().upper() if c.isalnum())
76
+ if not safe_id:
77
+ raise ValueError("Invalid task_id")
78
+ d = os.path.join(_resume_root(persistent_storage_path), safe_id)
79
+ os.makedirs(d, exist_ok=True)
80
+ return d
81
+
82
+
83
+ def task_exists(persistent_storage_path: Optional[str], task_id: str) -> bool:
84
  try:
85
+ d = os.path.join(_resume_root(persistent_storage_path), task_id.strip().upper())
86
+ return os.path.isdir(d)
87
+ except Exception:
88
+ return False
89
+
90
+
91
+ def get_task_status(persistent_storage_path: Optional[str], task_id: str) -> Dict[str, Any]:
92
+ """Return a human-readable status dict for a task_id, or {'found': False}."""
93
+ if not task_exists(persistent_storage_path, task_id):
94
+ return {"found": False}
95
+
96
+ d = _task_dir(persistent_storage_path, task_id)
97
+ status = {"found": True, "task_id": task_id.strip().upper(), "phase": "unknown"}
98
+
99
+ lm_meta_path = os.path.join(d, "lm_meta.json")
100
+ dit_meta_path = os.path.join(d, "dit_meta.json")
101
+ codes_path = os.path.join(d, "audio_codes.json")
102
+ final_audio_meta_path = os.path.join(d, "final_meta.json")
103
+
104
+ if os.path.exists(final_audio_meta_path):
105
+ with open(final_audio_meta_path, "r", encoding="utf-8") as f:
106
+ status.update(json.load(f))
107
+ status["phase"] = "complete"
108
+ elif os.path.exists(dit_meta_path):
109
+ with open(dit_meta_path, "r", encoding="utf-8") as f:
110
+ dit_meta = json.load(f)
111
+ status["phase"] = "dit_in_progress"
112
+ status["dit_step"] = dit_meta.get("current_step_idx")
113
+ status["dit_total_steps"] = dit_meta.get("total_steps")
114
+ status["saved_at"] = dit_meta.get("saved_at")
115
+ elif os.path.exists(codes_path):
116
+ status["phase"] = "lm_complete_dit_pending"
117
+ elif os.path.exists(lm_meta_path):
118
+ with open(lm_meta_path, "r", encoding="utf-8") as f:
119
+ lm_meta = json.load(f)
120
+ status["phase"] = "lm_in_progress"
121
+ status["lm_step"] = lm_meta.get("step")
122
+ status["saved_at"] = lm_meta.get("saved_at")
123
+
124
+ return status
125
+
126
+
127
+ def cleanup_task(persistent_storage_path: Optional[str], task_id: str) -> None:
128
+ """Remove all state for a finished/abandoned task."""
129
+ import shutil
130
+ try:
131
+ d = os.path.join(_resume_root(persistent_storage_path), task_id.strip().upper())
132
+ if os.path.isdir(d):
133
+ shutil.rmtree(d)
134
+ logger.info(f"[resume_utils] Cleaned up task {task_id}")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
135
  except Exception as e:
136
+ logger.warning(f"[resume_utils] Failed to clean up task {task_id}: {e}")
137
+
138
+
139
+ def cleanup_stale_tasks(persistent_storage_path: Optional[str], max_age_seconds: int = MAX_TASK_AGE_SECONDS) -> int:
140
+ """Delete tasks older than max_age_seconds. Call this occasionally (e.g. on app startup).
141
+
142
+ Returns the number of tasks removed.
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
143
  """
144
+ import shutil
145
+ root = _resume_root(persistent_storage_path)
146
+ removed = 0
147
+ now = time.time()
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
148
  try:
149
+ for name in os.listdir(root):
150
+ d = os.path.join(root, name)
151
+ if not os.path.isdir(d):
152
+ continue
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
153
  try:
154
+ mtime = os.path.getmtime(d)
155
+ if now - mtime > max_age_seconds:
156
+ shutil.rmtree(d)
157
+ removed += 1
158
+ except Exception:
159
+ continue
160
+ except FileNotFoundError:
161
+ pass
162
+ if removed:
163
+ logger.info(f"[resume_utils] Cleaned up {removed} stale task(s)")
164
+ return removed
165
+
166
+
167
+ # ---------------------------------------------------------------------------
168
+ # LM phase (token-by-token generation) save/load
169
+ # ---------------------------------------------------------------------------
170
+
171
+ def save_lm_checkpoint(
172
+ persistent_storage_path: Optional[str],
173
+ task_id: str,
174
+ generated_ids: torch.Tensor,
175
+ step: int,
176
+ extra: Optional[Dict[str, Any]] = None,
177
+ ) -> None:
178
+ """Persist LM token generation state to disk."""
179
+ d = _task_dir(persistent_storage_path, task_id)
180
+ torch.save(generated_ids.detach().to("cpu"), os.path.join(d, "lm_tokens.pt"))
181
+
182
+ meta = {
183
+ "step": step,
184
+ "saved_at": time.time(),
185
+ "shape": list(generated_ids.shape),
186
+ }
187
+ if extra:
188
+ meta.update(extra)
189
+
190
+ with open(os.path.join(d, "lm_meta.json"), "w", encoding="utf-8") as f:
191
+ json.dump(meta, f, ensure_ascii=False, indent=2)
192
+
193
+ logger.info(f"[resume_utils] LM checkpoint saved for task {task_id} at step {step}")
194
+
195
+
196
+ def load_lm_checkpoint(
197
+ persistent_storage_path: Optional[str],
198
+ task_id: str,
199
+ device: str = "cuda",
200
+ ) -> Optional[Dict[str, Any]]:
201
+ """Load previously saved LM token state, or None if not found."""
202
+ d = os.path.join(_resume_root(persistent_storage_path), task_id.strip().upper())
203
+ tokens_path = os.path.join(d, "lm_tokens.pt")
204
+ meta_path = os.path.join(d, "lm_meta.json")
205
+
206
+ if not (os.path.exists(tokens_path) and os.path.exists(meta_path)):
207
+ return None
208
+
209
+ generated_ids = torch.load(tokens_path, map_location=device)
210
+ with open(meta_path, "r", encoding="utf-8") as f:
211
+ meta = json.load(f)
212
+
213
+ logger.info(f"[resume_utils] LM checkpoint restored for task {task_id} from step {meta.get('step')}")
214
+ return {"generated_ids": generated_ids, "meta": meta}
215
+
216
+
217
+ def save_lm_audio_codes(
218
+ persistent_storage_path: Optional[str],
219
+ task_id: str,
220
+ metadata: Dict[str, Any],
221
+ audio_codes: str,
222
+ ) -> None:
223
+ """Persist the final (completed) LM phase output: metadata + audio codes string.
224
+
225
+ Once this is saved, the LM phase is fully done — the DiT phase can be
226
+ resumed/started independently using this file, even in a brand-new
227
+ call (today or tomorrow).
228
  """
229
+ d = _task_dir(persistent_storage_path, task_id)
230
+ with open(os.path.join(d, "audio_codes.json"), "w", encoding="utf-8") as f:
231
+ json.dump({"metadata": metadata, "audio_codes": audio_codes, "saved_at": time.time()}, f, ensure_ascii=False, indent=2)
232
+ logger.info(f"[resume_utils] LM phase output (codes) saved for task {task_id}")
233
+
234
+
235
+ def load_lm_audio_codes(persistent_storage_path: Optional[str], task_id: str) -> Optional[Dict[str, Any]]:
236
+ d = os.path.join(_resume_root(persistent_storage_path), task_id.strip().upper())
237
+ path = os.path.join(d, "audio_codes.json")
238
+ if not os.path.exists(path):
239
+ return None
240
+ with open(path, "r", encoding="utf-8") as f:
241
+ return json.load(f)
242
+
243
+
244
+ # ---------------------------------------------------------------------------
245
+ # DiT phase (diffusion denoise loop) save/load
246
+ # ---------------------------------------------------------------------------
247
+
248
+ def save_dit_checkpoint(
249
+ persistent_storage_path: Optional[str],
250
+ task_id: str,
251
+ latents: torch.Tensor,
252
+ current_step_idx: int,
253
+ total_steps: int,
254
+ extra: Optional[Dict[str, Any]] = None,
255
+ ) -> None:
256
+ """Persist DiT denoising state (latents + step index) to disk."""
257
+ d = _task_dir(persistent_storage_path, task_id)
258
+ torch.save(latents.detach().to("cpu"), os.path.join(d, "dit_latents.pt"))
259
+
260
+ meta = {
261
+ "current_step_idx": current_step_idx,
262
+ "total_steps": total_steps,
263
+ "saved_at": time.time(),
264
+ "shape": list(latents.shape),
265
+ }
266
+ if extra:
267
+ meta.update(extra)
268
+
269
+ with open(os.path.join(d, "dit_meta.json"), "w", encoding="utf-8") as f:
270
+ json.dump(meta, f, ensure_ascii=False, indent=2)
271
+
272
+ logger.info(f"[resume_utils] DiT checkpoint saved for task {task_id} at step {current_step_idx}/{total_steps}")
273
+
274
+
275
+ def load_dit_checkpoint(
276
+ persistent_storage_path: Optional[str],
277
+ task_id: str,
278
+ device: str = "cuda",
279
+ ) -> Optional[Dict[str, Any]]:
280
+ """Load previously saved DiT latent state, or None if not found."""
281
+ d = os.path.join(_resume_root(persistent_storage_path), task_id.strip().upper())
282
+ latents_path = os.path.join(d, "dit_latents.pt")
283
+ meta_path = os.path.join(d, "dit_meta.json")
284
+
285
+ if not (os.path.exists(latents_path) and os.path.exists(meta_path)):
286
+ return None
287
+
288
+ latents = torch.load(latents_path, map_location=device)
289
+ with open(meta_path, "r", encoding="utf-8") as f:
290
+ meta = json.load(f)
291
+
292
+ logger.info(f"[resume_utils] DiT checkpoint restored for task {task_id} from step {meta.get('current_step_idx')}")
293
+ return {"latents": latents, "meta": meta}
294
+
295
+
296
+ def save_final_marker(persistent_storage_path: Optional[str], task_id: str, audio_path: Optional[str] = None) -> None:
297
+ """Mark a task as fully complete (audio rendered). Useful for status display."""
298
+ d = _task_dir(persistent_storage_path, task_id)
299
+ with open(os.path.join(d, "final_meta.json"), "w", encoding="utf-8") as f:
300
+ json.dump({"completed_at": time.time(), "audio_path": audio_path}, f, ensure_ascii=False, indent=2)
301
+
302
+
303
+ class GenerationTimer:
304
+ """Small helper to check elapsed time against the ZeroGPU budget.
305
+
306
+ Usage:
307
+ timer = GenerationTimer(timeout=110)
308
+ for step in ...:
309
+ if timer.expired():
310
+ # save checkpoint and return early
311
+ break
312
  """
313
+
314
+ def __init__(self, timeout: float = DEFAULT_TIMEOUT_SECONDS):
315
+ self.start_time = time.time()
316
+ self.timeout = timeout
317
+
318
+ def elapsed(self) -> float:
319
+ return time.time() - self.start_time
320
+
321
+ def expired(self) -> bool:
322
+ return self.elapsed() > self.timeout
323
+
324
+ def remaining(self) -> float:
325
+ return max(0.0, self.timeout - self.elapsed())