zerogpu: eager model load fixes consecutive-call crash; ghost dial 3/4 distinct
Browse files- app.py +18 -1
- scripts/mondegreen.py +44 -8
app.py
CHANGED
|
@@ -272,7 +272,12 @@ class TTSEngine:
|
|
| 272 |
# Lazy-loaded inside generate(), which on ZeroGPU runs under @spaces.GPU, so
|
| 273 |
# F5-TTS's auto device detection picks the allocated GPU. (On the T4 Space it
|
| 274 |
# loads to cuda directly; locally it falls back to CPU.)
|
| 275 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 276 |
self.live = True
|
| 277 |
print(f"[engine] F5-TTS base loaded (device={self._tts.device})")
|
| 278 |
if LORA_PATH:
|
|
@@ -402,6 +407,18 @@ class TTSEngine:
|
|
| 402 |
|
| 403 |
ENGINE = TTSEngine()
|
| 404 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 405 |
|
| 406 |
# ----- crossfading for Morph mode -----
|
| 407 |
|
|
|
|
| 272 |
# Lazy-loaded inside generate(), which on ZeroGPU runs under @spaces.GPU, so
|
| 273 |
# F5-TTS's auto device detection picks the allocated GPU. (On the T4 Space it
|
| 274 |
# loads to cuda directly; locally it falls back to CPU.)
|
| 275 |
+
# On ZeroGPU force device="cuda" so the model registers its CUDA placement at
|
| 276 |
+
# import under `spaces`' deferred-CUDA interception (see eager load after ENGINE).
|
| 277 |
+
# Auto-detect would pick CPU at import (no real GPU in the main process) and never
|
| 278 |
+
# bind to the GPU. On T4/local, device=None auto-detects (cuda on T4, cpu locally).
|
| 279 |
+
self._tts = F5TTS(model="F5TTS_v1_Base",
|
| 280 |
+
device=("cuda" if _ON_ZEROGPU else None))
|
| 281 |
self.live = True
|
| 282 |
print(f"[engine] F5-TTS base loaded (device={self._tts.device})")
|
| 283 |
if LORA_PATH:
|
|
|
|
| 407 |
|
| 408 |
ENGINE = TTSEngine()
|
| 409 |
|
| 410 |
+
# ZeroGPU: load the model at IMPORT, not lazily inside the first @spaces.GPU call. Lazy
|
| 411 |
+
# loading initialized real CUDA inside the first GPU worker, after ZeroGPU's fork-server had
|
| 412 |
+
# already snapshotted the process, so the first call worked but the SECOND died in worker_init
|
| 413 |
+
# ("No CUDA GPUs are available"). Loading here puts the model's CUDA placement under `spaces`'
|
| 414 |
+
# deferred-CUDA tracking, so every GPU call re-materializes it and consecutive calls work.
|
| 415 |
+
# Hedged: if eager load fails, _tts stays None and the lazy path still runs (no worse off).
|
| 416 |
+
if _ON_ZEROGPU:
|
| 417 |
+
try:
|
| 418 |
+
ENGINE._ensure()
|
| 419 |
+
except Exception as _e:
|
| 420 |
+
print(f"[engine] eager ZeroGPU load failed, will lazy-load: {_e}")
|
| 421 |
+
|
| 422 |
|
| 423 |
# ----- crossfading for Morph mode -----
|
| 424 |
|
scripts/mondegreen.py
CHANGED
|
@@ -76,6 +76,24 @@ LEVEL_P = [0.0, 0.25, 0.50, 0.75, 1.0] # per-word substitution prob
|
|
| 76 |
# lv4 (p=1.0, full dissolution) reaches 10 so every content word actually transforms.
|
| 77 |
LEVEL_MAX_DIST = [0.0, 3.0, 5.0, 7.5, 10.0]
|
| 78 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 79 |
# Minimum word-frequency (zipf scale, log10 per-billion) for a candidate to qualify.
|
| 80 |
# CMUdict has ~135k entries including rare surnames / abbreviations ("selz" zipf 1.4).
|
| 81 |
# zipf >= 2.5 keeps real mishearings ("seashells" 2.5, "reefer" 2.7) and rejects junk.
|
|
@@ -317,10 +335,12 @@ class MondegreenIndex:
|
|
| 317 |
low = tok.lower()
|
| 318 |
if low in _FUNCTION_WORDS:
|
| 319 |
continue
|
| 320 |
-
#
|
| 321 |
-
#
|
| 322 |
-
#
|
| 323 |
-
|
|
|
|
|
|
|
| 324 |
if not cands:
|
| 325 |
continue
|
| 326 |
substitutable.append(i)
|
|
@@ -341,9 +361,20 @@ class MondegreenIndex:
|
|
| 341 |
chosen = sorted(substitutable, key=lambda i: (best_dist[i], i))[:k]
|
| 342 |
chosen_set = set(chosen)
|
| 343 |
|
|
|
|
|
|
|
|
|
|
|
|
|
| 344 |
per_position: list[list[tuple[str, float]] | None] = []
|
| 345 |
for i, tok in enumerate(tokens):
|
| 346 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 347 |
|
| 348 |
# Beam search. Each beam = (sequence_so_far: list[str], cumulative_log_prob: float).
|
| 349 |
beams: list[tuple[list[str], float]] = [([], 0.0)]
|
|
@@ -366,9 +397,14 @@ class MondegreenIndex:
|
|
| 366 |
# Reuses prefix cache internally; this is fast.
|
| 367 |
partial_text = self._compose_partial(tokens, pos, new_seq)
|
| 368 |
lm_score = reranker.score_next_token(partial_text)
|
| 369 |
-
#
|
| 370 |
-
|
| 371 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 372 |
if not expanded:
|
| 373 |
# Every candidate collided with an already-used word; keep the source.
|
| 374 |
beams = [(beam + [src_tok], score) for beam, score in beams]
|
|
|
|
| 76 |
# lv4 (p=1.0, full dissolution) reaches 10 so every content word actually transforms.
|
| 77 |
LEVEL_MAX_DIST = [0.0, 3.0, 5.0, 7.5, 10.0]
|
| 78 |
|
| 79 |
+
# Per-level preference on candidate phonetic distance, added to the LM coherence score
|
| 80 |
+
# during beam search. Negative = prefer the NEAREST (most natural) mishearing. Low dials
|
| 81 |
+
# pull toward the closest swap so the change reads as a near-miss ("sells -> sills"); the
|
| 82 |
+
# high dials sit at zero and let the nearest-skip below (LEVEL_SKIP_NEAR) do the work of
|
| 83 |
+
# reaching for farther, weirder mishearings, with the LM picking the most coherent one in
|
| 84 |
+
# that farther pool. Tuned against the LM score scale (mean per-token log-prob, ~-4 to -7).
|
| 85 |
+
LEVEL_DIST_PREF = [0.0, -0.18, -0.09, 0.0, 0.0]
|
| 86 |
+
|
| 87 |
+
# Fraction of each chosen word's NEAREST mishearings to drop from the beam's candidate pool,
|
| 88 |
+
# per level. Zero at low dials so they keep the most natural (closest) swap. Rising at the top
|
| 89 |
+
# so the same word is pushed past its obvious mishearing onto a farther, weirder one. This is
|
| 90 |
+
# the saturation fix: a short sentence ("she sells seashells by the seashore", three content
|
| 91 |
+
# words) has every word already changed by dial 3, so dial 4 has nothing new to change unless
|
| 92 |
+
# it changes the SAME words further. The soft LEVEL_DIST_PREF alone can't dislodge a strongly
|
| 93 |
+
# coherent near pick (the LM loves "seashells -> seagulls"); dropping the nearest candidates
|
| 94 |
+
# forces the beam off it. Capped per-word so a short candidate ladder still keeps a handful.
|
| 95 |
+
LEVEL_SKIP_NEAR = [0.0, 0.0, 0.0, 0.20, 0.55]
|
| 96 |
+
|
| 97 |
# Minimum word-frequency (zipf scale, log10 per-billion) for a candidate to qualify.
|
| 98 |
# CMUdict has ~135k entries including rare surnames / abbreviations ("selz" zipf 1.4).
|
| 99 |
# zipf >= 2.5 keeps real mishearings ("seashells" 2.5, "reefer" 2.7) and rejects junk.
|
|
|
|
| 335 |
low = tok.lower()
|
| 336 |
if low in _FUNCTION_WORDS:
|
| 337 |
continue
|
| 338 |
+
# Pull a WIDE ladder (24 within dist 13), not just the nearest few: the level
|
| 339 |
+
# gates both the COUNT of changes (which words, below) and HOW FAR each one
|
| 340 |
+
# drifts (the nearest-skip below), so the top dial needs farther candidates to
|
| 341 |
+
# reach for. best_dist still uses the nearest, so word-change ORDER is unchanged.
|
| 342 |
+
cands = self.find_candidates(low, max_dist=13.0,
|
| 343 |
+
max_results=max(24, n_candidates_per_word))
|
| 344 |
if not cands:
|
| 345 |
continue
|
| 346 |
substitutable.append(i)
|
|
|
|
| 361 |
chosen = sorted(substitutable, key=lambda i: (best_dist[i], i))[:k]
|
| 362 |
chosen_set = set(chosen)
|
| 363 |
|
| 364 |
+
# Per-level: drop the nearest mishearings from each chosen word so high dials reach
|
| 365 |
+
# farther (the saturation fix; see LEVEL_SKIP_NEAR). Always keep >= 4 so the beam has
|
| 366 |
+
# room and a short candidate ladder doesn't collapse to a single forced choice.
|
| 367 |
+
skip_frac = LEVEL_SKIP_NEAR[level]
|
| 368 |
per_position: list[list[tuple[str, float]] | None] = []
|
| 369 |
for i, tok in enumerate(tokens):
|
| 370 |
+
if i not in chosen_set:
|
| 371 |
+
per_position.append(None)
|
| 372 |
+
continue
|
| 373 |
+
pool = cand_cache[i]
|
| 374 |
+
if skip_frac > 0.0 and len(pool) > 4:
|
| 375 |
+
drop = min(len(pool) - 4, int(skip_frac * len(pool)))
|
| 376 |
+
pool = pool[drop:]
|
| 377 |
+
per_position.append(pool)
|
| 378 |
|
| 379 |
# Beam search. Each beam = (sequence_so_far: list[str], cumulative_log_prob: float).
|
| 380 |
beams: list[tuple[list[str], float]] = [([], 0.0)]
|
|
|
|
| 397 |
# Reuses prefix cache internally; this is fast.
|
| 398 |
partial_text = self._compose_partial(tokens, pos, new_seq)
|
| 399 |
lm_score = reranker.score_next_token(partial_text)
|
| 400 |
+
# Distance preference scales with the dial (see LEVEL_DIST_PREF): low
|
| 401 |
+
# dials prefer the nearest, most natural mishearing; high dials prefer a
|
| 402 |
+
# farther, weirder one, so the same word drifts further as the dial rises
|
| 403 |
+
# and dial 4 stays distinct from dial 3 even when the substitution count
|
| 404 |
+
# has already saturated. Alphabetical micro-term keeps ties deterministic.
|
| 405 |
+
dist_pref = LEVEL_DIST_PREF[level] * cand_dist
|
| 406 |
+
alpha_tb = -ord(cand_word[0]) * 1e-9
|
| 407 |
+
expanded.append((new_seq, score + lm_score + dist_pref + alpha_tb))
|
| 408 |
if not expanded:
|
| 409 |
# Every candidate collided with an already-used word; keep the source.
|
| 410 |
beams = [(beam + [src_tok], score) for beam, score in beams]
|