Spaces:
Opera10
/
Running on Zero

File size: 26,171 Bytes
c3da9f3
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
"""
ACE-Step v1.5 - HuggingFace Space Entry Point
This file serves as the entry point for HuggingFace Space deployment.
It initializes the service and launches the Gradio interface.
ZeroGPU Support:
- ZeroGPU uses the 'spaces' package to intercept CUDA operations
- Models are loaded to "cuda" during startup but actual GPU allocation is deferred
- Handlers are registered globally so forked processes inherit them without pickling
- @spaces.GPU decorators are on top-level Gradio event handlers, not internal functions
- nano-vllm uses direct CUDA APIs that bypass spaces interception, so we use PyTorch backend

اسپیسِ ترکیبی «ساخت آهنگ + کلون کردن صدا + آواتار سخنگو»
--------------------------------------------------------
• تبِ «🎵 ساخت آهنگ»: همان ACE-Step قبلی، بدونِ هیچ تغییری، روی GPU (ZeroGPU).
• تبِ «🎙️ کلون کردن صدا»: همان اسپیسِ صدای قبلی (Vevo)، کاملاً روی CPU، در
  یک پروسه‌ی جدا (voice_engine.py) که ZeroGPU را ایمپورت نمی‌کند — توضیحِ کامل
  بالای voice_engine.py و voice_clone.py. حالا تا «دو» تبدیل را هم‌زمان می‌سازد
  (کارِ تنها همه‌ی هسته‌ها، دو کار هر کدام نصف؛ اکشن پرو کارِ دوم را فقط وقتی
  می‌فرستد که هیچ اسپیسِ خالیِ دیگری نیست)، صدای مرجعِ بلند را به پُرصداترین ۱۲
  ثانیه‌اش کوتاه می‌کند و کارِ بی‌صاحب را همان لحظه لغو می‌کند.
• تبِ «🗣️ آواتار سخنگو»: همان اسپیسِ آواتارِ قبلی (MiniMax-H3، عکس + صدا ← ویدیو)
  روی GPU (کارتِ xlarge، مثلِ قبل)، بدونِ روشِ فایلِ .pt — توضیحِ کامل بالای
  avatar_tab.py.
• مسیرهای API:
    ساخت آهنگ  ← دکمه‌ی «Generate Music» (همان قبلی؛ اکشن پرو آن را از روی
                  نام دکمه و تعدادِ ورودی‌ها در کانفیگ پیدا می‌کند)
    کلون صدا   ← /predict_voice_conversion       (ورودی: صدای منبع، صدای مرجع)
    وضعیت صدا  ← /voice_status                   (فقط صفِ صدا؛ بدونِ صف و فوری؛ "slots": 2)
    آواتار     ← /generate                       (همان مسیرِ اسپیسِ آواتارِ قبلی)
• diffusers: یک نسخه‌ی قفل‌شده از diffusers main (همان کامیتِ اسپیسِ آواتار) در
  vendor/diffusers-d5baa4fb.zip — چرا: README، بخشِ «وابستگی‌ها».
"""
import os
import sys

# Get current directory (app.py location)
current_dir = os.path.dirname(os.path.abspath(__file__))

# Add nano-vllm to Python path (local package)
nano_vllm_path = os.path.join(current_dir, "acestep", "third_parts", "nano-vllm")
if os.path.exists(nano_vllm_path):
    sys.path.insert(0, nano_vllm_path)

# diffusers (هم VAEِ آهنگ و هم کلِ مدلِ آواتار از آن استفاده می‌کنند): نسخه‌ی قفل‌شده‌ی
# diffusers main @ d5baa4fb (همان کامیتی که اسپیسِ آواتار داشت)، به‌صورتِ یک فایلِ zip که
# پایتون مستقیم از داخلش ایمپورت می‌کند. از pip نصب نمی‌شود چون آن نسخه huggingface_hub>=1.23
# می‌خواهد و transformersِ آهنگ (<4.58) فقط huggingface_hub<1.0 را قبول می‌کند (توضیح: README).
_vendored_diffusers = os.path.join(current_dir, "vendor", "diffusers-d5baa4fb.zip")
if os.path.exists(_vendored_diffusers):
    sys.path.insert(0, _vendored_diffusers)

# Disable Gradio analytics
os.environ["GRADIO_ANALYTICS_ENABLED"] = "False"

