hetchyy commited on
Commit
cee53d8
·
1 Parent(s): a7294fb
app.py CHANGED
@@ -112,13 +112,14 @@ def parse_args():
112
  # Must be created at import time.
113
  initialize_app()
114
 
115
- # Initialize locale from environment or default to English
116
- # Set TAJWEED_LOCALE=ar to run in Arabic mode
 
117
  from i8n import set_locale, get_available_locales
118
  env_locale = os.environ.get("TAJWEED_LOCALE", "en")
119
  if env_locale in get_available_locales():
120
  set_locale(env_locale)
121
- print(f"Locale set to: {env_locale}")
122
 
123
  # Import after initialization (depends on shared_state being populated)
124
  from ui.builder import build_interface
 
112
  # Must be created at import time.
113
  initialize_app()
114
 
115
+ # Initialize locale from environment (fallback for initial render)
116
+ # The actual locale is set dynamically via ?__locale query param on page load
117
+ # User preference is stored in localStorage and applied automatically
118
  from i8n import set_locale, get_available_locales
119
  env_locale = os.environ.get("TAJWEED_LOCALE", "en")
120
  if env_locale in get_available_locales():
121
  set_locale(env_locale)
122
+ print(f"Default locale: {env_locale}")
123
 
124
  # Import after initialization (depends on shared_state being populated)
125
  from ui.builder import build_interface
recitation_engine/model_loader.py CHANGED
@@ -120,11 +120,8 @@ def load_models():
120
  Tuple (bundles, errors) where bundles is a list of dicts with keys
121
  path, processor, model. errors is a list of error strings (may be empty).
122
  """
123
- # Return cached models if already loaded
124
  if _model_cache["loaded"]:
125
- # Ensure devices are correct in case GPUs became available later
126
- for bundle in _model_cache["bundles"]:
127
- bundle["model"] = _ensure_device(bundle["model"])
128
  return _model_cache["bundles"], _model_cache["errors"]
129
 
130
  bundles = []
 
120
  Tuple (bundles, errors) where bundles is a list of dicts with keys
121
  path, processor, model. errors is a list of error strings (may be empty).
122
  """
123
+ # Return cached models if already loaded (don't move them - use move_models_to_gpu() for that)
124
  if _model_cache["loaded"]:
 
 
 
125
  return _model_cache["bundles"], _model_cache["errors"]
126
 
127
  bundles = []
recitation_engine/segment_processor.py CHANGED
@@ -147,19 +147,16 @@ def move_segment_models_to_gpu():
147
 
148
 
149
  def _load_segmenter():
150
- """Load the VAD segmenter model."""
151
- device, dtype = _get_device_and_dtype()
152
 
 
 
 
 
153
  if _segmenter_cache["loaded"]:
154
- model = _segmenter_cache["model"]
155
- if model is not None:
156
- current_device = next(model.parameters()).device
157
- current_dtype = next(model.parameters()).dtype
158
- if current_device != device or current_dtype != dtype:
159
- model = model.to(device=device, dtype=dtype)
160
- _segmenter_cache["model"] = model
161
- print(f"✓ Segmenter moved to {device}")
162
  return _segmenter_cache["model"], _segmenter_cache["processor"]
 
 
163
 
164
  try:
165
  from transformers import AutoFeatureExtractor, AutoModelForAudioFrameClassification
@@ -180,25 +177,17 @@ def _load_segmenter():
180
 
181
 
182
  def _load_whisper():
183
- """Load the Whisper ASR model."""
184
- device, dtype = _get_device_and_dtype()
185
 
 
 
 
 
186
  if _whisper_cache["loaded"]:
187
- model = _whisper_cache["model"]
188
- if model is not None:
189
- current_device = next(model.parameters()).device
190
- current_dtype = next(model.parameters()).dtype
191
- if current_device != device or current_dtype != dtype:
192
- model = model.to(device=device, dtype=dtype)
193
- _whisper_cache["model"] = model
194
- print(f"✓ Whisper moved to {device}")
195
- prompt_ids = _whisper_cache["prompt_ids"]
196
- if prompt_ids is not None and prompt_ids.device != device:
197
- prompt_ids = prompt_ids.to(device)
198
- _whisper_cache["prompt_ids"] = prompt_ids
199
-
200
- return (_whisper_cache["model"], _whisper_cache["processor"],
201
  _whisper_cache["gen_config"], _whisper_cache["prompt_ids"])
 
 
