PlotweaverModel commited on
Commit
df0d30b
Β·
verified Β·
1 Parent(s): 5b62e76

Upload app.py

Browse files
Files changed (1) hide show
  1. app.py +141 -39
app.py CHANGED
@@ -144,15 +144,30 @@ def resolve_text(text_input, file_input):
144
  # SINGLE-SPEAKER MODES
145
  # ==========================================
146
  @spaces.GPU
147
- def generate_custom_voice(text, file_input, language, speaker_label, instruction):
148
  resolved_text = resolve_text(text, file_input)
149
- model = get_model("custom")
150
  speaker = speaker_label.split("--")[0].strip()
151
  lang = language if language != "Auto" else "Auto"
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
152
  kwargs = {"text": resolved_text, "language": lang, "speaker": speaker}
153
  if instruction and instruction.strip():
154
  kwargs["instruct"] = instruction.strip()
155
- print(f"[TTS] Custom: speaker={speaker}, lang={lang}")
156
  wavs, sr = model.generate_custom_voice(**kwargs)
157
  path = os.path.join(OUTPUT_DIR, f"custom_{int(time.time())}.wav")
158
  sf.write(path, wavs[0], sr)
@@ -160,23 +175,39 @@ def generate_custom_voice(text, file_input, language, speaker_label, instruction
160
 
161
 
162
  @spaces.GPU
163
- def generate_voice_design(text, file_input, language, voice_description):
164
  resolved_text = resolve_text(text, file_input)
165
  if not voice_description.strip():
166
  raise gr.Error("Please describe the voice you want.")
167
- model = get_model("design")
168
  lang = language if language != "Auto" else "Auto"
169
- print(f"[TTS] Design: lang={lang}, desc={voice_description[:60]}")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
170
  wavs, sr = model.generate_voice_design(text=resolved_text, language=lang, instruct=voice_description)
171
  path = os.path.join(OUTPUT_DIR, f"design_{int(time.time())}.wav")
172
  sf.write(path, wavs[0], sr)
173
  return path
174
 
175
 
176
- def enhance_text_with_emotions(client, text):
177
- """Use AI to rewrite text with emotional delivery cues that TTS picks up naturally."""
178
  if not client:
179
- return text
180
  try:
181
  response = client.chat.completions.create(
182
  model=OMNI_MODEL, modalities=["text"],
@@ -184,32 +215,93 @@ def enhance_text_with_emotions(client, text):
184
  {
185
  "role": "system",
186
  "content": (
187
- "You are an audiobook performance coach. Rewrite the text to add natural "
188
- "emotional delivery cues that a TTS voice will pick up from context.\n\n"
189
- "Techniques:\n"
190
- "- Add pause markers: '...' for dramatic pauses\n"
191
- "- Use punctuation for emphasis: '!' for energy, '?' for curiosity\n"
192
- "- Add breath/pacing cues with commas and dashes\n"
193
- "- Extend words for emphasis: 'sooo beautiful'\n"
194
- "- Add interjections: 'Oh!', 'Hmm...', 'Ah,'\n"
195
- "- Use ellipsis for trailing off: 'I never thought...'\n\n"
196
  "Rules:\n"
197
- "1. Keep ALL the original meaning and content\n"
198
- "2. Do NOT add stage directions or [brackets]\n"
199
- "3. The output must be speakable text only\n"
200
- "4. Be subtle β€” enhance, don't overdo\n"
201
- "5. Output ONLY the enhanced text"
 
 
202
  ),
203
  },
204
- {"role": "user", "content": f"Enhance this for emotional TTS delivery:\n\n{text}"},
205
  ],
206
  )
207
- enhanced = response.choices[0].message.content.strip()
208
- print(f"[Emotions] Original: {len(text)} chars -> Enhanced: {len(enhanced)} chars")
209
- return enhanced
 
 
 
 
 
210
  except Exception as e:
211
- print(f"[Emotions] Enhancement failed: {e}")
212
- return text
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
213
 
214
 
215
  def extract_pdf_sections(filepath):
