PlotweaverModel commited on
Commit
833aa13
Β·
verified Β·
1 Parent(s): 6a9664a

Upload app.py

Browse files
Files changed (1) hide show
  1. app.py +123 -29
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, progress=gr.Progress()):
 
 
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 by gender
 
706
  voice_map = {}
707
  mi, fi = 0, 0
708
- for c in characters:
709
  name, gender = c["name"], c.get("gender", "neutral")
710
- if is_elevenlabs:
711
- el_m = EL_MALE.get(target_lang, EL_MALE.get("Arabic", []))
712
- el_f = EL_FEMALE.get(target_lang, EL_FEMALE.get("Arabic", []))
713
- if name == "Narrator":
714
- voice_map[name] = {"id": el_m[0]["id"] if el_m else "21m00Tcm4TlvDq8ikWAM", "name": el_m[0]["name"] if el_m else "Drew"}
715
- elif gender == "male" and el_m:
716
- voice_map[name] = {"id": el_m[mi % len(el_m)]["id"], "name": el_m[mi % len(el_m)]["name"]}
717
- mi += 1
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] = {"id": "21m00Tcm4TlvDq8ikWAM", "name": "Rachel"}
723
  else:
724
- if name == "Narrator":
725
- voice_map[name] = {"speaker": "Ryan"}
726
- elif gender == "male":
727
- voice_map[name] = {"speaker": MALE_SPEAKERS[mi % len(MALE_SPEAKERS)]}
728
- mi += 1
729
- elif gender == "female":
730
- voice_map[name] = {"speaker": FEMALE_SPEAKERS[fi % len(FEMALE_SPEAKERS)]}
731
- fi += 1
 
 
 
 
732
  else:
733
- voice_map[name] = {"speaker": "Ryan"}
 
 
 
 
 
 
 
 
 
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, check=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
- # Section detection state
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)