202
 
203
  try:
204
  from transformers import AutoProcessor, AutoModelForSpeechSeq2Seq, GenerationConfig
 
147
 
148
 
149
  def _load_segmenter():
150
+ """Load the VAD segmenter model.
 
151
 
152
+ Note: This function only loads the model, it does NOT move it between devices.
153
+ Use move_segment_models_to_gpu() to move models to GPU inside GPU contexts.
154
+ """
155
+ # If already loaded, just return it (don't move it)
156
  if _segmenter_cache["loaded"]:
 
 
 
 
 
 
 
 
157
  return _segmenter_cache["model"], _segmenter_cache["processor"]
158
+
159
+ device, dtype = _get_device_and_dtype()
160
 
161
  try:
162
  from transformers import AutoFeatureExtractor, AutoModelForAudioFrameClassification
 
177
 
178
 
179
  def _load_whisper():
180
+ """Load the Whisper ASR model.
 
181
 
182
+ Note: This function only loads the model, it does NOT move it between devices.
183
+ Use move_segment_models_to_gpu() to move models to GPU inside GPU contexts.
184
+ """
185
+ # If already loaded, just return it (don't move it)
186
  if _whisper_cache["loaded"]:
187
+ return (_whisper_cache["model"], _whisper_cache["processor"],
 
 
 
 
 
 
 
 
 
 
 
 
 
188
  _whisper_cache["gen_config"], _whisper_cache["prompt_ids"])
189
+
190
+ device, dtype = _get_device_and_dtype()
191
 
192
  try:
193
  from transformers import AutoProcessor, AutoModelForSpeechSeq2Seq, GenerationConfig
ui/builder.py CHANGED
@@ -5,7 +5,7 @@ Assembles all UI components and wires event handlers.
5
  """
6
  import gradio as gr
7
 
8
- from i8n import t, get_locale
9
  from config import (
10
  DEV_TAB_VISIBLE,
11
  IS_HF_SPACE,
@@ -46,6 +46,85 @@ from recitation_engine.reference_audio import get_verse_audio_path, parse_verse_
46
  from ui.handlers.lazy_audio import fetch_segment_clip_lazy
47
 
48
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
49
  def _compute_initial_state(chapters_list: list) -> dict:
50
  """
51
  Compute initial state by phonemizing the default verse selection.
@@ -136,62 +215,29 @@ def build_interface() -> gr.Blocks:
136
  if use_css_fallback:
137
  inject_css_fallback()
138
 
139
- # Header with language toggle
140
- current_locale = get_locale()
141
- en_active = "active" if current_locale == "en" else ""
142
- ar_active = "active" if current_locale == "ar" else ""
143
 
144
- gr.HTML(
 
145
  f"""
146
  <div style="text-align: center;">
147
  <h1 style="margin-bottom: 0;">{t("header.title")}</h1>
148
  </div>
149
- <div class="lang-toggle-container">
150
- <div class="lang-toggle">
151
- <button id="lang-btn-en" class="{en_active}" onclick="switchLocale('en')">English</button>
152
- <button id="lang-btn-ar" class="{ar_active}" onclick="switchLocale('ar')">العربية</button>
153
- </div>
154
- </div>
155
- <script>
156
- // Initialize locale from localStorage on page load
157
- (function() {{
158
- const storedLocale = localStorage.getItem('tajweed_locale');
159
- const currentServerLocale = '{current_locale}';
160
-
161
- // If stored locale differs from server locale, reload with locale param
162
- if (storedLocale && storedLocale !== currentServerLocale) {{
163
- // Update URL and reload to apply the stored locale
164
- const url = new URL(window.location.href);
165
- url.searchParams.set('__locale', storedLocale);
166
- if (!window.location.search.includes('__locale=' + storedLocale)) {{
167
- window.location.href = url.toString();
168
- }}
169
- }}
170
-
171
- // Sync toggle button visual state with stored preference
172
- if (storedLocale) {{
173
- document.getElementById('lang-btn-en')?.classList.toggle('active', storedLocale === 'en');
174
- document.getElementById('lang-btn-ar')?.classList.toggle('active', storedLocale === 'ar');
175
- }}
176
- }})();
177
-
178
- function switchLocale(locale) {{
179
- const currentLocale = localStorage.getItem('tajweed_locale') || 'en';
180
- if (locale !== currentLocale) {{
181
- localStorage.setItem('tajweed_locale', locale);
182
- // Reload with locale query param
183
- const url = new URL(window.location.href);
184
- url.searchParams.set('__locale', locale);
185
- window.location.href = url.toString();
186
- }}
187
- }}
188
- </script>
189
  """
190
  )
