Tajweed-AI / ui /components /reference_audio.py
hetchyy's picture
Add i8n
b8606b0
"""
Reference audio player component using native Gradio components.
Provides the reference reciter audio player with multi-verse navigation.
Supports:
- Single verse: Just audio player
- 2+ verses: Audio player with navigation arrows
- 3+ verses: Audio player with arrows and slider
Uses native Gradio components (gr.Audio, gr.Slider, gr.Button) instead of
custom HTML/JavaScript for HF Spaces compatibility.
"""
from typing import Optional
import gradio as gr
from i8n import t
def create_reference_audio_components(initial_audio_path: str | None = None) -> dict:
"""
Create native Gradio components for reference audio playback.
Args:
initial_audio_path: Optional path to initial audio file. Must be set during
component creation (not after) to properly render the waveform.
Returns a dict with all components needed for reference audio:
- nav_row: Row containing navigation buttons and slider
- prev_btn: Previous verse button
- slider: Slider for verse navigation (embedded in nav row)
- next_btn: Next verse button
- audio_player: Native Gradio audio player with "Reference Reciter" label
- verse_index: State tracking current index
- verse_keys: State tracking list of verse keys
Returns:
Dict with component references
"""
with gr.Column(elem_classes=["reference-audio-section"]):
# Navigation row with slider (hidden for single verse)
# Layout: [◀ 15%] [slider 70%] [▶ 15%]
with gr.Row(visible=False, elem_classes=["ref-audio-nav-row"]) as nav_row:
prev_btn = gr.Button(
"◀",
scale=1,
min_width=40,
elem_classes=["ref-audio-nav-btn"]
)
verse_slider = gr.Slider(
minimum=0,
maximum=0,
step=1,
value=0,
label="",
show_label=False,
scale=5,
elem_id="ref-audio-verse-slider",
elem_classes=["ref-audio-slider"]
)
next_btn = gr.Button(
"▶",
scale=1,
min_width=40,
elem_classes=["ref-audio-nav-btn"]
)
# Audio player (always visible)
audio_player = gr.Audio(
value=initial_audio_path,
label=t("audio.reference_reciter"),
show_label=False,
type="filepath",
interactive=False,
elem_classes=["ref-audio-player"]
)
# State components
verse_index = gr.State(0)
verse_keys = gr.State([])
return {
"nav_row": nav_row,
"prev_btn": prev_btn,
"slider": verse_slider,
"next_btn": next_btn,
"audio_player": audio_player,
"verse_index": verse_index,
"verse_keys": verse_keys,
}
# ============================================================================
# Legacy functions (kept for backward compatibility during transition)
# ============================================================================
def get_placeholder_audio_html() -> str:
"""
Return placeholder HTML to prevent empty-state Gradio bug.
DEPRECATED: Use create_reference_audio_components() instead.
Kept for backward compatibility.
"""
return '<div id="ref-audio-container" style="display:none;"></div>'
def build_reference_audio_html(
data_uris: list[str] | str | None = None,
urls: list[str] | str | None = None,
verse_keys: list[str] | None = None,
from_verse: int | None = None,
to_verse: int | None = None,
chapter: int | None = None,
label: str = "Reference Reciter",
lazy_load: bool = False
) -> str:
"""
Build HTML for audio player with multi-verse navigation.
DEPRECATED: Use create_reference_audio_components() instead.
Kept for backward compatibility during transition.
"""
# Determine which source to use (prefer urls for lazy loading)
sources = urls if urls is not None else data_uris
# Handle backward compatibility: convert single string to list
if isinstance(sources, str):
if not sources:
return ""
sources = [sources]
if from_verse is None:
from_verse = 1
if to_verse is None:
to_verse = 1
# In lazy_load mode, use verse_keys to determine verse count
if lazy_load and verse_keys:
verse_count = len(verse_keys)
elif sources:
verse_count = len(sources)
else:
return ""
# Ensure we have at least one source for the first verse
if not sources or len(sources) == 0:
return ""
# Determine verse range
if from_verse is None or to_verse is None:
from_verse = 1
to_verse = verse_count
# Determine what UI elements to show
show_navigation = verse_count >= 2
show_slider = verse_count >= 3
show_indicator = verse_count >= 2
# Serialize sources (URLs or data URIs) to JSON for JavaScript
import json
sources_json = json.dumps(sources)
verse_keys_json = json.dumps(verse_keys) if verse_keys else "[]"
# CSS styles
container_style = '''
position: relative;
padding: 12px 14px;
border: 1px solid var(--border-color-primary, #e5e7eb);
border-radius: 10px;
background: var(--background-fill-secondary, #f3f4f6);
'''
header_style = '''
display: flex;
justify-content: flex-start;
align-items: center;
margin-bottom: 8px;
'''
title_style = '''
font-weight: 600;
font-size: 14px;
color: var(--body-text-color, inherit);
'''
verse_indicator_style = '''
position: absolute;
top: 8px;
right: 14px;
background: #fb8c00;
color: white;
padding: 4px 10px;
border-radius: 6px;
font-size: 12px;
font-weight: 600;
'''
slider_row_style = '''
margin-bottom: 10px;
padding: 0 30px;
'''
slider_style = '''
width: 100%;
height: 6px;
cursor: pointer;
accent-color: #fb8c00;
'''
player_row_style = '''
display: flex;
align-items: center;
gap: 12px;
'''
nav_button_style = '''
background: var(--button-secondary-background-fill, #f3f4f6);
border: 1px solid var(--border-color-primary, #e5e7eb);
border-radius: 6px;
padding: 8px 12px;
cursor: pointer;
font-size: 16px;
font-weight: 600;
transition: background 0.2s;
color: var(--body-text-color, inherit);
'''
audio_style = '''
flex: 1;
height: 36px;
min-width: 0;
'''
# Build final HTML
html = f'''
<div id="ref-audio-container"
style="{container_style}"
data-verse-count="{verse_count}"
data-current-verse="0"
data-uris='{sources_json}'
data-verse-keys='{verse_keys_json}'
data-from-verse="{from_verse}"
data-chapter="{chapter or 1}"
data-lazy-load="{str(lazy_load).lower()}">
<div style="{header_style}">
<span style="{title_style}">{label}</span>
</div>
{'<div style="' + verse_indicator_style + '" id="verse-indicator">Verse ' + str(from_verse) + '</div>' if show_indicator else ''}
{'<div style="' + slider_row_style + '"><input type="range" id="verse-slider" min="0" max="' + str(verse_count - 1) + '" value="0" style="' + slider_style + '"></div>' if show_slider else ''}
<div style="{player_row_style}">
{'<button class="ref-audio-nav-btn" data-direction="-1" style="' + nav_button_style + '">◀</button>' if show_navigation else ''}
<audio id="ref-audio-player" controls style="{audio_style}">
<source src="{sources[0]}" type="audio/mpeg">
Your browser does not support the audio element.
</audio>
{'<button class="ref-audio-nav-btn" data-direction="1" style="' + nav_button_style + '">▶</button>' if show_navigation else ''}
</div>
</div>
'''
return html
def create_reference_audio_section(init_audio_html: str) -> dict:
"""
Create reference audio section with just the audio player.
DEPRECATED: Use create_reference_audio_components() instead.
Kept for backward compatibility during transition.
"""
# Main audio player row
with gr.Row(equal_height=True) as audio_row:
with gr.Column(scale=1, min_width=80, visible=False) as dropdown_col:
dropdown = gr.Dropdown(visible=False)
with gr.Column(scale=4) as audio_col:
audio_html = gr.HTML(
value=init_audio_html,
elem_id="reference-audio-container"
)
return {
"audio_row": audio_row,
"audio_html": audio_html,
"dropdown_col": dropdown_col,
"dropdown": dropdown,
}