PlotweaverModel commited on
Commit
0a3811e
Β·
verified Β·
1 Parent(s): ba5a437

Upload app.py

Browse files
Files changed (1) hide show
  1. app.py +184 -6
app.py CHANGED
@@ -145,20 +145,128 @@ def generate_voice_design(text, language, voice_description):
145
  return path
146
 
147
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
148
  @spaces.GPU
149
- def generate_voice_clone(text, language, ref_audio, ref_text):
150
  if not text.strip():
151
  raise gr.Error("Please enter some text.")
152
  if ref_audio is None:
153
  raise gr.Error("Please upload a reference audio sample.")
 
 
 
 
 
 
 
 
 
 
154
  model = get_model("clone")
155
  lang = language if language != "Auto" else "Auto"
156
- kwargs = {"text": text, "language": lang, "ref_audio": ref_audio}
157
  if ref_text and ref_text.strip():
158
  kwargs["ref_text"] = ref_text.strip()
159
  else:
160
  kwargs["x_vector_only_mode"] = True
161
- print(f"[TTS] Clone: lang={lang}")
162
  wavs, sr = model.generate_voice_clone(**kwargs)
163
  path = os.path.join(OUTPUT_DIR, f"clone_{int(time.time())}.wav")
164
  sf.write(path, wavs[0], sr)
@@ -445,7 +553,16 @@ The old man was silent for a long time. "I already did," he finally whispered. "
445
 
446
  DESCRIPTION = """
447
  # Qwen3-TTS Demo
 
 
 
 
 
 
 
 
448
 
 
449
  """
450
 
451
  with gr.Blocks(title="Qwen3-TTS Demo") as demo:
@@ -497,7 +614,10 @@ with gr.Blocks(title="Qwen3-TTS Demo") as demo:
497
 
498
  # ── Tab 3: Voice Clone ──
499
  with gr.Tab("Voice Clone"):
500
- gr.Markdown("Clone any voice from a short audio sample (3+ seconds).")
 
 
 
501
  with gr.Row():
502
  with gr.Column():
503
  vc_text = gr.Textbox(label="Text (in cloned voice)", lines=4, placeholder="What should the cloned voice say...")
@@ -505,11 +625,13 @@ with gr.Blocks(title="Qwen3-TTS Demo") as demo:
505
  vc_ref = gr.Audio(label="Reference Audio (3+ seconds)", type="filepath")
506
  vc_ref_text = gr.Textbox(label="Transcript (optional, improves quality)",
507
  placeholder="What the person says in the reference...")
 
 
508
  vc_btn = gr.Button("Clone & Generate", variant="primary")
509
  with gr.Column():
510
  vc_audio = gr.Audio(label="Cloned Voice Speech", type="filepath")
511
  vc_btn.click(fn=generate_voice_clone,
512
- inputs=[vc_text, vc_lang, vc_ref, vc_ref_text], outputs=vc_audio)
513
 
514
  # ── Tab 4: Multi-Speaker Story ──
515
  with gr.Tab("Multi-Speaker Story"):
@@ -517,8 +639,9 @@ with gr.Blocks(title="Qwen3-TTS Demo") as demo:
517
  "Paste a story with dialogue and the AI automatically:\n"
518
  "1. Detects all characters and their genders\n"
519
  "2. Assigns unique voices (male/female matched)\n"
520
- "3. Adds emotion instructions per line (sad, whispered, excited...)\n"
521
  "4. Generates the full audiobook with different voices\n\n"
 
522
  "*Requires DASHSCOPE_API_KEY for character/emotion analysis. TTS is local.*"
523
  )
524
  with gr.Row():
@@ -526,6 +649,18 @@ with gr.Blocks(title="Qwen3-TTS Demo") as demo:
526
  ms_text = gr.Textbox(label="Story Text", lines=10, placeholder="Paste your story with dialogue...")
527
  ms_file = gr.File(label="Or Upload (.txt, .pdf, .docx)",
528
  file_types=[".txt", ".md", ".pdf", ".docx"], type="filepath")
 
 
 
 
 
 
 
 
 
 
 
 
529
  ms_lang = gr.Dropdown(choices=LANGUAGES, value="English", label="Language")
530
  ms_sample = gr.Button("Load Sample Story", variant="secondary", size="sm")
531
  ms_btn = gr.Button("Generate Multi-Speaker Audiobook", variant="primary", size="lg")