191
 
192
- # ----- Create all components -----
193
- components = {}
194
- states = {}
 
 
 
 
 
 
 
195
 
196
  # Hidden components to hold session values (must be outside Row/Column for event wiring)
197
  states["expected_phonemes"] = gr.Textbox(
@@ -530,4 +576,54 @@ def build_interface() -> gr.Blocks:
530
  outputs=[states["segment_clip_response"]]
531
  )
532
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
533
  return app
 
5
  """
6
  import gradio as gr
7
 
8
+ from i8n import t, get_locale, set_locale, get_available_locales
9
  from config import (
10
  DEV_TAB_VISIBLE,
11
  IS_HF_SPACE,
 
46
  from ui.handlers.lazy_audio import fetch_segment_clip_lazy
47
 
48
 
49
+ def _on_locale_load(request: gr.Request):
50
+ """
51
+ Handle locale initialization on page load.
52
+
53
+ Reads the __locale query parameter and updates all component labels
54
+ to use the correct language.
55
+ """
56
+ # Read locale from query parameter
57
+ locale = request.query_params.get("__locale", "en")
58
+
59
+ # Validate and set locale
60
+ if locale in get_available_locales():
61
+ set_locale(locale)
62
+ else:
63
+ locale = "en"
64
+ set_locale("en")
65
+
66
+ # Build updated header HTML with correct locale
67
+ header_html = f"""
68
+ <div style="text-align: center;">
69
+ <h1 style="margin-bottom: 0;">{t("header.title")}</h1>
70
+ </div>
71
+ """
72
+
73
+ # Return updates for all translatable components
74
+ # Order must match the outputs list in the .load() event
75
+ return [
76
+ # 1. Header HTML
77
+ gr.update(value=header_html),
78
+ # 2. Language toggle (set to current locale)
79
+ gr.update(value=locale),
80
+ # 3. Audio input label
81
+ gr.update(label=t("audio.label")),
82
+ # 4. Analyze button
83
+ gr.update(value=t("controls.analyze")),
84
+ # 5. Duration tolerance slider
85
+ gr.update(label=t("settings.duration_tolerance"), info=t("settings.duration_tolerance_info")),
86
+ # 6. Iqlab/Ikhfaa sound radio
87
+ gr.update(
88
+ label=t("tajweed_settings.iqlab_ikhfaa_sound"),
89
+ choices=[
90
+ (t("tajweed_settings.meem_ghunnah"), "meem ghunnah"),
91
+ (t("tajweed_settings.ikhfaa"), "ikhfaa")
92
+ ]
93
+ ),
94
+ # 7. Ghunnah length radio
95
+ gr.update(label=t("tajweed_settings.ghunnah_length")),
96
+ # 8. Madd Jaiz length radio
97
+ gr.update(label=t("tajweed_settings.jaiz_munfasil")),
98
+ # 9. Madd Arid length radio
99
+ gr.update(label=t("tajweed_settings.arid_lissukun")),
100
+ # 10. Madd Leen length radio
101
+ gr.update(label=t("tajweed_settings.leen")),
102
+ # 11. Madd Wajib length radio
103
+ gr.update(label=t("tajweed_settings.wajib_muttasil")),
104
+ # 12. Madd Lazim length radio
105
+ gr.update(label=t("tajweed_settings.lazim")),
106
+ # 13. Madd Tabii length radio
107
+ gr.update(label=t("tajweed_settings.tabii")),
108
+ # 14. Error sort dropdown
109
+ gr.update(choices=[
110
+ (t("sort_options.text_order"), "text_order"),
111
+ (t("sort_options.by_errors"), "by_errors")
112
+ ]),
113
+ # 15. Ghunnah sort dropdown
114
+ gr.update(choices=[
115
+ (t("sort_options.text_order"), "text_order"),
116
+ (t("sort_options.by_rule"), "by_rule"),
117
+ (t("sort_options.by_errors"), "by_errors")
118
+ ]),
119
+ # 16. Madd sort dropdown
120
+ gr.update(choices=[
121
+ (t("sort_options.text_order"), "text_order"),
122
+ (t("sort_options.by_rule"), "by_rule"),
123
+ (t("sort_options.by_errors"), "by_errors")
124
+ ]),
125
+ ]
126
+
127
+
128
  def _compute_initial_state(chapters_list: list) -> dict:
129
  """