# Clear proxy settings that may affect Gradio
for proxy_var in ['http_proxy', 'https_proxy', 'HTTP_PROXY', 'HTTPS_PROXY', 'ALL_PROXY']:
    os.environ.pop(proxy_var, None)

# Import spaces for ZeroGPU support (must be imported before torch for proper interception)
# This is a no-op if not running on HuggingFace Spaces
try:
    import spaces
    HAS_SPACES = True
except ImportError:
    HAS_SPACES = False

import torch
from acestep.handler import AceStepHandler
from acestep.llm_inference import LLMHandler
from acestep.dataset_handler import DatasetHandler
from acestep.gradio_ui import create_gradio_interface

# پچ فوق‌العاده برای استخراج خودکار مسیر متنی و تبدیل خودکار فرمت‌های غیر استاندارد گوشی (مانند m4a) به wav استاندارد
original_load_audio_file = AceStepHandler._load_audio_file
import subprocess
import uuid

def convert_to_standard_wav(audio_file_path: str) -> str:
    """
    مبدل صوتی پویا: فایل‌های غیر استاندارد را به فرمت استودیویی WAV تبدیل می‌کند تا با تضمین ۱۰۰٪ لود شوند
    """
    if not audio_file_path or not os.path.exists(audio_file_path):
        return audio_file_path
        
    ext = os.path.splitext(audio_file_path)[1].lower()
    # لیست فرمت‌های رایج گوشی که نیاز به تبدیل خودکار به فرمت استاندارد wav دارند
    if ext in [".m4a", ".aac", ".3gp", ".amr", ".webm", ".ogg", ".opus", ".mp4"]:
        logger_name = "Auto-Convert"
        print(f"[{logger_name}] Detected format {ext}. Converting {audio_file_path} to standard WAV...")
        try:
            uploads_dir = os.path.join(current_dir, "data", "shared_uploads")
            os.makedirs(uploads_dir, exist_ok=True)
            
            temp_wav = os.path.join(uploads_dir, f"converted_{uuid.uuid4().hex}.wav")
            
            # فرآیند تبدیل بلادرنگ و سریع با ابزار سیستمی ffmpeg
            cmd = [
                "ffmpeg", "-y", 
                "-i", audio_file_path, 
                "-ac", "2", 
                "-ar", "48000", 
                "-acodec", "pcm_s16le", 
                temp_wav
            ]
            
            subprocess.run(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE, check=True)
            print(f"[{logger_name}] Converted successfully to: {temp_wav}")
            return temp_wav
        except Exception as e:
            print(f"[{logger_name}] Failed to convert: {e}", file=sys.stderr)
    return audio_file_path

def patched_load_audio_file(self, audio_file):
    if isinstance(audio_file, list) and len(audio_file) > 0:
        audio_file = audio_file[0]
    if isinstance(audio_file, dict):
        if "path" in audio_file and audio_file["path"]:
            audio_file = audio_file["path"]
        elif "value" in audio_file and audio_file["value"]:
            audio_file = audio_file["value"]
            
    # تبدیل خودکار فرمت‌های موبایل قبل از تحویل به موتور دکودر
    if isinstance(audio_file, str):
        audio_file = convert_to_standard_wav(audio_file)
        
    return original_load_audio_file(self, audio_file)

AceStepHandler._load_audio_file = patched_load_audio_file

# بخشِ «کلون کردن صدا» (فقط CPU، پروسه‌ی جدا) -- voice_clone.py
import voice_clone

# بخشِ «آواتار سخنگو» (GPU، کارتِ xlarge) -- avatar_tab.py
# AVATAR_DISABLED=1 (اختیاری، برای آزمایش) این تب و مدلش را کاملاً خاموش می‌کند.
AVATAR_ENABLED = os.environ.get("AVATAR_DISABLED", "").lower() not in ("1", "true", "yes")
if AVATAR_ENABLED:
    import avatar_tab

# پچ هوشمند برای جلوگیری از کرش تفکیک صدا در خودِ اسپیس و صادر کردن خطای صریح در صورت عدم پردازش فایل صوتی مرجع
original_prepare_batch = AceStepHandler._prepare_batch