@@ -535,14 +670,57 @@ with gr.Blocks(title="Qwen3-TTS Demo") as demo:
535
  with gr.Accordion("Full Transcript (with emotions)", open=False):
536
  ms_transcript = gr.Markdown()
537
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
538
  ms_sample.click(fn=lambda: SAMPLE_STORY, outputs=ms_text)
539
  ms_btn.click(fn=generate_multi_speaker_story,
540
  inputs=[ms_text, ms_file, ms_lang],
541
  outputs=[ms_audio, ms_stats, ms_transcript])
542
 
 
543
  gr.Markdown(
544
  "---\n"
 
545
  "**Languages:** EN, ZH, JA, KO, DE, FR, RU, PT, ES, IT | "
 
546
  "**Character Analysis:** Cloud AI (optional, for Multi-Speaker mode)"
547
  )
548
 
 
145
  return path
146
 
147
 
148
+ def enhance_text_with_emotions(client, text):
149
+ """Use AI to rewrite text with emotional delivery cues that TTS picks up naturally."""
150
+ if not client:
151
+ return text
152
+ try:
153
+ response = client.chat.completions.create(
154
+ model=OMNI_MODEL, modalities=["text"],
155
+ messages=[
156
+ {
157
+ "role": "system",
158
+ "content": (
159
+ "You are an audiobook performance coach. Rewrite the text to add natural "
160
+ "emotional delivery cues that a TTS voice will pick up from context.\n\n"
161
+ "Techniques:\n"
162
+ "- Add pause markers: '...' for dramatic pauses\n"
163
+ "- Use punctuation for emphasis: '!' for energy, '?' for curiosity\n"
164
+ "- Add breath/pacing cues with commas and dashes\n"
165
+ "- Extend words for emphasis: 'sooo beautiful'\n"
166
+ "- Add interjections: 'Oh!', 'Hmm...', 'Ah,'\n"
167
+ "- Use ellipsis for trailing off: 'I never thought...'\n\n"
168
+ "Rules:\n"
169
+ "1. Keep ALL the original meaning and content\n"
170
+ "2. Do NOT add stage directions or [brackets]\n"
171
+ "3. The output must be speakable text only\n"
172
+ "4. Be subtle β€” enhance, don't overdo\n"
173
+ "5. Output ONLY the enhanced text"
174
+ ),
175
+ },
176
+ {"role": "user", "content": f"Enhance this for emotional TTS delivery:\n\n{text}"},
177
+ ],
178
+ )
179
+ enhanced = response.choices[0].message.content.strip()
180
+ print(f"[Emotions] Original: {len(text)} chars -> Enhanced: {len(enhanced)} chars")
181
+ return enhanced
182
+ except Exception as e:
183
+ print(f"[Emotions] Enhancement failed: {e}")
184
+ return text
185
+
186
+
187
+ def extract_pdf_sections(filepath):
188
+ """Extract sections/chapters from a PDF and return them as a list."""
189
+ try:
190
+ import pypdf
191
+ except ImportError:
192
+ raise gr.Error("pypdf not installed.")
193
+
194
+ reader = pypdf.PdfReader(filepath)
195
+ full_text = ""
196
+ for page in reader.pages:
197
+ t = page.extract_text()
198
+ if t:
199
+ full_text += t + "\n\n"
200
+
201
+ # Try to detect chapters/sections
202
+ sections = []
203
+ # Common patterns: "Chapter X", "CHAPTER X", "Part X", "Section X", numbered headings
204
+ pattern = r'(?m)^(?:(?:Chapter|CHAPTER|Part|PART|Section|SECTION)\s+\w+[.:)?\s].*|(?:\d+[.)]\s+[A-Z].*)|(?:[IVXLCDM]+[.)]\s+.*))'
205
+ matches = list(re.finditer(pattern, full_text))
206
+
207
+ if matches:
208
+ for i, match in enumerate(matches):
209
+ start = match.start()
210
+ end = matches[i + 1].start() if i + 1 < len(matches) else len(full_text)
211
+ title = match.group().strip()[:80]
212
+ content = full_text[start:end].strip()
213
+ if len(content) > 20:
214
+ sections.append({"title": title, "content": content, "chars": len(content)})
215
+
216
+ # If no sections found, split by double newlines into chunks
217
+ if not sections:
218
+ paragraphs = re.split(r'\n\s*\n', full_text)
219
+ # Group into ~3000 char sections
220
+ current_section = ""
221
+ section_num = 1
222
+ for para in paragraphs:
223
+ para = para.strip()
224
+ if not para:
225
+ continue
226
+ if len(current_section) + len(para) > 3000 and current_section:
227
+ sections.append({
228
+ "title": f"Section {section_num}",
229
+ "content": current_section.strip(),
230
+ "chars": len(current_section),
231
+ })
232
+ section_num += 1
233
+ current_section = para
234
+ else:
235
+ current_section += "\n\n" + para
236
+ if current_section.strip():
237
+ sections.append({
238
+ "title": f"Section {section_num}",
239
+ "content": current_section.strip(),
240
+ "chars": len(current_section),
241
+ })
242
+
243
+ return sections, full_text
244
+
245
+
246
  @spaces.GPU
