pums-tools / tools /TextToSpeech /ui_utils.py
ndmhung6's picture
tool Text to Speech
d88b951
Raw
History Blame Contribute Delete
6.02 kB
import time
import gc
import sys
import gradio as gr
import soundfile as sf
from functools import lru_cache
def _format_duration(seconds: float) -> str:
"""Format seconds into a human-readable duration string."""
seconds = max(0, int(round(seconds)))
minutes, secs = divmod(seconds, 60)
hours, minutes = divmod(minutes, 60)
if hours:
return f"{hours}h {minutes}m {secs}s"
if minutes:
return f"{minutes}m {secs}s"
return f"{secs}s"
def _split_estimate_status(status: str) -> tuple[str, str]:
"""Split a status string into status text and estimate text."""
if not isinstance(status, str):
return status, ""
estimate_marker = " | Ước tính còn lại: "
if estimate_marker in status:
status_text, estimate_text = status.split(" | ", 1)
if status.endswith("...") and not status_text.endswith("..."):
status_text += "..."
return status_text, estimate_text.rstrip(". ")
if ("batch mẫu:" in status or "trung bình batch:" in status) and "ước tính còn lại:" in status:
start = status.find("(")
end = status.rfind(")")
if start != -1 and end != -1 and end > start:
status_text = status[:start].strip()
estimate_text = status[start + 1:end].replace(", ", "\n")
return status_text, estimate_text
return status, ""
def _extract_progress(status: str) -> tuple[str, int, int] | None:
"""Extract progress (current/total) from a status string."""
if not isinstance(status, str):
return None
for marker, label in (("Đang xử lý batch ", "batch"), ("Đang xử lý đoạn ", "đoạn")):
if marker not in status:
continue
progress_text = status.split(marker, 1)[1].split(" ", 1)[0].strip(".")
if "/" not in progress_text:
return None
current_text, total_text = progress_text.split("/", 1)
try:
current = int(current_text)
total = int(total_text)
except ValueError:
return None
if current > 0 and total > 0:
return label, current, total
return None
def wrap_with_estimate(synthesize_fn):
"""Wrapper that adds time estimation to synthesis progress."""
def wrapper(*args):
previous_progress_time = None
total_unit_duration = 0.0
completed_units = 0
for audio_path, status in synthesize_fn(*args):
status_text, estimate_text = _split_estimate_status(status)
if not estimate_text:
progress = _extract_progress(status_text)
if progress:
unit_label, current, total = progress
now = time.time()
if previous_progress_time is not None:
total_unit_duration += now - previous_progress_time
completed_units += 1
previous_progress_time = now
if completed_units == 0:
estimate_text = f"Đang đo thời gian {unit_label} đầu tiên..."
else:
average_unit_duration = total_unit_duration / completed_units
estimated_total = average_unit_duration * total
estimated_remaining = average_unit_duration * max(0, total - current + 1)
estimate_text = (
f"Ước tính còn lại: {_format_duration(estimated_remaining)}\n"
f"Tổng: {_format_duration(estimated_total)}"
)
yield audio_path, status_text, estimate_text
return wrapper
def cleanup_gpu_memory():
"""Aggressively cleanup GPU memory (CUDA, MPS, XPU)."""
if 'torch' in sys.modules:
import torch
if hasattr(torch, 'cuda') and torch.cuda.is_available():
torch.cuda.empty_cache()
torch.cuda.synchronize()
if hasattr(torch, 'backends') and hasattr(torch.backends, 'mps') and torch.backends.mps.is_available():
torch.mps.empty_cache()
if hasattr(torch, 'xpu') and torch.xpu.is_available():
torch.xpu.empty_cache()
torch.xpu.synchronize()
gc.collect()
@lru_cache(maxsize=32)
def get_ref_text_cached(text_path: str) -> str:
"""Cache reference text loading."""
with open(text_path, "r", encoding="utf-8") as f:
return f.read()
def on_codec_change(codec: str, current_mode: str):
"""Handle codec selection changes — hide cloning tab for ONNX codecs."""
is_onnx = "onnx" in codec.lower()
if is_onnx and current_mode == "custom_mode":
return gr.update(visible=False), gr.update(selected="preset_mode"), "preset_mode"
return gr.update(visible=not is_onnx), gr.update(), current_mode
def validate_audio_duration(audio_path):
"""Validate that reference audio is within optimal duration (3-5s)."""
if not audio_path:
return gr.update(visible=False)
try:
info = sf.info(audio_path)
if info.duration > 5.1:
return gr.update(
value=f"⚠️ **Cảnh báo:** Audio mẫu hiện tại dài {info.duration:.1f} giây. Để có kết quả clone giọng tối ưu, bạn nên sử dụng đoạn audio có độ dài lý tưởng từ **3 đến 5 giây**.",
visible=True
)
except Exception:
pass
return gr.update(visible=False)
def on_custom_id_change(model_id):
"""Auto detect LoRA and suggest base model."""
if model_id and "lora" in model_id.lower():
if "0.3" in model_id:
base_model = "VieNeu-TTS-0.3B (GPU)"
else:
base_model = "VieNeu-TTS (GPU)"
return (
gr.update(visible=True, value=base_model),
gr.update(), gr.update()
)
return (
gr.update(visible=False),
gr.update(),
gr.update()
)