def patched_prepare_batch(self, *args, **kwargs):
    args_list = list(args)
    
    # اگر کاربر فایل صوتی مرجع فرستاده ولی پردازش نشده و مقدار NoneType است، خطای واضح صادر می‌کنیم تا برنامه متوقف شود
    # و از تولید خروجی بی‌صدا یا پیش‌فرض جلوگیری به عمل آید.
    if "refer_audios" in kwargs and kwargs["refer_audios"] is None:
        raise ValueError("The uploaded Reference Audio could not be processed. Please make sure the audio file is valid, has sound, and is not corrupted.")
    elif len(args_list) > 5 and args_list[5] is None:
        raise ValueError("The uploaded Reference Audio could not be processed. Please make sure the audio file is valid, has sound, and is not corrupted.")
        
    return original_prepare_batch(self, *args_list, **kwargs)

AceStepHandler._prepare_batch = patched_prepare_batch

# پچ هوشمند و پویا برای پیدا کردن و تصحیح فیلد تعداد تولید در تمام مراحل اجرای متدها
import acestep.gradio_ui.events.results_handlers as res_h
original_gen = res_h.generate_with_batch_management

def patched_generate_with_batch_management(*args, **kwargs):
    args_list = list(args)
    if len(args_list) > 14:
        if args_list[14] is None or (isinstance(args_list[14], (int, float)) and args_list[14] < 1):
            args_list[14] = 1
    if "batch_size_input" in kwargs and (kwargs["batch_size_input"] is None or kwargs["batch_size_input"] < 1):
        kwargs["batch_size_input"] = 1
        
    generator = original_gen(*args_list, **kwargs)
    for partial_result in generator:
        if isinstance(partial_result, tuple):
            modified_result = list(partial_result)
            for i, item in enumerate(modified_result):
                if isinstance(item, dict) and "batch_size_input" in item:
                    if item["batch_size_input"] is None or item["batch_size_input"] < 1:
                        item["batch_size_input"] = 1
            yield tuple(modified_result)
        else:
            yield partial_result

res_h.generate_with_batch_management = patched_generate_with_batch_management

# پچ هوشمند برای حذف محدودیت قفل مرورگر در فیلدهای عددی با مقدار حداقل ۱
import gradio as gr
original_number_init = gr.Number.__init__
def patched_number_init(self, *args, **kwargs):
    if "minimum" in kwargs and kwargs["minimum"] == 1:
        kwargs["minimum"] = None  # رفع کلی مشکل فیلدهای با محدودیت حداقل ۱ عددی
    original_number_init(self, *args, **kwargs)
gr.Number.__init__ = patched_number_init

# اعمال پچ اصلاحی برای غیرفعال کردن Flash Attention ناسازگار روی ZeroGPU و استفاده از موتور پایدار sdpa
AceStepHandler.is_flash_attention_available = lambda self: False
AceStepHandler.is_flash_attn3_available = lambda self: False
AceStepHandler.get_best_attn_implementation = lambda self: "sdpa"

LLMHandler.is_flash_attention_available = lambda self: False
LLMHandler.is_flash_attn3_available = lambda self: False
LLMHandler.get_best_attn_implementation = lambda self: "sdpa"

# Detect ZeroGPU environment
IS_HUGGINGFACE_SPACE = os.environ.get("SPACE_ID") is not None
# ZeroGPU detection: check env var OR assume ZeroGPU for all HF Spaces (safer default)
# The SPACE_HARDWARE env var is unreliable, so we assume ZeroGPU if on HF Space
IS_ZEROGPU = IS_HUGGINGFACE_SPACE or os.environ.get("ZEROGPU") is not None


def get_gpu_memory_gb():
    """
    Get GPU memory in GB. Returns 0 if no GPU is available.
    """
    try:
        if torch.cuda.is_available():
            total_memory = torch.cuda.get_device_properties(0).total_memory
            memory_gb = total_memory / (1024**3)
            return memory_gb
        else:
            return 0
    except Exception as e:
        print(f"Warning: Failed to detect GPU memory: {e}", file=sys.stderr)
        return 0