@@ -277,23 +369,29 @@ def generate_voice_clone(text, file_input, language, ref_audio, ref_text, add_em
277
  if ref_audio is None:
278
  raise gr.Error("Please upload a reference audio sample.")
279
 
280
- # Enhance text with emotions if requested
281
- final_text = resolved_text
282
  if add_emotions:
283
  client = get_llm_client()
284
  if client:
285
- final_text = enhance_text_with_emotions(client, text)
286
- else:
287
- print("[Clone] No LLM client for emotion enhancement, using raw text")
 
 
 
 
 
 
 
 
288
 
 
289
  model = get_model("clone")
290
  lang = language if language != "Auto" else "Auto"
291
- kwargs = {"text": final_text, "language": lang, "ref_audio": ref_audio}
292
  if ref_text and ref_text.strip():
293
  kwargs["ref_text"] = ref_text.strip()
294
  else:
295
  kwargs["x_vector_only_mode"] = True
296
- print(f"[TTS] Clone: lang={lang}, emotions={'yes' if add_emotions else 'no'}")
297
  wavs, sr = model.generate_voice_clone(**kwargs)
298
  path = os.path.join(OUTPUT_DIR, f"clone_{int(time.time())}.wav")
299
  sf.write(path, wavs[0], sr)
@@ -608,13 +706,15 @@ with gr.Blocks(title="Qwen3-TTS Demo") as demo:
608
  cv_speaker = gr.Dropdown(choices=SPEAKER_CHOICES,
609
  value="Ryan -- Dynamic male, strong rhythmic drive (English)",
610
  label="Speaker")
611
- cv_instruct = gr.Textbox(label="Emotion / Style (optional)",
612
  placeholder="e.g. Very happy, Whisper softly, Speak with authority...")
 
 
613
  cv_btn = gr.Button("Generate", variant="primary")
614
  with gr.Column():
615
  cv_audio = gr.Audio(label="Generated Speech", type="filepath")
616
  cv_btn.click(fn=generate_custom_voice,
617
- inputs=[cv_text, cv_file, cv_lang, cv_speaker, cv_instruct], outputs=cv_audio)
618
 
619
  # ── Tab 2: Voice Design ──
620
  with gr.Tab("Voice Design"):
@@ -637,11 +737,13 @@ with gr.Blocks(title="Qwen3-TTS Demo") as demo:
637
  ],
638
  inputs=[vd_desc], label="Examples",
639
  )
 
 
640
  vd_btn = gr.Button("Generate", variant="primary")
641
  with gr.Column():
642
  vd_audio = gr.Audio(label="Generated Speech", type="filepath")
643
  vd_btn.click(fn=generate_voice_design,
644
- inputs=[vd_text, vd_file, vd_lang, vd_desc], outputs=vd_audio)
645
 
646
  # ── Tab 3: Voice Clone ──
647
  with gr.Tab("Voice Clone"):
 
144
  # SINGLE-SPEAKER MODES
145
  # ==========================================
146
  @spaces.GPU
147
+ def generate_custom_voice(text, file_input, language, speaker_label, instruction, auto_emotions):
148
  resolved_text = resolve_text(text, file_input)
 
149
  speaker = speaker_label.split("--")[0].strip()
150
  lang = language if language != "Auto" else "Auto"
151
+
152
+ if auto_emotions:
153
+ client = get_llm_client()
154
+ if client:
155
+ tmp_dir = os.path.join(OUTPUT_DIR, f"cv_{int(time.time())}")
156
+ os.makedirs(tmp_dir, exist_ok=True)
157
+ segments = analyze_emotions_for_segments(client, resolved_text)
158
+ audio_files = generate_segments_with_emotions(
159
+ segments, language, speaker, "custom", tmp_dir,
160
+ )
161
+ if audio_files:
162
+ final = os.path.join(OUTPUT_DIR, f"custom_{int(time.time())}.wav")
163
+ concatenate_wavs(audio_files, final)
164
+ return final
165
+
166
+ # Fallback: single generation
167
+ model = get_model("custom")
168
  kwargs = {"text": resolved_text, "language": lang, "speaker": speaker}
169
  if instruction and instruction.strip():
170
  kwargs["instruct"] = instruction.strip()
 
171
  wavs, sr = model.generate_custom_voice(**kwargs)
172
  path = os.path.join(OUTPUT_DIR, f"custom_{int(time.time())}.wav")
173
  sf.write(path, wavs[0], sr)
 
175
 
176
 
177
  @spaces.GPU
178
+ def generate_voice_design(text, file_input, language, voice_description, auto_emotions):
179
  resolved_text = resolve_text(text, file_input)
180
  if not voice_description.strip():
181
  raise gr.Error("Please describe the voice you want.")
 
182
  lang = language if language != "Auto" else "Auto"