130
  Compute initial state by phonemizing the default verse selection.
 
215
  if use_css_fallback:
216
  inject_css_fallback()
217
 
218
+ # ----- Create all components -----
219
+ components = {}
220
+ states = {}
 
221
 
222
+ # Header with title
223
+ components["header_html"] = gr.HTML(
224
  f"""
225
  <div style="text-align: center;">
226
  <h1 style="margin-bottom: 0;">{t("header.title")}</h1>
227
  </div>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
228
  """
229
  )
230
 
231
+ # Language toggle using native Gradio Radio (styled via CSS)
232
+ current_locale = get_locale()
233
+ with gr.Row(elem_classes=["lang-toggle-container"]):
234
+ components["lang_toggle"] = gr.Radio(
235
+ choices=[("English", "en"), ("العربية", "ar")],
236
+ value=current_locale,
237
+ show_label=False,
238
+ container=False,
239
+ elem_classes=["lang-toggle-radio"],
240
+ )
241
 
242
  # Hidden components to hold session values (must be outside Row/Column for event wiring)
243
  states["expected_phonemes"] = gr.Textbox(
 
576
  outputs=[states["segment_clip_response"]]
577
  )
578
 
579
+ # ----- Wire locale initialization on page load -----
580
+ # This reads the __locale query parameter and updates all translatable components
581
+ app.load(
582
+ fn=_on_locale_load,
583
+ inputs=[],
584
+ outputs=[
585
+ components["header_html"],
586
+ components["lang_toggle"],
587
+ components["audio_input"],
588
+ components["analyze_btn"],
589
+ components["duration_tolerance"],
590
+ components["iqlab_ikhfaa_sound"],
591
+ components["ghunnah_length"],
592
+ components["madd_jaiz_length"],
593
+ components["madd_arid_length"],
594
+ components["madd_leen_length"],
595
+ components["madd_wajib_length"],
596
+ components["madd_lazim_length"],
597
+ components["madd_tabii_length"],
598
+ components["error_sort_dropdown"],
599
+ components["ghunnah_sort_dropdown"],
600
+ components["madd_sort_dropdown"],
601
+ ]
602
+ )
603
+
604
+ # ----- Wire language toggle change -----
605
+ # When user clicks the toggle, store in localStorage and reload with new locale
606
+ locale_change_js = """
607
+ (locale) => {
608
+ if (locale) {
609
+ const url = new URL(window.location.href);
610
+ const currentUrlLocale = url.searchParams.get('__locale');
611
+
612
+ // Only reload if URL locale is different (prevents infinite loop)
613
+ if (currentUrlLocale !== locale) {
614
+ localStorage.setItem('tajweed_locale', locale);
615
+ url.searchParams.set('__locale', locale);
616
+ setTimeout(() => { window.location.href = url.toString(); }, 100);
617
+ }
618
+ }
619
+ return locale;
620
+ }
621
+ """
622
+ components["lang_toggle"].change(
623
+ fn=lambda x: x,
624
+ inputs=[components["lang_toggle"]],
625
+ outputs=[components["lang_toggle"]],
626
+ js=locale_change_js
627
+ )
628
+
629
  return app
