akshan-main commited on
Commit
e8b267d
·
verified ·
1 Parent(s): 7f337de

zerogpu: eager model load fixes consecutive-call crash; ghost dial 3/4 distinct

Browse files
Files changed (2) hide show
  1. app.py +18 -1
  2. 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
- self._tts = F5TTS(model="F5TTS_v1_Base")
 
 
 
 
 
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
- # Generous cap (12): once we DECIDE to change a word, it should always find
321
- # its nearest real-word mishearing regardless of word length. The level
322
- # gates the COUNT of changes, not the per-word distance.
323
- cands = self.find_candidates(low, max_dist=12.0, max_results=n_candidates_per_word)
 
 
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
- per_position.append(cand_cache[i] if i in chosen_set else None)
 
 
 
 
 
 
 
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
- # Tie-breaker: phonetic distance (lower = better), then alphabetical.
370
- tb = (-cand_dist * 1e-6, -ord(cand_word[0]) * 1e-9)
371
- expanded.append((new_seq, score + lm_score + tb[0] + tb[1]))
 
 
 
 
 
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]