def get_persistent_storage_path():
    """
    Detect and return a writable persistent storage path.
    """
    # Check for local checkpoint directory override (for development)
    checkpoint_dir_override = os.environ.get("CHECKPOINT_DIR")
    if checkpoint_dir_override:
        if checkpoint_dir_override.endswith("/checkpoints") or checkpoint_dir_override.endswith("\\checkpoints"):
            checkpoint_dir_override = os.path.dirname(checkpoint_dir_override)
        if os.path.exists(checkpoint_dir_override):
            print(f"Using local checkpoint directory (CHECKPOINT_DIR): {checkpoint_dir_override}")
            return checkpoint_dir_override
        else:
            print(f"Warning: CHECKPOINT_DIR path does not exist: {checkpoint_dir_override}")
    
    # Try HuggingFace Space persistent storage first
    hf_data_path = "/data"
    
    # Check if /data exists and is writable
    if os.path.exists(hf_data_path):
        try:
            test_file = os.path.join(hf_data_path, ".write_test")
            with open(test_file, 'w') as f:
                f.write("test")
            os.remove(test_file)
            print(f"Using HuggingFace persistent storage: {hf_data_path}")
            return hf_data_path
        except (PermissionError, OSError) as e:
            print(f"Warning: /data exists but is not writable: {e}")
    
    # Fall back to app directory (non-persistent but works without special config)
    fallback_path = os.path.join(current_dir, "data")
    os.makedirs(fallback_path, exist_ok=True)
    print(f"Using local storage (non-persistent): {fallback_path}")
    print("Note: To enable persistent storage, configure it in HuggingFace Space settings")
    return fallback_path


