Upload app.py
Browse files
app.py
CHANGED
|
@@ -665,7 +665,9 @@ def generate_single_speaker(text_input, file_input, source_lang, target_lang, sp
|
|
| 665 |
# MULTI-SPEAKER: Generate with character voices
|
| 666 |
# ==========================================
|
| 667 |
@spaces.GPU(duration=600)
|
| 668 |
-
def generate_multi_speaker(text_input, file_input, source_lang, target_lang,
|
|
|
|
|
|
|
| 669 |
resolved = resolve_text(text_input, file_input)
|
| 670 |
if len(resolved) < 30:
|
| 671 |
raise gr.Error("Text too short for multi-speaker.")
|
|
@@ -702,35 +704,46 @@ def generate_multi_speaker(text_input, file_input, source_lang, target_lang, pro
|
|
| 702 |
char_names = [c["name"] for c in characters]
|
| 703 |
print(f"[Multi] {len(characters)} characters: {char_names}, {len(segments)} segments")
|
| 704 |
|
| 705 |
-
# Assign voices
|
|
|
|
| 706 |
voice_map = {}
|
| 707 |
mi, fi = 0, 0
|
| 708 |
-
for c in characters:
|
| 709 |
name, gender = c["name"], c.get("gender", "neutral")
|
| 710 |
-
|
| 711 |
-
|
| 712 |
-
|
| 713 |
-
|
| 714 |
-
|
| 715 |
-
|
| 716 |
-
|
| 717 |
-
|
| 718 |
-
elif gender == "female" and el_f:
|
| 719 |
-
voice_map[name] = {"id": el_f[fi % len(el_f)]["id"], "name": el_f[fi % len(el_f)]["name"]}
|
| 720 |
-
fi += 1
|
| 721 |
else:
|
| 722 |
-
voice_map[name] = {"
|
| 723 |
else:
|
| 724 |
-
|
| 725 |
-
|
| 726 |
-
|
| 727 |
-
|
| 728 |
-
|
| 729 |
-
|
| 730 |
-
|
| 731 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 732 |
else:
|
| 733 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 734 |
print(f"[Multi] Voice map: {voice_map}")
|
| 735 |
|
| 736 |
# Generate segments
|
|
@@ -805,13 +818,28 @@ def generate_multi_speaker(text_input, file_input, source_lang, target_lang, pro
|
|
| 805 |
# Assemble
|
| 806 |
progress(0.92, desc="Assembling audiobook...")
|
| 807 |
final_wav = os.path.join(tmp_dir, "output.wav")
|
|
|
|
| 808 |
concatenate_wavs(audio_files, final_wav)
|
| 809 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 810 |
progress(0.96, desc="Converting to MP3...")
|
| 811 |
final_mp3 = os.path.join(OUTPUT_DIR, f"multi_{int(time.time())}.mp3")
|
| 812 |
-
subprocess.run(["ffmpeg", "-y", "-i", final_wav, "-codec:a", "libmp3lame",
|
| 813 |
"-b:a", "128k", "-ar", "24000", "-ac", "1", final_mp3],
|
| 814 |
-
capture_output=True,
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 815 |
|
| 816 |
progress(1.0, desc="Done!")
|
| 817 |
size = os.path.getsize(final_mp3) / (1024 * 1024)
|
|
@@ -1003,6 +1031,17 @@ with gr.Blocks(title="Audiobook Generator") as demo:
|
|
| 1003 |
ms_target_lang = gr.Dropdown(choices=LANGUAGES, value="English",
|
| 1004 |
label="Output Language",
|
| 1005 |
info="Language for the generated speech")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1006 |
ms_sample_btn = gr.Button("Load Sample Story", variant="secondary", size="sm")
|
| 1007 |
ms_btn = gr.Button("Generate Multi-Speaker Audiobook", variant="primary", size="lg")
|
| 1008 |
|
|
@@ -1012,15 +1051,70 @@ with gr.Blocks(title="Audiobook Generator") as demo:
|
|
| 1012 |
with gr.Accordion("Transcript (with cast & emotions)", open=False):
|
| 1013 |
ms_transcript = gr.Markdown()
|
| 1014 |
|
| 1015 |
-
#
|
| 1016 |
_ms_sections = gr.State(value=[])
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1017 |
|
| 1018 |
ms_detect_btn.click(fn=detect_sections_ui, inputs=[ms_file],
|
| 1019 |
outputs=[ms_sections_info, ms_section_choice, ms_load_btn, _ms_sections])
|
| 1020 |
ms_load_btn.click(fn=load_section_ui, inputs=[ms_section_choice, _ms_sections], outputs=[ms_text])
|
| 1021 |
ms_sample_btn.click(fn=lambda: SAMPLE, outputs=ms_text)
|
| 1022 |
ms_btn.click(fn=generate_multi_speaker,
|
| 1023 |
-
inputs=[ms_text, ms_file, ms_source_lang, ms_target_lang],
|
| 1024 |
outputs=[ms_audio, ms_stats, ms_transcript])
|
| 1025 |
|
| 1026 |
gr.Markdown(
|
|
@@ -1034,4 +1128,4 @@ with gr.Blocks(title="Audiobook Generator") as demo:
|
|
| 1034 |
)
|
| 1035 |
|
| 1036 |
if __name__ == "__main__":
|
| 1037 |
-
demo.launch(allowed_paths=[OUTPUT_DIR])
|
|
|
|
| 665 |
# MULTI-SPEAKER: Generate with character voices
|
| 666 |
# ==========================================
|
| 667 |
@spaces.GPU(duration=600)
|
| 668 |
+
def generate_multi_speaker(text_input, file_input, source_lang, target_lang,
|
| 669 |
+
v0, v1, v2, v3, v4, v5, v6, v7,
|
| 670 |
+
progress=gr.Progress()):
|
| 671 |
resolved = resolve_text(text_input, file_input)
|
| 672 |
if len(resolved) < 30:
|
| 673 |
raise gr.Error("Text too short for multi-speaker.")
|
|
|
|
| 704 |
char_names = [c["name"] for c in characters]
|
| 705 |
print(f"[Multi] {len(characters)} characters: {char_names}, {len(segments)} segments")
|
| 706 |
|
| 707 |
+
# Assign voices β use custom assignments from dropdowns if provided
|
| 708 |
+
voice_assignments = [v0, v1, v2, v3, v4, v5, v6, v7]
|
| 709 |
voice_map = {}
|
| 710 |
mi, fi = 0, 0
|
| 711 |
+
for ci, c in enumerate(characters):
|
| 712 |
name, gender = c["name"], c.get("gender", "neutral")
|
| 713 |
+
|
| 714 |
+
# Use custom assignment if provided
|
| 715 |
+
if ci < len(voice_assignments) and voice_assignments[ci]:
|
| 716 |
+
custom_label = voice_assignments[ci]
|
| 717 |
+
if is_elevenlabs:
|
| 718 |
+
voice_name = custom_label.split("--")[0].strip()
|
| 719 |
+
voice_id = get_el_voice_id(target_lang, custom_label)
|
| 720 |
+
voice_map[name] = {"id": voice_id, "name": voice_name}
|
|
|
|
|
|
|
|
|
|
| 721 |
else:
|
| 722 |
+
voice_map[name] = {"speaker": custom_label.split("--")[0].strip()}
|
| 723 |
else:
|
| 724 |
+
# Auto-assign by gender
|
| 725 |
+
if is_elevenlabs:
|
| 726 |
+
el_m = EL_MALE.get(target_lang, EL_MALE.get("Arabic", []))
|
| 727 |
+
el_f = EL_FEMALE.get(target_lang, EL_FEMALE.get("Arabic", []))
|
| 728 |
+
if gender == "male" and el_m:
|
| 729 |
+
voice_map[name] = {"id": el_m[mi % len(el_m)]["id"], "name": el_m[mi % len(el_m)]["name"]}
|
| 730 |
+
mi += 1
|
| 731 |
+
elif gender == "female" and el_f:
|
| 732 |
+
voice_map[name] = {"id": el_f[fi % len(el_f)]["id"], "name": el_f[fi % len(el_f)]["name"]}
|
| 733 |
+
fi += 1
|
| 734 |
+
else:
|
| 735 |
+
voice_map[name] = {"id": "21m00Tcm4TlvDq8ikWAM", "name": "Rachel"}
|
| 736 |
else:
|
| 737 |
+
if name == "Narrator":
|
| 738 |
+
voice_map[name] = {"speaker": "Ryan"}
|
| 739 |
+
elif gender == "male":
|
| 740 |
+
voice_map[name] = {"speaker": MALE_SPEAKERS[mi % len(MALE_SPEAKERS)]}
|
| 741 |
+
mi += 1
|
| 742 |
+
elif gender == "female":
|
| 743 |
+
voice_map[name] = {"speaker": FEMALE_SPEAKERS[fi % len(FEMALE_SPEAKERS)]}
|
| 744 |
+
fi += 1
|
| 745 |
+
else:
|
| 746 |
+
voice_map[name] = {"speaker": "Ryan"}
|
| 747 |
print(f"[Multi] Voice map: {voice_map}")
|
| 748 |
|
| 749 |
# Generate segments
|
|
|
|
| 818 |
# Assemble
|
| 819 |
progress(0.92, desc="Assembling audiobook...")
|
| 820 |
final_wav = os.path.join(tmp_dir, "output.wav")
|
| 821 |
+
print(f"[Multi] Concatenating {len(audio_files)} files...")
|
| 822 |
concatenate_wavs(audio_files, final_wav)
|
| 823 |
|
| 824 |
+
if not os.path.exists(final_wav):
|
| 825 |
+
raise gr.Error("Assembly failed β no WAV created.")
|
| 826 |
+
|
| 827 |
+
wav_size = os.path.getsize(final_wav) / (1024 * 1024)
|
| 828 |
+
print(f"[Multi] WAV assembled: {wav_size:.1f} MB")
|
| 829 |
+
|
| 830 |
progress(0.96, desc="Converting to MP3...")
|
| 831 |
final_mp3 = os.path.join(OUTPUT_DIR, f"multi_{int(time.time())}.mp3")
|
| 832 |
+
result = subprocess.run(["ffmpeg", "-y", "-i", final_wav, "-codec:a", "libmp3lame",
|
| 833 |
"-b:a", "128k", "-ar", "24000", "-ac", "1", final_mp3],
|
| 834 |
+
capture_output=True, text=True)
|
| 835 |
+
if result.returncode != 0:
|
| 836 |
+
print(f"[Multi] FFmpeg error: {result.stderr[-500:]}")
|
| 837 |
+
raise gr.Error(f"MP3 conversion failed.")
|
| 838 |
+
|
| 839 |
+
if not os.path.exists(final_mp3) or os.path.getsize(final_mp3) < 100:
|
| 840 |
+
raise gr.Error("MP3 conversion produced empty file.")
|
| 841 |
+
|
| 842 |
+
print(f"[Multi] MP3 ready: {final_mp3}, {os.path.getsize(final_mp3) / (1024*1024):.1f} MB")
|
| 843 |
|
| 844 |
progress(1.0, desc="Done!")
|
| 845 |
size = os.path.getsize(final_mp3) / (1024 * 1024)
|
|
|
|
| 1031 |
ms_target_lang = gr.Dropdown(choices=LANGUAGES, value="English",
|
| 1032 |
label="Output Language",
|
| 1033 |
info="Language for the generated speech")
|
| 1034 |
+
|
| 1035 |
+
# Character detection + voice assignment
|
| 1036 |
+
with gr.Accordion("Cast β Detect & Assign Voices", open=True):
|
| 1037 |
+
ms_char_btn = gr.Button("Detect Characters", variant="secondary")
|
| 1038 |
+
ms_char_info = gr.Markdown("Enter text then click 'Detect Characters' to see who's in the story.")
|
| 1039 |
+
ms_voices = []
|
| 1040 |
+
for idx in range(8):
|
| 1041 |
+
dd = gr.Dropdown(choices=SPEAKER_CHOICES, label=f"Character {idx+1}",
|
| 1042 |
+
visible=False, allow_custom_value=True)
|
| 1043 |
+
ms_voices.append(dd)
|
| 1044 |
+
|
| 1045 |
ms_sample_btn = gr.Button("Load Sample Story", variant="secondary", size="sm")
|
| 1046 |
ms_btn = gr.Button("Generate Multi-Speaker Audiobook", variant="primary", size="lg")
|
| 1047 |
|
|
|
|
| 1051 |
with gr.Accordion("Transcript (with cast & emotions)", open=False):
|
| 1052 |
ms_transcript = gr.Markdown()
|
| 1053 |
|
| 1054 |
+
# States
|
| 1055 |
_ms_sections = gr.State(value=[])
|
| 1056 |
+
_ms_characters = gr.State(value=[])
|
| 1057 |
+
|
| 1058 |
+
def detect_chars_ui(text_input, file_input, target_lang):
|
| 1059 |
+
text = resolve_text(text_input, file_input)
|
| 1060 |
+
client = get_llm_client()
|
| 1061 |
+
if not client:
|
| 1062 |
+
raise gr.Error("DASHSCOPE_API_KEY needed.")
|
| 1063 |
+
characters, _ = detect_characters_and_emotions(client, text)
|
| 1064 |
+
engine = get_engine(target_lang)
|
| 1065 |
+
|
| 1066 |
+
# Build separate male/female voice lists
|
| 1067 |
+
if engine == "elevenlabs":
|
| 1068 |
+
all_voices = EL_SPEAKER_CHOICES.get(target_lang, EL_SPEAKER_CHOICES.get("Arabic", []))
|
| 1069 |
+
male_voices = [f"{v['name']} -- {v['desc']}" for v in ELEVENLABS_VOICES.get(target_lang, ELEVENLABS_VOICES.get("Arabic", [])) if v["gender"] == "male"]
|
| 1070 |
+
female_voices = [f"{v['name']} -- {v['desc']}" for v in ELEVENLABS_VOICES.get(target_lang, ELEVENLABS_VOICES.get("Arabic", [])) if v["gender"] == "female"]
|
| 1071 |
+
else:
|
| 1072 |
+
all_voices = SPEAKER_CHOICES
|
| 1073 |
+
male_voices = [f"{n} -- {s['desc']}" for n, s in SPEAKERS.items() if s["gender"] == "male"]
|
| 1074 |
+
female_voices = [f"{n} -- {s['desc']}" for n, s in SPEAKERS.items() if s["gender"] == "female"]
|
| 1075 |
+
|
| 1076 |
+
info = f"**Detected {len(characters)} characters:**\n\n"
|
| 1077 |
+
updates = []
|
| 1078 |
+
used_male, used_female = 0, 0
|
| 1079 |
+
|
| 1080 |
+
for i in range(8):
|
| 1081 |
+
if i < len(characters):
|
| 1082 |
+
c = characters[i]
|
| 1083 |
+
name, gender = c["name"], c.get("gender", "neutral")
|
| 1084 |
+
info += f"{i+1}. **{name}** ({gender})\n"
|
| 1085 |
+
|
| 1086 |
+
# Assign unique voice per character β no duplicates
|
| 1087 |
+
if gender == "female" and female_voices:
|
| 1088 |
+
default = female_voices[used_female % len(female_voices)]
|
| 1089 |
+
used_female += 1
|
| 1090 |
+
elif gender == "male" and male_voices:
|
| 1091 |
+
default = male_voices[used_male % len(male_voices)]
|
| 1092 |
+
used_male += 1
|
| 1093 |
+
else:
|
| 1094 |
+
# Neutral/narrator β use a male voice by default
|
| 1095 |
+
default = male_voices[used_male % len(male_voices)] if male_voices else all_voices[0]
|
| 1096 |
+
used_male += 1
|
| 1097 |
+
|
| 1098 |
+
updates.append(gr.update(visible=True, label=f"{name} ({gender})",
|
| 1099 |
+
choices=all_voices, value=default))
|
| 1100 |
+
else:
|
| 1101 |
+
updates.append(gr.update(visible=False))
|
| 1102 |
+
|
| 1103 |
+
info += "\n*Change any voice below, then click Generate.*"
|
| 1104 |
+
return [info, characters] + updates
|
| 1105 |
+
|
| 1106 |
+
ms_char_btn.click(
|
| 1107 |
+
fn=detect_chars_ui,
|
| 1108 |
+
inputs=[ms_text, ms_file, ms_target_lang],
|
| 1109 |
+
outputs=[ms_char_info, _ms_characters] + ms_voices,
|
| 1110 |
+
)
|
| 1111 |
|
| 1112 |
ms_detect_btn.click(fn=detect_sections_ui, inputs=[ms_file],
|
| 1113 |
outputs=[ms_sections_info, ms_section_choice, ms_load_btn, _ms_sections])
|
| 1114 |
ms_load_btn.click(fn=load_section_ui, inputs=[ms_section_choice, _ms_sections], outputs=[ms_text])
|
| 1115 |
ms_sample_btn.click(fn=lambda: SAMPLE, outputs=ms_text)
|
| 1116 |
ms_btn.click(fn=generate_multi_speaker,
|
| 1117 |
+
inputs=[ms_text, ms_file, ms_source_lang, ms_target_lang] + ms_voices,
|
| 1118 |
outputs=[ms_audio, ms_stats, ms_transcript])
|
| 1119 |
|
| 1120 |
gr.Markdown(
|
|
|
|
| 1128 |
)
|
| 1129 |
|
| 1130 |
if __name__ == "__main__":
|
| 1131 |
+
demo.launch(allowed_paths=[OUTPUT_DIR, tempfile.gettempdir()], ssr_mode=False)
|