hetchyy commited on
Commit
a5c555e
·
1 Parent(s): 3e13e03

Add ghunnah/madd durations

Browse files
recitation_analysis/duration_analysis/ghunnah_analysis.py CHANGED
@@ -276,13 +276,15 @@ def _find_idgham_shafawi_target(
276
  next_word = words[source_word_idx + 1]
277
 
278
  for letter in next_word.letters:
279
- if letter.is_silent:
280
- continue
281
-
282
- # Look for meem with shaddah
283
  if letter.char == '\u0645' and letter.shaddah:
284
  return (source_word_idx + 1, letter.index, next_word.location, _get_letter_text(letter), _get_word_text(next_word))
285
 
 
 
 
 
 
286
  break
287
 
288
  return None
 
276
  next_word = words[source_word_idx + 1]
277
 
278
  for letter in next_word.letters:
279
+ # Check for target meem first (it may be silent when followed by vowel carrier)
 
 
 
280
  if letter.char == '\u0645' and letter.shaddah:
281
  return (source_word_idx + 1, letter.index, next_word.location, _get_letter_text(letter), _get_word_text(next_word))
282
 
283
+ # Skip silent letters (like alif wasl)
284
+ if letter.is_silent:
285
+ continue
286
+
287
+ # First non-silent letter wasn't target - stop
288
  break
289
 
290
  return None
recitation_analysis/ui/components.py CHANGED
@@ -190,19 +190,10 @@ def render_collapsible_section(
190
  """
191
  open_attr = '' if collapsed else 'open'
192
 
193
- # Status-based colors
194
- if status == 'incorrect':
195
- accent_color = '#e53935' # red
196
- bg_color = 'rgba(229, 57, 53, 0.08)'
197
- border_color = 'rgba(229, 57, 53, 0.3)'
198
- elif status == 'correct':
199
- accent_color = '#43a047' # green
200
- bg_color = 'rgba(67, 160, 71, 0.08)'
201
- border_color = 'rgba(67, 160, 71, 0.3)'
202
- else:
203
- accent_color = 'var(--body-text-color, inherit)'
204
- bg_color = 'var(--background-fill-primary, #1f2937)'
205
- border_color = 'var(--border-color-primary, #374151)'
206
 
207
  return f'''
208
  <style>
 
190
  """
191
  open_attr = '' if collapsed else 'open'
192
 
193
+ # Use app theme color (orange) for all statuses
194
+ accent_color = '#f97316' # app theme orange
195
+ bg_color = 'rgba(249, 115, 22, 0.08)'
196
+ border_color = 'rgba(249, 115, 22, 0.3)'
 
 
 
 
 
 
 
 
 
197
 
198
  return f'''
199
  <style>
ui/builder.py CHANGED
@@ -207,7 +207,8 @@ def build_interface() -> gr.Blocks:
207
  components["audio_input"] = gr.Audio(
208
  sources=["microphone", "upload"],
209
  type="filepath",
210
- label="Record or Upload Your Recitation"
 
211
  )
212
 
213
  # Analyze button
 
207
  components["audio_input"] = gr.Audio(
208
  sources=["microphone", "upload"],
209
  type="filepath",
210
+ label="Record or Upload Your Recitation",
211
+ show_label=False
212
  )
213
 
214
  # Analyze button
ui/components/audio_input.py CHANGED
@@ -19,7 +19,8 @@ def create_audio_input_section() -> dict:
19
  audio_input = gr.Audio(
20
  sources=["microphone", "upload"],
21
  type="numpy",
22
- label="🎤 Record or Upload Your Recitation"
 
23
  )
24
 
25
  analyze_btn = gr.Button("Analyze", variant="primary", visible=False)
 
19
  audio_input = gr.Audio(
20
  sources=["microphone", "upload"],
21
  type="numpy",
22
+ label="Record or Upload Your Recitation",
23
+ show_label=False
24
  )
25
 
26
  analyze_btn = gr.Button("Analyze", variant="primary", visible=False)
ui/components/reference_audio.py CHANGED
@@ -59,9 +59,10 @@ def create_reference_audio_components() -> dict:
59
  elem_classes=["ref-audio-nav-btn"]
60
  )
61
 
62
- # Audio player with label (always visible)
63
  audio_player = gr.Audio(
64
- label="🔊 Reference Reciter",
 
65
  type="filepath",
66
  interactive=False,
67
  elem_classes=["ref-audio-player"]
 
59
  elem_classes=["ref-audio-nav-btn"]
60
  )
61
 
62
+ # Audio player (always visible)
63
  audio_player = gr.Audio(
64
+ label="Reference Reciter",
65
+ show_label=False,
66
  type="filepath",
67
  interactive=False,
68
  elem_classes=["ref-audio-player"]
ui/handlers/reference_audio.py CHANGED
@@ -10,6 +10,14 @@ import gradio as gr
10
  from recitation_engine.reference_audio import get_verse_audio_path, parse_verse_range
11
 
12
 
 
 
 
 
 
 
 
 
13
  def on_ref_audio_prev_click(
14
  current_idx: int,
15
  keys: List[str]
@@ -23,6 +31,7 @@ def on_ref_audio_prev_click(
23
 
24
  Returns:
25
  Tuple of (new_index, slider_value, audio_path)
 
26
  """
27
  if not keys:
28
  return 0, 0, None
@@ -30,8 +39,9 @@ def on_ref_audio_prev_click(
30
  new_idx = max(0, current_idx - 1)
31
  verse_key = keys[new_idx]
32
  audio_path = get_verse_audio_path(verse_key)
 
33
 
34
- return new_idx, new_idx, audio_path
35
 
36
 
37
  def on_ref_audio_next_click(
@@ -47,6 +57,7 @@ def on_ref_audio_next_click(
47
 
48
  Returns:
49
  Tuple of (new_index, slider_value, audio_path)
 
50
  """
51
  if not keys:
52
  return 0, 0, None
@@ -54,8 +65,9 @@ def on_ref_audio_next_click(
54
  new_idx = min(len(keys) - 1, current_idx + 1)
55
  verse_key = keys[new_idx]
56
  audio_path = get_verse_audio_path(verse_key)
 
57
 
58
- return new_idx, new_idx, audio_path
59
 
60
 
61
  def on_ref_audio_slider_change(
@@ -66,22 +78,29 @@ def on_ref_audio_slider_change(
66
  Handle slider change - navigate to specific verse.
67
 
68
  Args:
69
- slider_val: Slider value (verse index, may be float)
70
  keys: List of verse keys
71
 
72
  Returns:
73
  Tuple of (new_index, audio_path)
74
  """
 
 
 
75
  # Handle decimal input by rounding to nearest integer
76
  slider_val = int(round(slider_val))
77
 
78
- if not keys or slider_val < 0 or slider_val >= len(keys):
 
 
 
 
79
  return 0, None
80
 
81
- verse_key = keys[slider_val]
82
  audio_path = get_verse_audio_path(verse_key)
83
 
84
- return slider_val, audio_path
85
 
86
 
87
  def init_ref_audio_for_verse_range(
@@ -90,7 +109,7 @@ def init_ref_audio_for_verse_range(
90
  List[str], # verse_keys state
91
  int, # verse_index state
92
  gr.update, # nav_row visibility
93
- gr.update, # slider max + value
94
  Optional[str] # audio_path
95
  ]:
96
  """
@@ -109,7 +128,7 @@ def init_ref_audio_for_verse_range(
109
  [], # verse_keys
110
  0, # verse_index
111
  gr.update(visible=False), # nav_row
112
- gr.update(maximum=0, value=0), # slider
113
  None # audio_path
114
  )
115
 
@@ -119,7 +138,7 @@ def init_ref_audio_for_verse_range(
119
  [],
120
  0,
121
  gr.update(visible=False),
122
- gr.update(maximum=0, value=0),
123
  None
124
  )
125
 
@@ -127,8 +146,13 @@ def init_ref_audio_for_verse_range(
127
 
128
  # Load first verse audio
129
  first_key = keys[0]
 
130
  audio_path = get_verse_audio_path(first_key)
131
 
 
 
 
 
132
  # Show nav row for 2+ verses (slider is always in nav row now)
133
  show_nav = count > 1
134
 
@@ -136,7 +160,7 @@ def init_ref_audio_for_verse_range(
136
  keys, # verse_keys
137
  0, # verse_index
138
  gr.update(visible=show_nav), # nav_row
139
- gr.update(maximum=count - 1, value=0), # slider
140
  audio_path # audio_path
141
  )
142
 
 
10
  from recitation_engine.reference_audio import get_verse_audio_path, parse_verse_range
11
 
12
 
13
+ def _get_verse_number(verse_key: str) -> int:
14
+ """Extract verse number from verse key like '1:5' -> 5."""
15
+ try:
16
+ return int(verse_key.split(":")[1])
17
+ except (IndexError, ValueError):
18
+ return 0
19
+
20
+
21
  def on_ref_audio_prev_click(
22
  current_idx: int,
23
  keys: List[str]
 
31
 
32
  Returns:
33
  Tuple of (new_index, slider_value, audio_path)
34
+ slider_value is actual verse number, not index
35
  """
36
  if not keys:
37
  return 0, 0, None
 
39
  new_idx = max(0, current_idx - 1)
40
  verse_key = keys[new_idx]
41
  audio_path = get_verse_audio_path(verse_key)
42
+ slider_val = _get_verse_number(verse_key)
43
 
44
+ return new_idx, slider_val, audio_path
45
 
46
 
47
  def on_ref_audio_next_click(
 
57
 
58
  Returns:
59
  Tuple of (new_index, slider_value, audio_path)
60
+ slider_value is actual verse number, not index
61
  """
62
  if not keys:
63
  return 0, 0, None
 
65
  new_idx = min(len(keys) - 1, current_idx + 1)
66
  verse_key = keys[new_idx]
67
  audio_path = get_verse_audio_path(verse_key)
68
+ slider_val = _get_verse_number(verse_key)
69
 
70
+ return new_idx, slider_val, audio_path
71
 
72
 
73
  def on_ref_audio_slider_change(
 
78
  Handle slider change - navigate to specific verse.
79
 
80
  Args:
81
+ slider_val: Slider value (actual verse number, may be float)
82
  keys: List of verse keys
83
 
84
  Returns:
85
  Tuple of (new_index, audio_path)
86
  """
87
+ if not keys:
88
+ return 0, None
89
+
90
  # Handle decimal input by rounding to nearest integer
91
  slider_val = int(round(slider_val))
92
 
93
+ # Convert verse number to index
94
+ first_verse = _get_verse_number(keys[0])
95
+ idx = slider_val - first_verse
96
+
97
+ if idx < 0 or idx >= len(keys):
98
  return 0, None
99
 
100
+ verse_key = keys[idx]
101
  audio_path = get_verse_audio_path(verse_key)
102
 
103
+ return idx, audio_path
104
 
105
 
106
  def init_ref_audio_for_verse_range(
 
109
  List[str], # verse_keys state
110
  int, # verse_index state
111
  gr.update, # nav_row visibility
112
+ gr.update, # slider min, max, value
113
  Optional[str] # audio_path
114
  ]:
115
  """
 
128
  [], # verse_keys
129
  0, # verse_index
130
  gr.update(visible=False), # nav_row
131
+ gr.update(minimum=0, maximum=0, value=0), # slider
132
  None # audio_path
133
  )
134
 
 
138
  [],
139
  0,
140
  gr.update(visible=False),
141
+ gr.update(minimum=0, maximum=0, value=0),
142
  None
143
  )
144
 
 
146
 
147
  # Load first verse audio
148
  first_key = keys[0]
149
+ last_key = keys[-1]
150
  audio_path = get_verse_audio_path(first_key)
151
 
152
+ # Get actual verse numbers for slider range
153
+ first_verse = _get_verse_number(first_key)
154
+ last_verse = _get_verse_number(last_key)
155
+
156
  # Show nav row for 2+ verses (slider is always in nav row now)
157
  show_nav = count > 1
158
 
 
160
  keys, # verse_keys
161
  0, # verse_index
162
  gr.update(visible=show_nav), # nav_row
163
+ gr.update(minimum=first_verse, maximum=last_verse, value=first_verse), # slider
164
  audio_path # audio_path
165
  )
166
 
ui/handlers/verse_selection.py CHANGED
@@ -14,6 +14,7 @@ from ui.components.verse_selector import (
14
  from ui.components.output_display import format_arabic_text, format_no_verse_message, format_error_message
15
  from utils.phonemizer_utils import phonemize_verse, format_verse_reference
16
  from recitation_engine.reference_audio import get_verse_audio_path, parse_verse_range
 
17
  from recitation_analysis.text_display import render_ghunnah_analysis_from_ref, render_madd_analysis_from_ref
18
  from shared_state import clear_all_audio_caches
19
 
@@ -163,7 +164,7 @@ def verse_selection_wrapper(from_chapter: str, from_verse: str, to_verse: str) -
163
  ref_audio_verse_keys = []
164
  ref_audio_verse_index = 0
165
  ref_audio_nav_row = gr.update(visible=False)
166
- ref_audio_slider = gr.update(maximum=0, value=0)
167
  ref_audio_player = None
168
 
169
  if verse_ref:
@@ -172,14 +173,20 @@ def verse_selection_wrapper(from_chapter: str, from_verse: str, to_verse: str) -
172
  if verse_keys:
173
  count = len(verse_keys)
174
  first_key = verse_keys[0]
 
175
  audio_path = get_verse_audio_path(first_key)
176
 
 
 
 
 
177
  ref_audio_verse_keys = verse_keys
178
  ref_audio_verse_index = 0
179
  ref_audio_nav_row = gr.update(visible=count > 1)
180
  ref_audio_slider = gr.update(
181
- maximum=count - 1,
182
- value=0
 
183
  )
184
  ref_audio_player = audio_path
185
  except Exception as e:
 
14
  from ui.components.output_display import format_arabic_text, format_no_verse_message, format_error_message
15
  from utils.phonemizer_utils import phonemize_verse, format_verse_reference
16
  from recitation_engine.reference_audio import get_verse_audio_path, parse_verse_range
17
+ from ui.handlers.reference_audio import _get_verse_number
18
  from recitation_analysis.text_display import render_ghunnah_analysis_from_ref, render_madd_analysis_from_ref
19
  from shared_state import clear_all_audio_caches
20
 
 
164
  ref_audio_verse_keys = []
165
  ref_audio_verse_index = 0
166
  ref_audio_nav_row = gr.update(visible=False)
167
+ ref_audio_slider = gr.update(minimum=0, maximum=0, value=0)
168
  ref_audio_player = None
169
 
170
  if verse_ref:
 
173
  if verse_keys:
174
  count = len(verse_keys)
175
  first_key = verse_keys[0]
176
+ last_key = verse_keys[-1]
177
  audio_path = get_verse_audio_path(first_key)
178
 
179
+ # Get actual verse numbers for slider range
180
+ first_verse = _get_verse_number(first_key)
181
+ last_verse = _get_verse_number(last_key)
182
+
183
  ref_audio_verse_keys = verse_keys
184
  ref_audio_verse_index = 0
185
  ref_audio_nav_row = gr.update(visible=count > 1)
186
  ref_audio_slider = gr.update(
187
+ minimum=first_verse,
188
+ maximum=last_verse,
189
+ value=first_verse
190
  )
191
  ref_audio_player = audio_path
192
  except Exception as e:
ui/styles.py CHANGED
@@ -213,10 +213,27 @@ def get_custom_css() -> str:
213
  .madd-settings-row {
214
  flex-wrap: nowrap !important;
215
  }
216
- /* Hide default Gradio label icons (music emoji on Audio, etc.) */
217
- .label-icon {
 
 
 
 
 
 
218
  display: none !important;
219
  }
 
 
 
 
 
 
 
 
 
 
 
220
  """
221
 
222
  return get_force_dark_mode_css() + get_digital_khatt_font_face(DIGITAL_KHATT_FONT_B64) + get_uthmanic_font_face() + toggle_button_css
 
213
  .madd-settings-row {
214
  flex-wrap: nowrap !important;
215
  }
216
+ /* Hide number input box and reset button in verse slider, remove empty space */
217
+ #ref-audio-verse-slider {
218
+ padding-top: 0 !important;
219
+ margin-top: 0 !important;
220
+ min-height: 0 !important;
221
+ }
222
+ #ref-audio-verse-slider input[type="number"],
223
+ #ref-audio-verse-slider button {
224
  display: none !important;
225
  }
226
+ #ref-audio-verse-slider .wrap {
227
+ padding: 0 !important;
228
+ gap: 0 !important;
229
+ }
230
+ /* Compact reference audio player */
231
+ .ref-audio-player {
232
+ min-height: 0 !important;
233
+ }
234
+ .ref-audio-player audio {
235
+ height: 40px !important;
236
+ }
237
  """
238
 
239
  return get_force_dark_mode_css() + get_digital_khatt_font_face(DIGITAL_KHATT_FONT_B64) + get_uthmanic_font_face() + toggle_button_css