183
+
184
+ if auto_emotions:
185
+ client = get_llm_client()
186
+ if client:
187
+ tmp_dir = os.path.join(OUTPUT_DIR, f"vd_{int(time.time())}")
188
+ os.makedirs(tmp_dir, exist_ok=True)
189
+ segments = analyze_emotions_for_segments(client, resolved_text)
190
+ audio_files = generate_segments_with_emotions(
191
+ segments, language, None, "design", tmp_dir,
192
+ voice_desc=voice_description.strip(),
193
+ )
194
+ if audio_files:
195
+ final = os.path.join(OUTPUT_DIR, f"design_{int(time.time())}.wav")
196
+ concatenate_wavs(audio_files, final)
197
+ return final
198
+
199
+ # Fallback: single generation
200
+ model = get_model("design")
201
  wavs, sr = model.generate_voice_design(text=resolved_text, language=lang, instruct=voice_description)
202
  path = os.path.join(OUTPUT_DIR, f"design_{int(time.time())}.wav")
203
  sf.write(path, wavs[0], sr)
204
  return path
205
 
206
 
207
+ def analyze_emotions_for_segments(client, text):
208
+ """Split text into segments with emotion instructions for single-speaker mode."""
209
  if not client:
210
+ return [{"text": text, "emotion": ""}]
211
  try:
212
  response = client.chat.completions.create(
213
  model=OMNI_MODEL, modalities=["text"],
 
215
  {
216
  "role": "system",
217
  "content": (
218
+ "You are an audiobook director. Split this text into segments where "
219
+ "the emotional tone changes. For each segment, provide an emotion/delivery "
220
+ "instruction for the voice actor.\n\n"
221
+ "Output ONLY valid JSON:\n"
222
+ '{"segments": [\n'
223
+ ' {"text": "The lighthouse stood tall against the storm.", "emotion": "Atmospheric, steady, painting the scene"},\n'
224
+ ' {"text": "She ran to the door, heart pounding.", "emotion": "Urgent, breathless, rising tension"},\n'
225
+ ' {"text": "And then... silence.", "emotion": "Quiet, dramatic pause, barely above a whisper"}\n'
226
+ "]}\n\n"
227
  "Rules:\n"
228
+ "- Include ALL text, do not skip anything\n"
229
+ "- Keep segments in original order\n"
230
+ "- Each segment should be 1-4 sentences with a consistent emotion\n"
231
+ "- Be specific with emotions: not just 'sad' but 'quietly heartbroken, voice trailing off'\n"
232
+ "- Merge text with the same emotion\n"
233
+ "- Dialogue should have the emotion of the speaker\n"
234
+ "- Output ONLY JSON, no markdown, no backticks"
235
  ),
236
  },
237
+ {"role": "user", "content": f"Direct this text with emotions:\n\n{text[:6000]}"},
238
  ],
239
  )
240
+ raw = response.choices[0].message.content.strip()
241
+ raw = re.sub(r'^```json\s*', '', raw)
242
+ raw = re.sub(r'\s*```$', '', raw)
243
+ data = json.loads(raw)
244
+ segments = data.get("segments", [])
245
+ if segments:
246
+ print(f"[Emotions] Split into {len(segments)} emotional segments")
247
+ return segments
248
  except Exception as e:
249
+ print(f"[Emotions] Analysis failed: {e}")
250
+ return [{"text": text, "emotion": ""}]
251
+
252
+
253
+ @spaces.GPU
254
+ def generate_segments_with_emotions(segments, language, speaker, model_type, tmp_dir,
255
+ voice_desc=None, ref_audio=None, ref_text=None):
256
+ """Generate audio for multiple emotion-tagged segments using one voice."""
257
+ model = get_model(model_type)
258
+ lang = language if language != "Auto" else "Auto"
259
+ audio_files = []
260
+
261
+ # Create pause between segments
262
+ pause_path = os.path.join(tmp_dir, "seg_pause.wav")
263
+ generate_silence(0.6, pause_path)
264
+
265
+ for i, seg in enumerate(segments):
266
+ seg_text = seg.get("text", "").strip()
267
+ emotion = seg.get("emotion", "")
268
+ if not seg_text:
269
+ continue
270
+
271
+ path = os.path.join(tmp_dir, f"emoseg_{i:04d}.wav")
272
+ try:
273
+ if model_type == "custom":
274
+ kwargs = {"text": seg_text, "language": lang, "speaker": speaker}
275
+ if emotion:
276
+ kwargs["instruct"] = emotion
277
+ wavs, sr = model.generate_custom_voice(**kwargs)
278
+ elif model_type == "design":
279
+ instruct = voice_desc or ""
280
+ if emotion:
281
+ instruct = f"{instruct}. {emotion}" if instruct else emotion
282
+ wavs, sr = model.generate_voice_design(text=seg_text, language=lang, instruct=instruct)
283
+ elif model_type == "clone":
284
+ kwargs = {"text": seg_text, "language": lang, "ref_audio": ref_audio}
285
+ if ref_text:
286
+ kwargs["ref_text"] = ref_text
287
+ else:
288
+ kwargs["x_vector_only_mode"] = True
289
+ wavs, sr = model.generate_voice_clone(**kwargs)
290
+
291
+ sf.write(path, wavs[0], sr)
292
+ audio_files.append(path)
293
+ print(f"[Emotions] Seg {i}: '{emotion[:40]}' -> OK")
294
+ except Exception as e:
295
+ print(f"[Emotions] Seg {i} failed: {e}")
296
+ fail = os.path.join(tmp_dir, f"fail_{i:04d}.wav")
297
+ generate_silence(1.0, fail)
298
+ audio_files.append(fail)
299
+
300
+ # Add pause between segments
301
+ if i < len(segments) - 1:
302
+ audio_files.append(pause_path)
303
+
304
+ return audio_files
305
 
