techfreakworm commited on
Commit
49b81d4
·
unverified ·
1 Parent(s): c77559f

LoRA per-request application (ZeroGPU forks don't persist model mutations)

Browse files

Adapter is applied inside each generation fork and unloaded after, instead of a
global apply+toggle (which crashed on ZeroGPU with CUDA-init-outside-fork and
wouldn't persist across forks anyway). Clone gets an optional adapter field;
LoRA Lab = inspect + self-contained quick-test; save-voice/design-bridge are now
GPU-decorated too.

Files changed (1) hide show
  1. app.py +89 -55
app.py CHANGED
@@ -21,7 +21,19 @@ from qvs.registry import ModelRegistry
21
  from qvs.ui import theme
22
 
23
  REG = ModelRegistry()
24
- MGR = AdapterManager()
 
 
 
 
 
 
 
 
 
 
 
 
25
 
26
  # NOTE: on ZeroGPU we deliberately do NOT preload at module level. A 14 GB
27
  # download + all-3 load + tensor-packing at import overran the Space startup
@@ -43,9 +55,8 @@ if not on_zerogpu():
43
 
44
  # ---- helpers -----------------------------------------------------------------
45
  def meter_html() -> str:
46
- if MGR.info:
47
- state = "on" if MGR.info.enabled else "off"
48
- lora = f' · LoRA <b>{MGR.info.source.split("/")[-1]}</b> ({state})'
49
  else:
50
  lora = ""
51
  if on_zerogpu():
@@ -127,25 +138,36 @@ def do_design(text, instruct, language, longform, *adv):
127
 
128
 
129
  @gpu(duration=120)
130
- def do_clone(ref_audio, ref_text, xvec, voice_pick, use_adapter, text, language, longform, *adv):
131
  if not (text or "").strip():
132
  return None, status_line("Enter text to synthesize.", hot=True), meter_html()
133
- if MGR.info is not None:
134
- MGR.set_enabled(bool(use_adapter))
135
  t0 = time.time()
136
  model = REG.to_device("base")
137
- if voice_pick and voice_pick != NONE_VOICE:
138
- items = voices.load_voice(voice_pick)
139
- wav, sr = engine.synth_clone(model, text.strip(), config.LANGUAGES[language], gp(adv),
140
- voice_clone_prompt=items, longform=bool(longform))
141
- else:
142
- ref = qaudio.ref_from_gradio(ref_audio)
143
- if ref is None:
144
- return None, status_line("Upload reference audio or pick a saved voice.", hot=True), meter_html()
145
- if not xvec and not (ref_text or "").strip():
146
- return None, status_line("Add the reference transcript, or enable x-vector-only.", hot=True), meter_html()
147
- wav, sr = engine.synth_clone(model, text.strip(), config.LANGUAGES[language], gp(adv),
148
- ref_audio=ref, ref_text=(ref_text or None), x_vector_only=bool(xvec), longform=bool(longform))
 
 
 
 
 
 
 
 
 
 
 
 
 
149
  return qaudio.to_gradio(wav, sr), _done(t0, wav), meter_html()
150
 
151
 
@@ -163,47 +185,60 @@ def do_library_gen(voice_name, text, language, longform, *adv):
163
  return qaudio.to_gradio(wav, sr), _done(t0, wav), meter_html()
164
 
165
 
166
- @gpu(duration=90)
167
- def do_lora_quicktest(sentence):
168
- if not MGR.info:
169
- return None, status_line("Apply an adapter first.", hot=True)
170
- emb = load_speaker_embedding(MGR.info.source)
 
171
  if emb is None:
172
- return None, status_line("This adapter ships no voice — test it from the Clone tab with your own reference.", hot=True)
173
  import torch
174
  from qwen_tts import VoiceClonePromptItem
175
  model = REG.to_device("base")
176
- item = VoiceClonePromptItem(ref_code=None,
177
- ref_spk_embedding=torch.as_tensor(emb).to(model.device).to(torch.bfloat16),
178
- x_vector_only_mode=True, icl_mode=False, ref_text=None)
179
- wav, sr = engine.synth_clone(model, sentence.strip() or "Hello from the adapter.", "Auto",
180
- engine.GenParams(max_new_tokens=512), voice_clone_prompt=[item], longform=False)
181
- return qaudio.to_gradio(wav, sr), status_line("quick test done")
 
 
 
 
 
 
 
 
 
 
 
182
 
183
 
184
- # non-GPU management callbacks
185
  def do_apply_lora(source):
186
- if not (source or "").strip():
187
- return status_line("Enter a Hugging Face repo id or local path.", hot=True), meter_html(), gr.update()
 
188
  try:
189
- info = MGR.apply(REG.to_device("base"), source.strip())
 
190
  except Exception as e:
191
- return status_line(f"Couldn't load adapter: {type(e).__name__}: {e}", hot=True), meter_html(), gr.update()
192
- return _adapter_report(info), meter_html(), gr.update(value=True, interactive=True)
193
-
194
-
195
- def do_toggle_lora(enabled):
196
- MGR.set_enabled(bool(enabled))
197
- return meter_html()
198
 
199
 
200
  def do_unload_lora():
201
- if not MGR.info:
202
- return status_line("No adapter applied."), meter_html(), gr.update(value=False)
203
- MGR.unload(REG.get("base"))
204
- return status_line("removed adapter — Base restored"), meter_html(), gr.update(value=False)
205
 
206
 
 
207
  def do_save_voice(name, ref_audio, ref_text, xvec):
208
  if not (name or "").strip():
209
  return status_line("Give the voice a name.", hot=True)
@@ -224,6 +259,7 @@ def do_lora_voice_to_library(source, name):
224
  return status_line(f'saved "{(name or "lora_voice").strip()}" to library')
225
 
226
 
 
227
  def do_design_to_library(design_audio, design_text, name):
228
  if design_audio is None:
229
  return status_line("Generate a designed voice first.", hot=True)
@@ -253,7 +289,7 @@ def build() -> gr.Blocks:
253
  c_reftext = gr.Textbox(label="Reference transcript", lines=2, placeholder="What the reference says (improves fidelity).")
254
  c_xvec = gr.Checkbox(False, label="x-vector only (skip transcript, lower fidelity)")
255
  c_voice = gr.Dropdown([NONE_VOICE] + voices.list_voices(), value=NONE_VOICE, label="…or use a saved voice")
256
- c_useadapter = gr.Checkbox(False, label="Apply active LoRA adapter (manage in LoRA Lab)")
257
  c_text = gr.Textbox(label="Text to speak", lines=4, placeholder="Type what the cloned voice should say…")
258
  c_lang = gr.Dropdown(LANG_CHOICES, value="Auto (detect)", label="Language")
259
  c_long = gr.Checkbox(True, label="Long-form chunking")
@@ -263,7 +299,7 @@ def build() -> gr.Blocks:
263
  c_out = gr.Audio(label="Output", type="numpy", interactive=False)
264
  c_status = gr.HTML(status_line("Ready."))
265
  voice_pickers.append(c_voice)
266
- c_btn.click(do_clone, [c_ref, c_reftext, c_xvec, c_voice, c_useadapter, c_text, c_lang, c_long, *c_adv], [c_out, c_status, meter])
267
 
268
  # ---- Preset Voices ----
269
  with gr.Tab("Preset Voices"):
@@ -314,9 +350,8 @@ def build() -> gr.Blocks:
314
  with gr.Column():
315
  l_src = gr.Textbox(label="Adapter (HF repo id or local path)", value="loubna1101/Qwen3-TTS-Darija-LoRa")
316
  with gr.Row():
317
- l_apply = gr.Button("Apply", variant="primary", elem_classes="qvs-generate")
318
- l_toggle = gr.Checkbox(False, label="Adapter on", interactive=False)
319
- l_remove = gr.Button("Remove", variant="secondary")
320
  gr.HTML('<div class="qvs-eyebrow">save the adapter\'s bundled voice to your library</div>')
321
  with gr.Row():
322
  l_vname = gr.Textbox(label="Save voice as", value="darija_voice", scale=2)
@@ -327,10 +362,9 @@ def build() -> gr.Blocks:
327
  l_testtext = gr.Textbox(label="Test sentence", value="Salam, hada ikhtibar dyal les voix.", lines=2)
328
  l_testbtn = gr.Button("Quick test", variant="secondary")
329
  l_testout = gr.Audio(label="Quick test output", type="numpy", interactive=False)
330
- l_apply.click(do_apply_lora, [l_src], [l_status, meter, l_toggle])
331
- l_toggle.change(do_toggle_lora, [l_toggle], [meter])
332
- l_remove.click(do_unload_lora, None, [l_status, meter, l_toggle])
333
- l_testbtn.click(do_lora_quicktest, [l_testtext], [l_testout, l_status])
334
 
335
  # ---- Voice Library ----
336
  with gr.Tab("Voice Library"):
 
21
  from qvs.ui import theme
22
 
23
  REG = ModelRegistry()
24
+ # LoRA is applied per-generation *inside* the @spaces.GPU fork: ZeroGPU forks do
25
+ # not persist in-place model mutations across requests, so "apply once, use later"
26
+ # can't work there. We track only the selected adapter here (a plain string).
27
+ SELECTED_ADAPTER = {"source": ""}
28
+
29
+
30
+ def _apply_adapter(model, source: str):
31
+ """Attach the adapter to a fresh manager (caller must .unload after gen)."""
32
+ if not (source or "").strip():
33
+ return None
34
+ mgr = AdapterManager()
35
+ mgr.apply(model, source.strip())
36
+ return mgr
37
 
38
  # NOTE: on ZeroGPU we deliberately do NOT preload at module level. A 14 GB
39
  # download + all-3 load + tensor-packing at import overran the Space startup
 
55
 
56
  # ---- helpers -----------------------------------------------------------------
57
  def meter_html() -> str:
58
+ if SELECTED_ADAPTER["source"]:
59
+ lora = f' · LoRA <b>{SELECTED_ADAPTER["source"].split("/")[-1]}</b>'
 
60
  else:
61
  lora = ""
62
  if on_zerogpu():
 
138
 
139
 
140
  @gpu(duration=120)
141
+ def do_clone(ref_audio, ref_text, xvec, voice_pick, adapter_source, text, language, longform, *adv):
142
  if not (text or "").strip():
143
  return None, status_line("Enter text to synthesize.", hot=True), meter_html()
 
 
144
  t0 = time.time()
145
  model = REG.to_device("base")
146
+ lora = None
147
+ try:
148
+ if (adapter_source or "").strip():
149
+ try:
150
+ lora = _apply_adapter(model, adapter_source)
151
+ except Exception as e:
152
+ return None, status_line(f"adapter error: {type(e).__name__}: {e}", hot=True), meter_html()
153
+ if voice_pick and voice_pick != NONE_VOICE:
154
+ items = voices.load_voice(voice_pick)
155
+ wav, sr = engine.synth_clone(model, text.strip(), config.LANGUAGES[language], gp(adv),
156
+ voice_clone_prompt=items, longform=bool(longform))
157
+ else:
158
+ ref = qaudio.ref_from_gradio(ref_audio)
159
+ if ref is None:
160
+ return None, status_line("Upload reference audio or pick a saved voice.", hot=True), meter_html()
161
+ if not xvec and not (ref_text or "").strip():
162
+ return None, status_line("Add the reference transcript, or enable x-vector-only.", hot=True), meter_html()
163
+ wav, sr = engine.synth_clone(model, text.strip(), config.LANGUAGES[language], gp(adv),
164
+ ref_audio=ref, ref_text=(ref_text or None), x_vector_only=bool(xvec), longform=bool(longform))
165
+ finally:
166
+ if lora is not None:
167
+ try:
168
+ lora.unload(model)
169
+ except Exception:
170
+ pass
171
  return qaudio.to_gradio(wav, sr), _done(t0, wav), meter_html()
172
 
173
 
 
185
  return qaudio.to_gradio(wav, sr), _done(t0, wav), meter_html()
186
 
187
 
188
+ @gpu(duration=120)
189
+ def do_lora_quicktest(source, sentence):
190
+ src = (source or "").strip()
191
+ if not src:
192
+ return None, status_line("Enter an adapter (repo id or path) above first.", hot=True)
193
+ emb = load_speaker_embedding(src)
194
  if emb is None:
195
+ return None, status_line("This adapter ships no voice — use it in the Clone tab with your own reference.", hot=True)
196
  import torch
197
  from qwen_tts import VoiceClonePromptItem
198
  model = REG.to_device("base")
199
+ lora = None
200
+ try:
201
+ lora = _apply_adapter(model, src)
202
+ item = VoiceClonePromptItem(ref_code=None,
203
+ ref_spk_embedding=torch.as_tensor(emb).to(model.device).to(torch.bfloat16),
204
+ x_vector_only_mode=True, icl_mode=False, ref_text=None)
205
+ wav, sr = engine.synth_clone(model, sentence.strip() or "Hello from the adapter.", "Auto",
206
+ engine.GenParams(max_new_tokens=512), voice_clone_prompt=[item], longform=False)
207
+ return qaudio.to_gradio(wav, sr), status_line("quick test done")
208
+ except Exception as e:
209
+ return None, status_line(f"quick test failed: {type(e).__name__}: {e}", hot=True)
210
+ finally:
211
+ if lora is not None:
212
+ try:
213
+ lora.unload(model)
214
+ except Exception:
215
+ pass
216
 
217
 
218
+ # management callbacks — validate/inspect only (adapter is applied per generation)
219
  def do_apply_lora(source):
220
+ src = (source or "").strip()
221
+ if not src:
222
+ return status_line("Enter a Hugging Face repo id or local path.", hot=True), meter_html()
223
  try:
224
+ from qvs.lora import read_adapter_config, resolve_adapter
225
+ cfg = read_adapter_config(resolve_adapter(src))
226
  except Exception as e:
227
+ return status_line(f"Couldn't load adapter: {type(e).__name__}: {e}", hot=True), meter_html()
228
+ SELECTED_ADAPTER["source"] = src
229
+ has_emb = load_speaker_embedding(src) is not None
230
+ targets = ", ".join(t.replace("_proj", "") for t in (cfg.get("target_modules") or []))
231
+ emb = " · ships a voice" if has_emb else ""
232
+ return (status_line(f"selected <b>{src.split('/')[-1]}</b> · r={cfg.get('r')} α={cfg.get('lora_alpha')} · "
233
+ f"{targets}{emb} — applied per generation (Clone tab or Quick test)"), meter_html())
234
 
235
 
236
  def do_unload_lora():
237
+ SELECTED_ADAPTER["source"] = ""
238
+ return status_line("adapter cleared."), meter_html()
 
 
239
 
240
 
241
+ @gpu(duration=90)
242
  def do_save_voice(name, ref_audio, ref_text, xvec):
243
  if not (name or "").strip():
244
  return status_line("Give the voice a name.", hot=True)
 
259
  return status_line(f'saved "{(name or "lora_voice").strip()}" to library')
260
 
261
 
262
+ @gpu(duration=90)
263
  def do_design_to_library(design_audio, design_text, name):
264
  if design_audio is None:
265
  return status_line("Generate a designed voice first.", hot=True)
 
289
  c_reftext = gr.Textbox(label="Reference transcript", lines=2, placeholder="What the reference says (improves fidelity).")
290
  c_xvec = gr.Checkbox(False, label="x-vector only (skip transcript, lower fidelity)")
291
  c_voice = gr.Dropdown([NONE_VOICE] + voices.list_voices(), value=NONE_VOICE, label="…or use a saved voice")
292
+ c_adapter = gr.Textbox(label="LoRA adapter (optional HF repo id)", placeholder="e.g. loubna1101/Qwen3-TTS-Darija-LoRa")
293
  c_text = gr.Textbox(label="Text to speak", lines=4, placeholder="Type what the cloned voice should say…")
294
  c_lang = gr.Dropdown(LANG_CHOICES, value="Auto (detect)", label="Language")
295
  c_long = gr.Checkbox(True, label="Long-form chunking")
 
299
  c_out = gr.Audio(label="Output", type="numpy", interactive=False)
300
  c_status = gr.HTML(status_line("Ready."))
301
  voice_pickers.append(c_voice)
302
+ c_btn.click(do_clone, [c_ref, c_reftext, c_xvec, c_voice, c_adapter, c_text, c_lang, c_long, *c_adv], [c_out, c_status, meter])
303
 
304
  # ---- Preset Voices ----
305
  with gr.Tab("Preset Voices"):
 
350
  with gr.Column():
351
  l_src = gr.Textbox(label="Adapter (HF repo id or local path)", value="loubna1101/Qwen3-TTS-Darija-LoRa")
352
  with gr.Row():
353
+ l_apply = gr.Button("Load & inspect", variant="primary", elem_classes="qvs-generate")
354
+ l_remove = gr.Button("Clear", variant="secondary")
 
355
  gr.HTML('<div class="qvs-eyebrow">save the adapter\'s bundled voice to your library</div>')
356
  with gr.Row():
357
  l_vname = gr.Textbox(label="Save voice as", value="darija_voice", scale=2)
 
362
  l_testtext = gr.Textbox(label="Test sentence", value="Salam, hada ikhtibar dyal les voix.", lines=2)
363
  l_testbtn = gr.Button("Quick test", variant="secondary")
364
  l_testout = gr.Audio(label="Quick test output", type="numpy", interactive=False)
365
+ l_apply.click(do_apply_lora, [l_src], [l_status, meter])
366
+ l_remove.click(do_unload_lora, None, [l_status, meter])
367
+ l_testbtn.click(do_lora_quicktest, [l_src, l_testtext], [l_testout, l_status])
 
368
 
369
  # ---- Voice Library ----
370
  with gr.Tab("Voice Library"):