Spaces:
Paused
Paused
Update acestep/resume_utils.py
Browse files- acestep/resume_utils.py +307 -1254
acestep/resume_utils.py
CHANGED
|
@@ -1,1272 +1,325 @@
|
|
| 1 |
"""
|
| 2 |
-
|
| 3 |
-
|
| 4 |
-
|
| 5 |
-
|
| 6 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 7 |
"""
|
| 8 |
|
| 9 |
-
import math
|
| 10 |
import os
|
| 11 |
-
import
|
| 12 |
-
|
| 13 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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 |
-
|
| 186 |
-
|
| 187 |
-
|
| 188 |
-
|
| 189 |
-
|
| 190 |
-
|
| 191 |
-
|
| 192 |
-
|
| 193 |
-
|
| 194 |
-
|
| 195 |
-
|
| 196 |
-
|
| 197 |
-
|
| 198 |
-
|
| 199 |
-
|
| 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 |
-
|
| 320 |
-
|
| 321 |
-
|
| 322 |
-
|
| 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 |
-
|
| 775 |
-
|
| 776 |
-
|
| 777 |
-
|
| 778 |
-
|
| 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 |
-
|
| 827 |
-
|
| 828 |
-
|
| 829 |
-
|
| 830 |
-
|
| 831 |
-
|
| 832 |
-
|
| 833 |
-
|
| 834 |
-
|
| 835 |
-
|
| 836 |
-
|
| 837 |
-
|
|
|
|
|
|
|
| 838 |
try:
|
| 839 |
-
|
| 840 |
-
|
| 841 |
-
|
| 842 |
-
|
| 843 |
-
|
| 844 |
-
|
| 845 |
-
|
| 846 |
-
|
| 847 |
-
|
| 848 |
-
|
| 849 |
-
|
| 850 |
-
|
| 851 |
-
|
| 852 |
-
|
| 853 |
-
|
| 854 |
-
|
| 855 |
-
|
| 856 |
-
|
| 857 |
-
|
| 858 |
-
|
| 859 |
-
|
| 860 |
-
|
| 861 |
-
|
| 862 |
-
|
| 863 |
-
|
| 864 |
-
|
| 865 |
-
|
| 866 |
-
|
| 867 |
-
|
| 868 |
-
|
| 869 |
-
|
| 870 |
-
|
| 871 |
-
|
| 872 |
-
|
| 873 |
-
|
| 874 |
-
|
| 875 |
-
|
| 876 |
-
|
| 877 |
-
|
| 878 |
-
|
| 879 |
-
|
| 880 |
-
|
| 881 |
-
|
| 882 |
-
|
| 883 |
-
|
| 884 |
-
|
| 885 |
-
|
| 886 |
-
if
|
| 887 |
-
|
| 888 |
-
|
| 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.
|
| 906 |
-
|
| 907 |
-
|
| 908 |
-
|
| 909 |
-
|
| 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
|
| 1004 |
-
|
| 1005 |
-
|
| 1006 |
-
|
| 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 |
-
|
| 1026 |
-
|
| 1027 |
-
|
| 1028 |
-
|
| 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 |
-
|
| 1069 |
-
|
| 1070 |
-
|
| 1071 |
-
|
| 1072 |
-
|
| 1073 |
-
|
| 1074 |
-
|
| 1075 |
-
|
| 1076 |
-
|
| 1077 |
-
|
| 1078 |
-
|
| 1079 |
-
|
| 1080 |
-
|
| 1081 |
-
|
| 1082 |
-
|
| 1083 |
-
|
| 1084 |
-
|
| 1085 |
-
|
| 1086 |
-
|
| 1087 |
-
|
| 1088 |
-
|
| 1089 |
-
|
| 1090 |
-
|
| 1091 |
-
|
| 1092 |
-
|
| 1093 |
-
|
| 1094 |
-
|
| 1095 |
-
|
| 1096 |
-
|
| 1097 |
-
|
| 1098 |
-
|
| 1099 |
-
|
| 1100 |
-
|
| 1101 |
-
|
| 1102 |
-
|
| 1103 |
-
|
| 1104 |
-
|
| 1105 |
-
|
| 1106 |
-
|
| 1107 |
-
|
| 1108 |
-
|
| 1109 |
-
|
| 1110 |
-
|
| 1111 |
-
|
| 1112 |
-
|
| 1113 |
-
|
| 1114 |
-
|
| 1115 |
-
|
| 1116 |
-
|
| 1117 |
-
|
| 1118 |
-
|
| 1119 |
-
|
| 1120 |
-
|
| 1121 |
-
|
| 1122 |
-
|
| 1123 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1124 |
"""
|
| 1125 |
-
|
| 1126 |
-
|
| 1127 |
-
|
| 1128 |
-
|
| 1129 |
-
|
| 1130 |
-
|
| 1131 |
-
|
| 1132 |
-
|
| 1133 |
-
|
| 1134 |
-
|
| 1135 |
-
|
| 1136 |
-
|
| 1137 |
-
|
| 1138 |
-
|
| 1139 |
-
|
| 1140 |
-
|
| 1141 |
-
|
| 1142 |
-
|
| 1143 |
-
|
| 1144 |
-
def
|
| 1145 |
-
|
| 1146 |
-
|
| 1147 |
-
|
| 1148 |
-
|
| 1149 |
-
|
| 1150 |
-
|
| 1151 |
-
|
| 1152 |
-
|
| 1153 |
-
|
| 1154 |
-
|
| 1155 |
-
|
| 1156 |
-
|
| 1157 |
-
|
| 1158 |
-
|
| 1159 |
-
|
| 1160 |
-
|
| 1161 |
-
|
| 1162 |
-
|
| 1163 |
-
|
| 1164 |
-
|
| 1165 |
-
|
| 1166 |
-
|
| 1167 |
-
|
| 1168 |
-
|
| 1169 |
-
|
| 1170 |
-
|
| 1171 |
-
|
| 1172 |
-
|
| 1173 |
-
|
| 1174 |
-
|
| 1175 |
-
|
| 1176 |
-
|
| 1177 |
-
|
| 1178 |
-
|
| 1179 |
-
|
| 1180 |
-
|
| 1181 |
-
|
| 1182 |
-
|
| 1183 |
-
|
| 1184 |
-
|
| 1185 |
-
|
| 1186 |
-
.
|
| 1187 |
-
|
| 1188 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1189 |
"""
|
| 1190 |
-
|
| 1191 |
-
|
| 1192 |
-
|
| 1193 |
-
|
| 1194 |
-
|
| 1195 |
-
|
| 1196 |
-
)
|
| 1197 |
-
|
| 1198 |
-
|
| 1199 |
-
|
| 1200 |
-
|
| 1201 |
-
|
| 1202 |
-
|
| 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())
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|