Spaces:
Paused
Paused
Commit Β·
57efb6d
1
Parent(s): a51c3c3
fix: voice cards clickable - select cloned voice as narrator
Browse files- Add onclick JS to voice cards to set voice_selector
- Add hidden voice_selector textbox + change handler
- Store profile_id in voice list when saving cloned voice
- Map card click β voice_profile_state update
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
app.py
CHANGED
|
@@ -347,9 +347,21 @@ def render_cloned_voices_html(voices_list):
|
|
| 347 |
status_bg = "#f0fdf4" if v["status"] == "ready" else "#fef3c7"
|
| 348 |
safe_name = html.escape(v['name'])
|
| 349 |
safe_desc = html.escape(v['description'])
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 350 |
|
| 351 |
markup += f"""
|
| 352 |
-
<div class="shelf-card" style="align-items: center;">
|
| 353 |
<div style="width: 48px; height: 48px; border-radius: 50%; background: #ede8e3; border: 1px solid #ddc1b0; font-size: 24px; display: flex; align-items: center; justify-content: center; shrink-0: 0;">
|
| 354 |
{v['avatar_url']}
|
| 355 |
</div>
|
|
@@ -586,9 +598,11 @@ with gr.Blocks(title="MomsVoice", css=css_code) as demo:
|
|
| 586 |
gr.HTML("""
|
| 587 |
<div style="margin-top: 32px; border-top: 1px solid #ebdccb; padding-top: 24px;">
|
| 588 |
<h4 class="serif-header" style="font-size: 18px; margin-bottom: 16px;">Currently Available Voices</h4>
|
|
|
|
| 589 |
</div>
|
| 590 |
""")
|
| 591 |
cloned_voices_list_grid = gr.HTML(value=render_cloned_voices_html(mock_voices))
|
|
|
|
| 592 |
|
| 593 |
# TAB 4: Profile
|
| 594 |
with gr.TabItem("βοΈ Profile") as profile_tab:
|
|
@@ -803,7 +817,7 @@ with gr.Blocks(title="MomsVoice", css=css_code) as demo:
|
|
| 803 |
)
|
| 804 |
|
| 805 |
# 4. Add cloned voice to inventory
|
| 806 |
-
def save_new_cloned_voice(v_name, v_gender, list_voices):
|
| 807 |
avatar = "π©" if v_gender == "Female" else "π¨"
|
| 808 |
new_voice_item = {
|
| 809 |
"id": f"v-{time.time()}",
|
|
@@ -812,7 +826,8 @@ with gr.Blocks(title="MomsVoice", css=css_code) as demo:
|
|
| 812 |
"description": f"Custom cloned {v_gender.lower()} companion voice. Highly optimized.",
|
| 813 |
"gender": v_gender,
|
| 814 |
"created_date": "2026-06-06",
|
| 815 |
-
"status": "ready"
|
|
|
|
| 816 |
}
|
| 817 |
updated = [new_voice_item] + list_voices
|
| 818 |
return (
|
|
@@ -827,10 +842,34 @@ with gr.Blocks(title="MomsVoice", css=css_code) as demo:
|
|
| 827 |
|
| 828 |
add_to_library_btn.click(
|
| 829 |
save_new_cloned_voice,
|
| 830 |
-
inputs=[new_voice_name, new_voice_gender, voices_state],
|
| 831 |
outputs=[voices_state, cloned_voices_list_grid, cloning_progress_msg, voice_cloning_success_panel, voice_sample_preview_widget, add_to_library_btn, new_voice_name]
|
| 832 |
)
|
| 833 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 834 |
# 5. Play button β streams TTS audio chunk by chunk
|
| 835 |
def stream_tts(tts_chunks, paras, profile_id):
|
| 836 |
_status_playing = """
|
|
|
|
| 347 |
status_bg = "#f0fdf4" if v["status"] == "ready" else "#fef3c7"
|
| 348 |
safe_name = html.escape(v['name'])
|
| 349 |
safe_desc = html.escape(v['description'])
|
| 350 |
+
voice_id = html.escape(v.get('profile_id', v['id']))
|
| 351 |
+
|
| 352 |
+
onclick = f"""onclick="
|
| 353 |
+
const sel = document.querySelector('#voice_selector textarea, #voice_selector input');
|
| 354 |
+
if (sel) {{
|
| 355 |
+
sel.value = '{voice_id}';
|
| 356 |
+
sel.dispatchEvent(new Event('input', {{bubbles: true}}));
|
| 357 |
+
sel.dispatchEvent(new Event('change', {{bubbles: true}}));
|
| 358 |
+
}}
|
| 359 |
+
document.querySelectorAll('.voice-card-selected').forEach(c => c.classList.remove('voice-card-selected'));
|
| 360 |
+
this.classList.add('voice-card-selected');
|
| 361 |
+
" """
|
| 362 |
|
| 363 |
markup += f"""
|
| 364 |
+
<div class="shelf-card" style="align-items: center; cursor: pointer;" {onclick}>
|
| 365 |
<div style="width: 48px; height: 48px; border-radius: 50%; background: #ede8e3; border: 1px solid #ddc1b0; font-size: 24px; display: flex; align-items: center; justify-content: center; shrink-0: 0;">
|
| 366 |
{v['avatar_url']}
|
| 367 |
</div>
|
|
|
|
| 598 |
gr.HTML("""
|
| 599 |
<div style="margin-top: 32px; border-top: 1px solid #ebdccb; padding-top: 24px;">
|
| 600 |
<h4 class="serif-header" style="font-size: 18px; margin-bottom: 16px;">Currently Available Voices</h4>
|
| 601 |
+
<p style="font-size: 11px; color: #6f6257; margin-bottom: 12px;">Click a voice card to select it as the narrator.</p>
|
| 602 |
</div>
|
| 603 |
""")
|
| 604 |
cloned_voices_list_grid = gr.HTML(value=render_cloned_voices_html(mock_voices))
|
| 605 |
+
voice_selector = gr.Textbox(visible=False, elem_id="voice_selector")
|
| 606 |
|
| 607 |
# TAB 4: Profile
|
| 608 |
with gr.TabItem("βοΈ Profile") as profile_tab:
|
|
|
|
| 817 |
)
|
| 818 |
|
| 819 |
# 4. Add cloned voice to inventory
|
| 820 |
+
def save_new_cloned_voice(v_name, v_gender, list_voices, current_profile_id):
|
| 821 |
avatar = "π©" if v_gender == "Female" else "π¨"
|
| 822 |
new_voice_item = {
|
| 823 |
"id": f"v-{time.time()}",
|
|
|
|
| 826 |
"description": f"Custom cloned {v_gender.lower()} companion voice. Highly optimized.",
|
| 827 |
"gender": v_gender,
|
| 828 |
"created_date": "2026-06-06",
|
| 829 |
+
"status": "ready",
|
| 830 |
+
"profile_id": current_profile_id,
|
| 831 |
}
|
| 832 |
updated = [new_voice_item] + list_voices
|
| 833 |
return (
|
|
|
|
| 842 |
|
| 843 |
add_to_library_btn.click(
|
| 844 |
save_new_cloned_voice,
|
| 845 |
+
inputs=[new_voice_name, new_voice_gender, voices_state, voice_profile_state],
|
| 846 |
outputs=[voices_state, cloned_voices_list_grid, cloning_progress_msg, voice_cloning_success_panel, voice_sample_preview_widget, add_to_library_btn, new_voice_name]
|
| 847 |
)
|
| 848 |
|
| 849 |
+
# Voice card selection β update voice_profile_state when a card is clicked
|
| 850 |
+
def handle_voice_select(selected_id, voices):
|
| 851 |
+
"""Map selected voice ID to profile_id for TTS."""
|
| 852 |
+
if not selected_id:
|
| 853 |
+
return gr.update()
|
| 854 |
+
# Check if it matches a saved profile ID directly
|
| 855 |
+
from voice_clone import has_profile
|
| 856 |
+
if has_profile(selected_id):
|
| 857 |
+
return selected_id
|
| 858 |
+
# Check if it matches a voice in the list with a profile_id
|
| 859 |
+
for v in voices:
|
| 860 |
+
if v.get('profile_id') == selected_id or v.get('id') == selected_id:
|
| 861 |
+
pid = v.get('profile_id')
|
| 862 |
+
if pid and has_profile(pid):
|
| 863 |
+
return pid
|
| 864 |
+
# No valid profile β use stock voice (None)
|
| 865 |
+
return None
|
| 866 |
+
|
| 867 |
+
voice_selector.change(
|
| 868 |
+
handle_voice_select,
|
| 869 |
+
inputs=[voice_selector, voices_state],
|
| 870 |
+
outputs=[voice_profile_state]
|
| 871 |
+
)
|
| 872 |
+
|
| 873 |
# 5. Play button β streams TTS audio chunk by chunk
|
| 874 |
def stream_tts(tts_chunks, paras, profile_id):
|
| 875 |
_status_playing = """
|