def main():
    """Main entry point for HuggingFace Space"""

    # موتورِ صدا همین اول روشن می‌شود تا مدلِ صدا (روی CPU) هم‌زمان با مدل‌های
    # آهنگ بالا بیاید؛ هیچ ربطی به GPU ندارد.
    if os.environ.get("VOICE_ENGINE_DISABLED", "").lower() not in ("1", "true", "yes"):
        voice_clone.start_engine()
    # فیلترِ محتوای آواتار (ncii_guard، پروسه‌ی جدا و فقط CPU) هم همین اول، در پس‌زمینه.
    if AVATAR_ENABLED:
        avatar_tab.start_guard_async()

    # Check for DEBUG_UI mode (skip model initialization for UI development)
    debug_ui = os.environ.get("DEBUG_UI", "").lower() in ("1", "true", "yes")
    if debug_ui:
        print("=" * 60)
        print("DEBUG_UI mode enabled - skipping model initialization")
        print("UI will be fully functional but generation is disabled")
        print("=" * 60)
    
    # Log ZeroGPU detection
    if IS_ZEROGPU:
        print("=" * 60)
        print("ZeroGPU environment detected")
        print("- Using spaces package for GPU allocation")
        print("- PyTorch backend forced for LLM (nano-vllm incompatible)")
        print("- GPU will be allocated on-demand during generation")
        print("=" * 60)
    
    # Get persistent storage path (auto-detect)
    persistent_storage_path = get_persistent_storage_path()
    
    # Detect GPU memory for auto-configuration
    # Note: In ZeroGPU, GPU may not be available during startup, so this may return 0
    gpu_memory_gb = get_gpu_memory_gb()
    
    # For ZeroGPU, we don't need CPU offload as GPU is allocated dynamically
    if IS_ZEROGPU:
        auto_offload = False
        print("ZeroGPU: CPU offload disabled (GPU allocated on-demand)")
    else:
        auto_offload = gpu_memory_gb > 0 and gpu_memory_gb < 16
    
    if not debug_ui and not IS_ZEROGPU:
        if auto_offload:
            print(f"Detected GPU memory: {gpu_memory_gb:.2f} GB (< 16GB)")
            print("Auto-enabling CPU offload to reduce GPU memory usage")
        elif gpu_memory_gb > 0:
            print(f"Detected GPU memory: {gpu_memory_gb:.2f} GB (>= 16GB)")
            print("CPU offload disabled by default")
        else:
            print("No GPU detected, running on CPU")
    
    # Create handler instances
    print("Creating handlers...")
    dit_handler = AceStepHandler(persistent_storage_path=persistent_storage_path)
    llm_handler = LLMHandler(persistent_storage_path=persistent_storage_path)
    dataset_handler = DatasetHandler()
    
    # Service mode configuration from environment variables
    # Default changed to the non-turbo XL SFT checkpoint (5B DiT, 50-step CFG sampling)
    # for maximum audio quality. This is the same weights as
    # ACE-Step/acestep-v15-xl-sft-diffusers (just packaged for this handler
    # instead of HF `diffusers`), and is the highest-quality checkpoint ACE-Step
    # currently ships. Override with the SERVICE_MODE_DIT_MODEL env var if you
    # want turbo speed instead (e.g. "acestep-v15-xl-turbo", 8 steps, no CFG).
    config_path = os.environ.get(
        "SERVICE_MODE_DIT_MODEL",
        "acestep-v15-xl-sft"
    )
    # Second DiT model: disabled by default because xl-sft + xl-turbo loaded
    # together needs ~18GB+ VRAM. Set SERVICE_MODE_DIT_MODEL_2 (e.g. to
    # "acestep-v15-xl-turbo") if your GPU has enough VRAM and you want a fast
    # draft model alongside the quality model in the UI dropdown.
    config_path_2 = os.environ.get("SERVICE_MODE_DIT_MODEL_2", "").strip()
    
    lm_model_path = os.environ.get(
        "SERVICE_MODE_LM_MODEL",
        "acestep-5Hz-lm-1.7B"
    )
    # For ZeroGPU, force PyTorch backend (nano-vllm uses direct CUDA APIs)
    if IS_ZEROGPU:
        backend = "pt"
    else:
        backend = os.environ.get("SERVICE_MODE_BACKEND", "vllm")
    device = "auto"
    
    print(f"Service mode configuration:")
    print(f"  DiT model 1: {config_path}")
    if config_path_2:
        print(f"  DiT model 2: {config_path_2}")
    print(f"  LM model: {lm_model_path}")
    print(f"  Backend: {backend}")
    print(f"  Offload to CPU: {auto_offload}")
    print(f"  DEBUG_UI: {debug_ui}")
    print(f"  ZeroGPU: {IS_ZEROGPU}")
    
    # Determine flash attention availability
    use_flash_attention = dit_handler.is_flash_attention_available()
    print(f"  Flash Attention: {use_flash_attention}")
    
    # Initialize models (skip in DEBUG_UI mode)
    init_status = ""
    enable_generate = False
    dit_handler_2 = None
    
    if debug_ui:
        # In DEBUG_UI mode, skip all model initialization
        init_status = "⚠️ DEBUG_UI mode - models not loaded\nUI is functional but generation is disabled"
        enable_generate = False
        print("Skipping model initialization (DEBUG_UI mode)")
    else:
        # Initialize primary DiT model
        print(f"Initializing DiT model 1: {config_path}...")
        init_status, enable_generate = dit_handler.initialize_service(
            project_root=current_dir,
            config_path=config_path,
            device=device,
            use_flash_attention=use_flash_attention,
            compile_model=False,
            offload_to_cpu=auto_offload,
            offload_dit_to_cpu=False
        )
        
        if not enable_generate:
            print(f"Warning: DiT model 1 initialization issue: {init_status}", file=sys.stderr)
        else:
            print("DiT model 1 initialized successfully")
        
        # Initialize second DiT model if configured
        if config_path_2:
            print(f"Initializing DiT model 2: {config_path_2}...")
            dit_handler_2 = AceStepHandler(persistent_storage_path=persistent_storage_path)
            
            # Share VAE, text_encoder, and silence_latent from the first handler to save memory
            init_status_2, enable_generate_2 = dit_handler_2.initialize_service(
                project_root=current_dir,
                config_path=config_path_2,
                device=device,
                use_flash_attention=use_flash_attention,
                compile_model=False,
                offload_to_cpu=auto_offload,
                offload_dit_to_cpu=False,
                # Share components from first handler
                shared_vae=dit_handler.vae,
                shared_text_encoder=dit_handler.text_encoder,
                shared_text_tokenizer=dit_handler.text_tokenizer,
                shared_silence_latent=dit_handler.silence_latent,
            )
            
            if not enable_generate_2:
                print(f"Warning: DiT model 2 initialization issue: {init_status_2}", file=sys.stderr)
                init_status += f"\n⚠️ DiT model 2 failed: {init_status_2}"
            else:
                print("DiT model 2 initialized successfully")
                init_status += f"\n✅ DiT model 2: {config_path_2}"
        
        # Initialize LM model
        checkpoint_dir = dit_handler._get_checkpoint_dir()
        print(f"Initializing 5Hz LM: {lm_model_path}...")
        lm_status, lm_success = llm_handler.initialize(
            checkpoint_dir=checkpoint_dir,
            lm_model_path=lm_model_path,
            backend=backend,
            device=device,
            offload_to_cpu=auto_offload,
            dtype=dit_handler.dtype
        )
        
        if lm_success:
            print("5Hz LM initialized successfully")
            init_status += f"\n{lm_status}"
        else:
            print(f"Warning: 5Hz LM initialization failed: {lm_status}", file=sys.stderr)
            init_status += f"\n{lm_status}"

    # Build available models list for UI
    available_dit_models = [config_path]
    if config_path_2 and dit_handler_2 is not None:
        available_dit_models.append(config_path_2)
    
    # Prepare initialization parameters for UI
    init_params = {
        'pre_initialized': True,
        'service_mode': True,
        'checkpoint': None,
        'config_path': config_path,
        'config_path_2': config_path_2 if config_path_2 else None,
        'device': device,
        'init_llm': True,
        'lm_model_path': lm_model_path,
        'backend': backend,
        'use_flash_attention': use_flash_attention,
        'offload_to_cpu': auto_offload,
        'offload_dit_to_cpu': False,
        'init_status': init_status,
        'enable_generate': enable_generate,
        'dit_handler': dit_handler,
        'dit_handler_2': dit_handler_2,
        'available_dit_models': available_dit_models,
        'llm_handler': llm_handler,
        'language': 'en',
        'persistent_storage_path': persistent_storage_path,
        'debug_ui': debug_ui,
    }
    
    print("Service initialization completed!")
    
    # Create Gradio interface with pre-initialized handlers
    print("Creating Gradio interface...")
    demo = create_gradio_interface(
        dit_handler,
        llm_handler,
        dataset_handler,
        init_params=init_params,
        language='en',
        # تبِ دوم: «کلون کردن صدا» (همان رابطِ اسپیسِ صدای قبلی)
        # تبِ سوم: «آواتار سخنگو» (همان رابطِ اسپیسِ آواتارِ قبلی، بدونِ بخشِ فایلِ .pt)
        extra_tabs=[("🎙️ کلون کردن صدا", "voice", voice_clone.build_voice_tab)]
        + ([("🗣️ آواتار سخنگو", "avatar", avatar_tab.build_avatar_tab)] if AVATAR_ENABLED else []),
    )

    # مدلِ آواتار درست بعد از اینکه ZeroGPU وزن‌های آهنگ را بسته‌بندی (و از رم آزاد) کرد
    # بالا می‌آید، ولی هنوز پیش از اولین درخواست؛ یعنی کمترین اوجِ مصرفِ رم و باز هم
    # «وقتی اسپیس بالا آمد همه‌چیز آماده است». بدونِ ZeroGPU همین‌جا بالا می‌آید.
    if AVATAR_ENABLED and not debug_ui:
        if not avatar_tab.start_after_zerogpu_pack():
            print("Loading the talking-avatar model (MiniMax-H3)...")
            avatar_tab.load_models()
    
    # پیاده‌سازی سرویس اختصاصی آپلود اشتراکی برای اشتراک‌گذاری بی‌نقص فایل‌های صوتی بین کانتینرهای ZeroGPU
    from fastapi import UploadFile, File
    import shutil
    
    @demo.app.post("/custom_upload")
    async def custom_upload(file: UploadFile = File(...)):
        # ساخت دایرکتوری در حافظه مشترک و دائم پروژه
        uploads_dir = os.path.join(current_dir, "data", "shared_uploads")
        os.makedirs(uploads_dir, exist_ok=True)
        
        # ذخیره‌سازی مطمئن فایل صوتی — فقط نامِ فایل، بدونِ مسیر (نامی مثلِ «../../app.py» نباید
        # بیرون از همین پوشه چیزی بنویسد یا فایل‌های خودِ اسپیس را عوض کند)
        safe_name = os.path.basename((file.filename or "").replace("\\", "/"))
        if safe_name in ("", ".", ".."):
            safe_name = "upload.bin"
        file_path = os.path.join(uploads_dir, safe_name)
        with open(file_path, "wb") as buffer:
            shutil.copyfileobj(file.file, buffer)
            
        return {"path": file_path}
    
    # فعال کردن پویا و مستقیم دکمه‌های دانلود برای تمام کامپوننت‌های صوتی
    for component in demo.blocks.values():
        if isinstance(component, gr.Audio):
            component.buttons = ["play", "download"]
            
    # Enable queue for multi-user support
    print("Enabling queue for multi-user support...")
    demo.queue(max_size=20)
    
    # Launch
    print("Launching server on 0.0.0.0:7860...")
    # ssr_mode=False: رندرِ سمتِ سرور (Node) فقط CPU مصرف می‌کرد و در لاگ خطای «[405] POST /upload»
    # می‌داد؛ این اسپیس بیشتر از راهِ API استفاده می‌شود و صفحه‌اش بدونِ آن هم کامل کار می‌کند.
    demo.launch(
        server_name="0.0.0.0",
        server_port=7860,
        share=False,
        show_error=True,
        ssr_mode=False,
    )


if __name__ == "__main__":
    main()