306
 
307
  def extract_pdf_sections(filepath):
 
369
  if ref_audio is None:
370
  raise gr.Error("Please upload a reference audio sample.")
371
 
 
 
372
  if add_emotions:
373
  client = get_llm_client()
374
  if client:
375
+ tmp_dir = os.path.join(OUTPUT_DIR, f"vc_{int(time.time())}")
376
+ os.makedirs(tmp_dir, exist_ok=True)
377
+ segments = analyze_emotions_for_segments(client, resolved_text)
378
+ audio_files = generate_segments_with_emotions(
379
+ segments, language, None, "clone", tmp_dir,
380
+ ref_audio=ref_audio, ref_text=ref_text if ref_text and ref_text.strip() else None,
381
+ )
382
+ if audio_files:
383
+ final = os.path.join(OUTPUT_DIR, f"clone_{int(time.time())}.wav")
384
+ concatenate_wavs(audio_files, final)
385
+ return final
386
 
387
+ # Fallback: single generation
388
  model = get_model("clone")
389
  lang = language if language != "Auto" else "Auto"
390
+ kwargs = {"text": resolved_text, "language": lang, "ref_audio": ref_audio}
391
  if ref_text and ref_text.strip():
392
  kwargs["ref_text"] = ref_text.strip()
393
  else:
394
  kwargs["x_vector_only_mode"] = True
 
395
  wavs, sr = model.generate_voice_clone(**kwargs)
396
  path = os.path.join(OUTPUT_DIR, f"clone_{int(time.time())}.wav")
397
  sf.write(path, wavs[0], sr)
 
706
  cv_speaker = gr.Dropdown(choices=SPEAKER_CHOICES,
707
  value="Ryan -- Dynamic male, strong rhythmic drive (English)",
708
  label="Speaker")
709
+ cv_instruct = gr.Textbox(label="Emotion / Style (optional, used when auto-emotions is off)",
710
  placeholder="e.g. Very happy, Whisper softly, Speak with authority...")
711
+ cv_emotions = gr.Checkbox(value=True, label="Auto-detect emotions",
712
+ info="AI analyzes text and varies tone/emotion per segment. Requires DASHSCOPE_API_KEY.")
713
  cv_btn = gr.Button("Generate", variant="primary")
714
  with gr.Column():
715
  cv_audio = gr.Audio(label="Generated Speech", type="filepath")
716
  cv_btn.click(fn=generate_custom_voice,
717
+ inputs=[cv_text, cv_file, cv_lang, cv_speaker, cv_instruct, cv_emotions], outputs=cv_audio)
718
 
719
  # ── Tab 2: Voice Design ──
720
  with gr.Tab("Voice Design"):
 
737
  ],
738
  inputs=[vd_desc], label="Examples",
739
  )
740
+ vd_emotions = gr.Checkbox(value=True, label="Auto-detect emotions",
741
+ info="AI varies tone/emotion per segment. Requires DASHSCOPE_API_KEY.")
742
  vd_btn = gr.Button("Generate", variant="primary")
743
  with gr.Column():
744
  vd_audio = gr.Audio(label="Generated Speech", type="filepath")
745
  vd_btn.click(fn=generate_voice_design,
746
+ inputs=[vd_text, vd_file, vd_lang, vd_desc, vd_emotions], outputs=vd_audio)
747
 
748
  # ── Tab 3: Voice Clone ──
749
  with gr.Tab("Voice Clone"):