ui/styles.py CHANGED
@@ -234,22 +234,33 @@ def get_custom_css() -> str:
234
  .ref-audio-player audio {
235
  height: 40px !important;
236
  }
237
- /* Language toggle - styled like sort toggle pills */
238
  .lang-toggle-container {
239
  display: flex !important;
240
  justify-content: center !important;
241
  margin-top: 4px !important;
242
  margin-bottom: 8px !important;
 
 
 
 
 
 
243
  }
244
- .lang-toggle {
 
245
  display: inline-flex !important;
246
- gap: 0 !important;
247
  background: var(--background-fill-primary, #1f2937) !important;
248
  border-radius: 20px !important;
249
- padding: 3px !important;
250
  border: 1px solid var(--border-color-primary, #374151) !important;
251
  }
252
- .lang-toggle button {
 
 
 
 
 
253
  padding: 6px 16px !important;
254
  border: none !important;
255
  border-radius: 17px !important;
@@ -259,17 +270,21 @@ def get_custom_css() -> str:
259
  font-weight: 500 !important;
260
  cursor: pointer !important;
261
  transition: all 0.2s ease !important;
262
- font-family: inherit !important;
263
  }
264
- .lang-toggle button:hover:not(.active) {
265
  background: var(--background-fill-secondary, #374151) !important;
266
  color: var(--body-text-color, #f3f4f6) !important;
267
  }
268
- .lang-toggle button.active {
 
269
  background: linear-gradient(135deg, #f97316 0%, #ea580c 100%) !important;
270
  color: white !important;
271
  box-shadow: 0 2px 6px rgba(249, 115, 22, 0.35) !important;
272
  }
 
 
 
273
  """
274
 
275
  return get_force_dark_mode_css() + get_digital_khatt_font_face(DIGITAL_KHATT_FONT_B64) + get_uthmanic_font_face() + toggle_button_css
 
234
  .ref-audio-player audio {
235
  height: 40px !important;
236
  }
237
+ /* Language toggle container - centered under title */
238
  .lang-toggle-container {
239
  display: flex !important;
240
  justify-content: center !important;
241
  margin-top: 4px !important;
242
  margin-bottom: 8px !important;
243
+ min-height: auto !important;
244
+ padding: 0 !important;
245
+ }
246
+ .lang-toggle-container > div {
247
+ flex: 0 0 auto !important;
248
+ width: auto !important;
249
  }
250
+ /* Language toggle radio - pill style like sort toggle */
251
+ .lang-toggle-radio {
252
  display: inline-flex !important;
253
+ padding: 0 !important;
254
  background: var(--background-fill-primary, #1f2937) !important;
255
  border-radius: 20px !important;
 
256
  border: 1px solid var(--border-color-primary, #374151) !important;
257
  }
258
+ .lang-toggle-radio > div {
259
+ display: inline-flex !important;
260
+ gap: 0 !important;
261
+ padding: 3px !important;
262
+ }
263
+ .lang-toggle-radio label {
264
  padding: 6px 16px !important;
265
  border: none !important;
266
  border-radius: 17px !important;
 
270
  font-weight: 500 !important;
271
  cursor: pointer !important;
272
  transition: all 0.2s ease !important;
273
+ margin: 0 !important;
274
  }
275
+ .lang-toggle-radio label:hover {
276
  background: var(--background-fill-secondary, #374151) !important;
277
  color: var(--body-text-color, #f3f4f6) !important;
278
  }
279
+ .lang-toggle-radio input:checked + label,
280
+ .lang-toggle-radio label.selected {
281
  background: linear-gradient(135deg, #f97316 0%, #ea580c 100%) !important;
282
  color: white !important;
283
  box-shadow: 0 2px 6px rgba(249, 115, 22, 0.35) !important;
284
  }
285
+ .lang-toggle-radio input[type="radio"] {
286
+ display: none !important;
287
+ }
288
  """
289
 
290
  return get_force_dark_mode_css() + get_digital_khatt_font_face(DIGITAL_KHATT_FONT_B64) + get_uthmanic_font_face() + toggle_button_css