247
+ def generate_voice_clone(text, language, ref_audio, ref_text, add_emotions):
248
  if not text.strip():
249
  raise gr.Error("Please enter some text.")
250
  if ref_audio is None:
251
  raise gr.Error("Please upload a reference audio sample.")
252
+
253
+ # Enhance text with emotions if requested
254
+ final_text = text
255
+ if add_emotions:
256
+ client = get_llm_client()
257
+ if client:
258
+ final_text = enhance_text_with_emotions(client, text)
259
+ else:
260
+ print("[Clone] No LLM client for emotion enhancement, using raw text")
261
+
262
  model = get_model("clone")
263
  lang = language if language != "Auto" else "Auto"
264
+ kwargs = {"text": final_text, "language": lang, "ref_audio": ref_audio}
265
  if ref_text and ref_text.strip():
266
  kwargs["ref_text"] = ref_text.strip()
267
  else:
268
  kwargs["x_vector_only_mode"] = True
269
+ print(f"[TTS] Clone: lang={lang}, emotions={'yes' if add_emotions else 'no'}")
270
  wavs, sr = model.generate_voice_clone(**kwargs)
271
  path = os.path.join(OUTPUT_DIR, f"clone_{int(time.time())}.wav")
272
  sf.write(path, wavs[0], sr)
 
553
 
554
  DESCRIPTION = """
555
  # Qwen3-TTS Demo
556
+ ### Open-Source Text-to-Speech (1.7B) β€” Self-Hosted, No API Keys for TTS
557
+
558
+ | Mode | What it does |
559
+ |------|-------------|
560
+ | **Custom Voice** | Pick a preset voice + optional emotion instruction |
561
+ | **Voice Design** | Describe any voice in natural language |
562
+ | **Voice Clone** | Clone a voice from 3 seconds of audio |
563
+ | **Multi-Speaker Story** | Auto-detect characters, assign voices, add emotions β€” full audiobook |
564
 
565
+ 10 languages supported. Running on ZeroGPU (free).
566
  """
567
 
568
  with gr.Blocks(title="Qwen3-TTS Demo") as demo:
 
614
 
615
  # ── Tab 3: Voice Clone ──
616
  with gr.Tab("Voice Clone"):
617
+ gr.Markdown(
618
+ "Clone any voice from a short audio sample (3+ seconds).\n"
619
+ "Enable 'Enhance with emotions' to have AI add natural emotional cues to the text."
620
+ )
621
  with gr.Row():
622
  with gr.Column():
623
  vc_text = gr.Textbox(label="Text (in cloned voice)", lines=4, placeholder="What should the cloned voice say...")
 
625
  vc_ref = gr.Audio(label="Reference Audio (3+ seconds)", type="filepath")
626
  vc_ref_text = gr.Textbox(label="Transcript (optional, improves quality)",
627
  placeholder="What the person says in the reference...")
628
+ vc_emotions = gr.Checkbox(value=False, label="Enhance with emotions",
629
+ info="AI adds natural emotional cues (pauses, emphasis, interjections) to make the delivery more expressive. Requires DASHSCOPE_API_KEY.")
630
  vc_btn = gr.Button("Clone & Generate", variant="primary")
631
  with gr.Column():
632
  vc_audio = gr.Audio(label="Cloned Voice Speech", type="filepath")
633
  vc_btn.click(fn=generate_voice_clone,
634
+ inputs=[vc_text, vc_lang, vc_ref, vc_ref_text, vc_emotions], outputs=vc_audio)
635
 
636
  # ── Tab 4: Multi-Speaker Story ──
637
  with gr.Tab("Multi-Speaker Story"):
 
639
  "Paste a story with dialogue and the AI automatically:\n"
640
  "1. Detects all characters and their genders\n"
641
  "2. Assigns unique voices (male/female matched)\n"
642
+ "3. Adds emotion instructions per line\n"
643
  "4. Generates the full audiobook with different voices\n\n"
644
+ "**PDF users:** Upload a PDF, click 'Detect Sections' to pick a specific chapter.\n\n"
645
  "*Requires DASHSCOPE_API_KEY for character/emotion analysis. TTS is local.*"
646
  )
647
  with gr.Row():
 
649
  ms_text = gr.Textbox(label="Story Text", lines=10, placeholder="Paste your story with dialogue...")
650
  ms_file = gr.File(label="Or Upload (.txt, .pdf, .docx)",
651
  file_types=[".txt", ".md", ".pdf", ".docx"], type="filepath")
652
+
653
+ # PDF section selection
654
+ with gr.Accordion("PDF Section Selection (optional)", open=False):
655
+ ms_detect_btn = gr.Button("Detect Sections from PDF", variant="secondary", size="sm")
656
+ ms_sections_info = gr.Markdown(value="Upload a PDF then click 'Detect Sections' to see chapters.")
657
+ ms_section_choice = gr.Dropdown(
658
+ choices=[], label="Select Section to Generate",
659
+ visible=False, allow_custom_value=False,
660
+ info="Pick a chapter/section, or close this accordion to process the full text",
661
+ )
662
+ ms_load_section = gr.Button("Load Selected Section", variant="secondary", size="sm", visible=False)
663
+
664
  ms_lang = gr.Dropdown(choices=LANGUAGES, value="English", label="Language")
665
  ms_sample = gr.Button("Load Sample Story", variant="secondary", size="sm")
666
  ms_btn = gr.Button("Generate Multi-Speaker Audiobook", variant="primary", size="lg")
 
670
  with gr.Accordion("Full Transcript (with emotions)", open=False):
671
  ms_transcript = gr.Markdown()
672
 
673
+ # Section detection state
674
+ _section_cache = gr.State(value=[])
675
+
676
+ def detect_sections_from_pdf(file_input):
677
+ if file_input is None:
678
+ raise gr.Error("Upload a PDF first.")
679
+ ext = os.path.splitext(file_input)[1].lower()
680
+ if ext != ".pdf":
681
+ raise gr.Error("Section detection works with PDF files. For other formats, paste text directly.")
682
+ sections, _ = extract_pdf_sections(file_input)
683
+ if not sections:
684
+ return "No sections detected.", gr.update(visible=False), gr.update(visible=False), []
685
+ choices = [f"{s['title']} ({s['chars']:,} chars)" for s in sections]
686
+ info = f"**Found {len(sections)} sections:**\n\n"
687
+ for i, s in enumerate(sections):
688
+ preview = s["content"][:100].replace("\n", " ")
689
+ info += f"{i+1}. **{s['title']}** ({s['chars']:,} chars) β€” {preview}...\n"
690
+ return (
691
+ info,
692
+ gr.update(visible=True, choices=choices, value=choices[0]),
693
+ gr.update(visible=True),
694
+ sections,
695
+ )
696
+
697
+ def load_selected_section(choice, sections):
698
+ if not sections or not choice:
699
+ return ""
700
+ idx = next((i for i, s in enumerate(sections)
701
+ if f"{s['title']} ({s['chars']:,} chars)" == choice), 0)
702
+ return sections[idx]["content"]
703
+
704
+ ms_detect_btn.click(
705
+ fn=detect_sections_from_pdf, inputs=[ms_file],
706
+ outputs=[ms_sections_info, ms_section_choice, ms_load_section, _section_cache],
707
+ )
708
+ ms_load_section.click(
709
+ fn=load_selected_section, inputs=[ms_section_choice, _section_cache],
710
+ outputs=[ms_text],
711
+ )
712
+
713
  ms_sample.click(fn=lambda: SAMPLE_STORY, outputs=ms_text)
714
  ms_btn.click(fn=generate_multi_speaker_story,
715
  inputs=[ms_text, ms_file, ms_lang],
716
  outputs=[ms_audio, ms_stats, ms_transcript])
717
 
718
+
719
  gr.Markdown(
720
  "---\n"
721
+ "**Models:** Qwen3-TTS-12Hz-1.7B (Apache 2.0) | "
722
  "**Languages:** EN, ZH, JA, KO, DE, FR, RU, PT, ES, IT | "
723
+ "**TTS:** Self-hosted on ZeroGPU | "
724
  "**Character Analysis:** Cloud AI (optional, for Multi-Speaker mode)"
725
  )
726