aseelflihan commited on
Commit
e879d8d
·
1 Parent(s): 44a460d
Files changed (7) hide show
  1. app.py +152 -254
  2. audio_processor.py +12 -29
  3. integrated_server.py +0 -235
  4. recorder_server.py +0 -452
  5. summarizer.py +10 -19
  6. templates/recorder.html +0 -1958
  7. translator.py +25 -31
app.py CHANGED
@@ -1,4 +1,4 @@
1
- # app.py - Final, Corrected, and Heavily Logged Version
2
 
3
  import streamlit as st
4
  import os
@@ -8,28 +8,7 @@ from pathlib import Path
8
  import time
9
  import traceback
10
  import streamlit.components.v1 as components
11
-
12
- # --- Integrated Server Import ---
13
- import threading
14
-
15
- # Global variable to track server status
16
- recorder_server_status = {"started": False, "error": None}
17
-
18
- def start_recorder_async():
19
- """Start recorder server in background thread"""
20
- try:
21
- from integrated_server import ensure_recorder_server, stop_all_servers
22
- result = ensure_recorder_server()
23
- recorder_server_status["started"] = result
24
- # Make stop_all_servers available globally
25
- globals()['stop_all_servers'] = stop_all_servers
26
- except Exception as e:
27
- recorder_server_status["error"] = str(e)
28
- print(f"Warning: Could not start integrated recorder server: {e}")
29
-
30
- # Start recorder server in background to avoid blocking UI
31
- recorder_thread = threading.Thread(target=start_recorder_async, daemon=True)
32
- recorder_thread.start()
33
 
34
  # --- Critical Imports and Initial Checks ---
35
  AUDIO_PROCESSOR_CLASS = None
@@ -42,8 +21,27 @@ except Exception:
42
 
43
  from video_generator import VideoGenerator
44
  from mp3_embedder import MP3Embedder
45
- from utils import format_timestamp, validate_audio_file, get_audio_info
46
- from translator import get_translator, get_translation, UI_TRANSLATIONS
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
47
 
48
  # --- Page Configuration ---
49
  st.set_page_config(
@@ -86,8 +84,8 @@ def initialize_session_state():
86
  """Initializes the session state variables if they don't exist."""
87
  if 'step' not in st.session_state:
88
  st.session_state.step = 1
89
- if 'audio_file' not in st.session_state:
90
- st.session_state.audio_file = None
91
  if 'language' not in st.session_state:
92
  st.session_state.language = 'en'
93
  if 'enable_translation' not in st.session_state:
@@ -104,12 +102,96 @@ def initialize_session_state():
104
  'highlight_color': '#FFD700', 'background_color': '#000000',
105
  'font_family': 'Arial', 'font_size': 48
106
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
107
 
108
  # --- Main Application Logic ---
109
  def main():
110
  initialize_session_state()
111
 
112
- # Apply fast loading optimizations
113
  st.markdown("""
114
  <style>
115
  .stSpinner { display: none !important; }
@@ -119,11 +201,6 @@ def main():
119
  </style>
120
  """, unsafe_allow_html=True)
121
 
122
- # Check recorder server status (non-blocking)
123
- if recorder_server_status.get("error"):
124
- st.warning(f"⚠️ Recorder server issue: {recorder_server_status['error']}")
125
-
126
- # Language Selection in sidebar
127
  with st.sidebar:
128
  st.markdown("## 🌐 Language Settings")
129
  language_options = {'English': 'en', 'العربية': 'ar'}
@@ -134,7 +211,6 @@ def main():
134
  )
135
  st.session_state.language = language_options[selected_lang_display]
136
 
137
- # Translation Settings
138
  st.markdown("## 🔤 Translation Settings")
139
  st.session_state.enable_translation = st.checkbox(
140
  "Enable AI Translation" if st.session_state.language == 'en' else "تفعيل الترجمة بالذكاء الاصطناعي",
@@ -144,48 +220,27 @@ def main():
144
 
145
  if st.session_state.enable_translation:
146
  target_lang_options = {
147
- 'Arabic (العربية)': 'ar',
148
- 'English': 'en',
149
- 'French (Français)': 'fr',
150
- 'Spanish (Español)': 'es'
151
  }
152
  selected_target = st.selectbox(
153
  "Target Language" if st.session_state.language == 'en' else "اللغة المستهدفة",
154
- options=list(target_lang_options.keys()),
155
- index=0
156
  )
157
  st.session_state.target_language = target_lang_options[selected_target]
158
 
159
- # Get translations for current language
160
- current_translations = UI_TRANSLATIONS.get(st.session_state.language, UI_TRANSLATIONS['en'])
161
-
162
- # Main title with translation support
163
  st.title("🎵 SyncMaster")
164
  if st.session_state.language == 'ar':
165
  st.markdown("### منصة المزامنة الذكية بين الصوت والنص")
166
- st.markdown("حول ملفاتك الصوتية إلى ملفات MP3 متوافقة مع الأجهزة المحمولة مع كلمات متزامنة ومقاطع فيديو MP4 متحركة.")
167
  else:
168
  st.markdown("### The Intelligent Audio-Text Synchronization Platform")
169
- st.markdown("Transform your audio files into mobile-compatible MP3s with synchronized lyrics and animated MP4 videos.")
170
 
171
  col1, col2, col3 = st.columns(3)
172
-
173
  with col1:
174
- if st.session_state.step >= 1:
175
- st.success("Step 1: Upload & Process")
176
- else:
177
- st.info("Step 1: Upload & Process")
178
  with col2:
179
- if st.session_state.step >= 2:
180
- st.success("Step 2: Review & Customize")
181
- else:
182
- st.info("Step 2: Review & Customize")
183
  with col3:
184
- if st.session_state.step >= 3:
185
- st.success("Step 3: Export")
186
- else:
187
- st.info("Step 3: Export")
188
-
189
  st.divider()
190
 
191
  if AUDIO_PROCESSOR_CLASS is None:
@@ -211,167 +266,28 @@ def step_1_upload_and_process():
211
  st.subheader("Upload an existing audio file")
212
  uploaded_file = st.file_uploader("Choose an audio file", type=['mp3', 'wav', 'm4a'], help="Supported formats: MP3, WAV, M4A")
213
  if uploaded_file:
214
- st.session_state.audio_file = uploaded_file
215
- st.success(f"File uploaded: {uploaded_file.name}")
216
- st.audio(uploaded_file)
217
  if st.button("🚀 Start AI Processing", type="primary", use_container_width=True):
218
- process_audio()
219
- if st.session_state.audio_file:
220
- if st.button("🔄 Upload Different File"):
221
  reset_session()
222
  st.rerun()
223
 
224
  with record_tab:
225
  st.subheader("Record audio directly from your microphone")
226
- st.info("Use the recorder below. After processing, copy the resulting file path and paste it here.")
227
-
228
- # Show the recorder component
229
- show_recorder_page()
230
 
231
- st.divider()
232
-
233
- # Input for the file path
234
- result_file_path = st.text_input("Paste the result file path here:")
235
-
236
- if st.button("Process Recorded Audio", use_container_width=True):
237
- if result_file_path and os.path.exists(result_file_path):
238
- process_recorded_audio(result_file_path)
239
- else:
240
- st.error("Please provide a valid file path.")
241
-
242
- def show_recorder_page():
243
- """Renders the HTML for the recorder page."""
244
- try:
245
- with open('templates/recorder.html', 'r', encoding='utf-8') as f:
246
- html_string = f.read()
247
- components.html(html_string, height=650, scrolling=True)
248
- except FileNotFoundError:
249
- st.error("Could not find the recorder HTML file. Please ensure 'templates/recorder.html' exists.")
250
-
251
- def process_recorded_audio(file_path):
252
- """Processes the audio data from the recorded file."""
253
- log_to_browser_console("--- INFO: Starting recorded audio processing. ---")
254
- try:
255
- with open(file_path, 'r') as f:
256
- data = json.load(f)
257
- log_to_browser_console(f"--- DEBUG: Loaded data from {file_path}. ---")
258
-
259
- # Convert hex back to bytes
260
- data['audio_bytes'] = bytes.fromhex(data['audio_bytes'])
261
- log_to_browser_console("--- DEBUG: Converted hex audio data to bytes. ---")
262
-
263
- st.session_state.transcription_data = data
264
- st.session_state.edited_text = data['text']
265
- st.session_state.step = 2
266
- st.success("🎉 Recording processed successfully! Please review the results.")
267
- log_to_browser_console("--- SUCCESS: Recording processed and session updated. ---")
268
-
269
- except Exception as e:
270
- st.error(f"An error occurred while processing the recorded audio: {e}")
271
- log_to_browser_console(f"--- FATAL ERROR in process_recorded_audio: {traceback.format_exc()} ---")
272
- finally:
273
- if os.path.exists(file_path):
274
- os.unlink(file_path)
275
- log_to_browser_console(f"--- DEBUG: Cleaned up temp file: {file_path}")
276
-
277
- time.sleep(1)
278
- st.rerun()
279
-
280
- def process_audio():
281
- if not st.session_state.audio_file:
282
- st.error("No audio file found.")
283
- return
284
-
285
- tmp_file_path = None
286
- log_to_browser_console("--- INFO: Starting enhanced audio processing with translation. ---")
287
- try:
288
- with tempfile.NamedTemporaryFile(delete=False, suffix=Path(st.session_state.audio_file.name).suffix) as tmp_file:
289
- st.session_state.audio_file.seek(0)
290
- tmp_file.write(st.session_state.audio_file.getvalue())
291
- tmp_file_path = tmp_file.name
292
-
293
- processor = AUDIO_PROCESSOR_CLASS()
294
-
295
- # Enhanced processing with translation support
296
- if st.session_state.enable_translation:
297
- with st.spinner("🎤 Performing AI processing (Transcription, Timestamps & Translation)..."):
298
- result_data, processor_logs = processor.get_word_timestamps_with_translation(
299
- tmp_file_path,
300
- st.session_state.target_language
301
- )
302
-
303
- log_to_browser_console(processor_logs)
304
-
305
- if not result_data or not result_data.get('original_text'):
306
- st.warning("Could not generate transcription with translation. Check the browser console (F12) for detailed error logs.")
307
- return
308
-
309
- # Store enhanced results
310
- word_timestamps = result_data['word_timestamps']
311
- full_text = result_data['original_text']
312
- translated_text = result_data['translated_text']
313
- translation_success = result_data.get('translation_success', False)
314
- detected_language = result_data.get('language_detected', 'unknown')
315
-
316
- # Show translation results
317
- if translation_success and translated_text != full_text:
318
- st.success(f"🌐 Translation completed successfully! Detected language: {detected_language}")
319
-
320
- col1, col2 = st.columns(2)
321
- with col1:
322
- st.subheader("Original Text")
323
- st.text_area("Original Transcription", value=full_text, height=150, disabled=True)
324
-
325
- with col2:
326
- st.subheader(f"Translation ({st.session_state.target_language.upper()})")
327
- st.text_area("Translated Text", value=translated_text, height=150, disabled=True)
328
-
329
- # Store both versions
330
- st.session_state.translated_text = translated_text
331
- st.session_state.translation_success = True
332
- else:
333
- st.info("Translation was requested but may not have been successful.")
334
- st.session_state.translation_success = False
335
-
336
- else:
337
- # Standard processing without translation
338
- with st.spinner("🎤 Performing AI processing (Transcription & Timestamps)..."):
339
- word_timestamps, processor_logs = processor.get_word_timestamps(tmp_file_path)
340
-
341
- log_to_browser_console(processor_logs)
342
-
343
- if not word_timestamps:
344
- st.warning("Could not generate timestamps. Check the browser console (F12) for detailed error logs.")
345
- return
346
-
347
- full_text = " ".join([d['word'] for d in word_timestamps])
348
- st.session_state.translation_success = False
349
-
350
- st.session_state.audio_file.seek(0)
351
- audio_bytes = st.session_state.audio_file.read()
352
-
353
- st.session_state.transcription_data = {
354
- 'text': full_text, # Use the text we derived
355
- 'word_timestamps': word_timestamps,
356
- 'audio_bytes': audio_bytes,
357
- 'original_suffix': Path(st.session_state.audio_file.name).suffix
358
- }
359
- st.session_state.edited_text = full_text
360
- st.session_state.step = 2
361
- st.success("🎉 AI processing complete! Please review the results.")
362
-
363
- except Exception as e:
364
- st.error("An unexpected error occurred in the main processing flow!")
365
- st.exception(e)
366
- log_to_browser_console(f"--- FATAL ERROR in app.py's process_audio: {traceback.format_exc()} ---")
367
- finally:
368
- if tmp_file_path and os.path.exists(tmp_file_path):
369
- os.unlink(tmp_file_path)
370
-
371
- time.sleep(1)
372
- st.rerun()
373
-
374
 
 
 
 
 
 
375
 
376
  # --- Step 2: Review and Customize ---
377
  def step_2_review_and_customize():
@@ -381,9 +297,22 @@ def step_2_review_and_customize():
381
  if st.button("← Back to Step 1"):
382
  st.session_state.step = 1; st.rerun()
383
  return
 
 
 
 
 
 
 
 
 
 
 
 
384
  col1, col2 = st.columns([3, 2])
385
  with col1:
386
  st.subheader("📝 Text Editor")
 
387
  edited_text = st.text_area("Transcribed Text", value=st.session_state.edited_text, height=300)
388
  st.session_state.edited_text = edited_text
389
  with col2:
@@ -427,26 +356,23 @@ def step_3_export():
427
  if st.button("🔄 Start Over"):
428
  reset_session(); st.rerun()
429
 
430
- # --- MP3 Export Function (with robust stateless logic and logging) ---
431
  def export_mp3():
432
  audio_path_for_export = None
433
  log_to_browser_console("--- INFO: Starting MP3 export process. ---")
434
  try:
435
- with st.spinner("Embedding lyrics..."):
436
  suffix = st.session_state.transcription_data['original_suffix']
437
  audio_bytes = st.session_state.transcription_data['audio_bytes']
438
- log_to_browser_console(f"--- DEBUG: Retrieved {len(audio_bytes) / 1024:.2f} KB of audio data from session_state.")
439
-
440
  with tempfile.NamedTemporaryFile(delete=False, suffix=suffix) as tmp_audio_file:
441
  tmp_audio_file.write(audio_bytes)
442
  audio_path_for_export = tmp_audio_file.name
443
- log_to_browser_console(f"--- DEBUG: Temp file for export created at: {audio_path_for_export}")
444
 
445
  embedder = MP3Embedder()
446
  word_timestamps = st.session_state.transcription_data['word_timestamps']
447
- log_to_browser_console(f"--- DEBUG: Passing {len(word_timestamps)} timestamps to embedder.")
448
 
449
- output_filename = f"synced_{Path(st.session_state.audio_file.name).stem}.mp3"
450
 
451
  output_path, log_messages = embedder.embed_sylt_lyrics(
452
  audio_path_for_export, word_timestamps,
@@ -461,9 +387,6 @@ def export_mp3():
461
  st.audio(audio_bytes_to_download, format='audio/mp3')
462
 
463
  verification = embedder.verify_sylt_embedding(output_path)
464
- log_to_browser_console(f"--- DEBUG: Verification result: {json.dumps(verification)}")
465
- st.json(verification)
466
-
467
  if verification.get('has_sylt'):
468
  st.success(f"Successfully embedded {verification.get('sylt_entries', 0)} words!")
469
  else:
@@ -473,7 +396,6 @@ def export_mp3():
473
  output_filename, "audio/mpeg", use_container_width=True)
474
  else:
475
  st.error("Failed to create the final MP3 file.")
476
- log_to_browser_console(f"--- ERROR: Final output file not found at expected path: {output_path}")
477
 
478
  except Exception as e:
479
  st.error(f"An unexpected error occurred during MP3 export: {e}")
@@ -481,16 +403,17 @@ def export_mp3():
481
  finally:
482
  if audio_path_for_export and os.path.exists(audio_path_for_export):
483
  os.unlink(audio_path_for_export)
484
- log_to_browser_console(f"--- DEBUG: Cleaned up temp export file: {audio_path_for_export}")
485
 
486
  # --- Placeholder and Utility Functions ---
487
  def export_mp4():
 
 
488
  st.info("MP4 export functionality is not yet implemented.")
489
 
490
  def reset_session():
491
  """Resets the session state by clearing specific keys and re-initializing."""
492
  log_to_browser_console("--- INFO: Resetting session state. ---")
493
- keys_to_clear = ['step', 'audio_file', 'transcription_data', 'edited_text', 'video_style']
494
  for key in keys_to_clear:
495
  if key in st.session_state:
496
  del st.session_state[key]
@@ -498,31 +421,6 @@ def reset_session():
498
 
499
  # --- Entry Point ---
500
  if __name__ == "__main__":
501
- # Compatibility Patches for older Streamlit versions
502
- import inspect as _st_inspect
503
- if "type" not in _st_inspect.signature(st.button).parameters:
504
- _orig_button = st.button
505
- def _patched_button(label, *args, **kwargs):
506
- kwargs.pop("type", None); kwargs.pop("use_container_width", None)
507
- return _orig_button(label, *args, **kwargs)
508
- st.button = _patched_button
509
- if hasattr(st, "download_button"):
510
- import inspect as _dl_inspect
511
- _dl_sig = _dl_inspect.signature(st.download_button)
512
- if "use_container_width" not in _dl_sig.parameters:
513
- _orig_download_button = st.download_button
514
- def _patched_download_button(label, data, *args, **kwargs):
515
- kwargs.pop("use_container_width", None)
516
- return _orig_download_button(label, data, *args, **kwargs)
517
- st.download_button = _patched_download_button
518
-
519
- # Setup cleanup handler
520
- import atexit
521
- try:
522
- from integrated_server import stop_all_servers
523
- atexit.register(stop_all_servers)
524
- except ImportError:
525
- pass
526
-
527
- initialize_session_state()
528
- main()
 
1
+ # app.py - Refactored to eliminate recorder_server.py dependency
2
 
3
  import streamlit as st
4
  import os
 
8
  import time
9
  import traceback
10
  import streamlit.components.v1 as components
11
+ from st_audiorec import st_audiorec # Import the new recorder component
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
12
 
13
  # --- Critical Imports and Initial Checks ---
14
  AUDIO_PROCESSOR_CLASS = None
 
21
 
22
  from video_generator import VideoGenerator
23
  from mp3_embedder import MP3Embedder
24
+ from utils import format_timestamp
25
+ from translator import get_translator, UI_TRANSLATIONS
26
+ from dotenv import load_dotenv
27
+
28
+ # --- API Key Check ---
29
+ def check_api_key():
30
+ """Check for Gemini API key and display instructions if not found."""
31
+ load_dotenv()
32
+ if not os.getenv("GEMINI_API_KEY"):
33
+ st.error("🔴 FATAL ERROR: GEMINI_API_KEY is not set!")
34
+ st.info("To fix this, please follow these steps:")
35
+ st.markdown("""
36
+ 1. **Find the file named `.env.example`** in the `syncmaster2` directory.
37
+ 2. **Rename it to `.env`**.
38
+ 3. **Open the `.env` file** with a text editor.
39
+ 4. **Get your free API key** from [Google AI Studio](https://aistudio.google.com/app/apikey).
40
+ 5. **Paste your key** into the file, replacing `"PASTE_YOUR_GEMINI_API_KEY_HERE"`.
41
+ 6. **Save the file and restart the application.**
42
+ """)
43
+ return False
44
+ return True
45
 
46
  # --- Page Configuration ---
47
  st.set_page_config(
 
84
  """Initializes the session state variables if they don't exist."""
85
  if 'step' not in st.session_state:
86
  st.session_state.step = 1
87
+ if 'audio_data' not in st.session_state:
88
+ st.session_state.audio_data = None
89
  if 'language' not in st.session_state:
90
  st.session_state.language = 'en'
91
  if 'enable_translation' not in st.session_state:
 
102
  'highlight_color': '#FFD700', 'background_color': '#000000',
103
  'font_family': 'Arial', 'font_size': 48
104
  }
105
+ if 'new_recording' not in st.session_state:
106
+ st.session_state.new_recording = None
107
+
108
+ # --- Centralized Audio Processing Function ---
109
+ def run_audio_processing(audio_bytes, original_filename="recorded_audio.wav"):
110
+ """
111
+ A single, robust function to handle all audio processing.
112
+ Takes audio bytes as input and returns the processed data.
113
+ """
114
+ if not audio_bytes:
115
+ st.error("No audio data provided to process.")
116
+ return
117
+
118
+ tmp_file_path = None
119
+ log_to_browser_console("--- INFO: Starting unified audio processing. ---")
120
+
121
+ try:
122
+ with tempfile.NamedTemporaryFile(delete=False, suffix=Path(original_filename).suffix) as tmp_file:
123
+ tmp_file.write(audio_bytes)
124
+ tmp_file_path = tmp_file.name
125
+
126
+ processor = AUDIO_PROCESSOR_CLASS()
127
+ result_data = None
128
+ full_text = ""
129
+ word_timestamps = []
130
+
131
+ # Determine which processing path to take
132
+ if st.session_state.enable_translation:
133
+ with st.spinner("⏳ Performing AI Transcription & Translation... please wait."):
134
+ result_data, processor_logs = processor.get_word_timestamps_with_translation(
135
+ tmp_file_path,
136
+ st.session_state.target_language
137
+ )
138
+
139
+ log_to_browser_console(processor_logs)
140
+
141
+ if not result_data or not result_data.get('original_text'):
142
+ st.warning("Could not generate transcription with translation. Check browser console (F12) for logs.")
143
+ return
144
+
145
+ st.session_state.transcription_data = {
146
+ 'text': result_data['original_text'],
147
+ 'translated_text': result_data['translated_text'],
148
+ 'word_timestamps': result_data['word_timestamps'],
149
+ 'audio_bytes': audio_bytes,
150
+ 'original_suffix': Path(original_filename).suffix,
151
+ 'translation_success': result_data.get('translation_success', False),
152
+ 'detected_language': result_data.get('language_detected', 'unknown')
153
+ }
154
+ st.session_state.edited_text = result_data['original_text']
155
+
156
+ else: # Standard processing without translation
157
+ with st.spinner("⏳ Performing AI Transcription... please wait."):
158
+ word_timestamps, processor_logs = processor.get_word_timestamps(tmp_file_path)
159
+
160
+ log_to_browser_console(processor_logs)
161
+
162
+ if not word_timestamps:
163
+ st.warning("Could not generate timestamps. Check browser console (F12) for logs.")
164
+ return
165
+
166
+ full_text = " ".join([d['word'] for d in word_timestamps])
167
+ st.session_state.transcription_data = {
168
+ 'text': full_text,
169
+ 'word_timestamps': word_timestamps,
170
+ 'audio_bytes': audio_bytes,
171
+ 'original_suffix': Path(original_filename).suffix,
172
+ 'translation_success': False
173
+ }
174
+ st.session_state.edited_text = full_text
175
+
176
+ st.session_state.step = 2
177
+ st.success("🎉 AI processing complete! Please review the results.")
178
+
179
+ except Exception as e:
180
+ st.error("An unexpected error occurred during audio processing!")
181
+ st.exception(e)
182
+ log_to_browser_console(f"--- FATAL ERROR in run_audio_processing: {traceback.format_exc()} ---")
183
+ finally:
184
+ if tmp_file_path and os.path.exists(tmp_file_path):
185
+ os.unlink(tmp_file_path)
186
+
187
+ time.sleep(1)
188
+ st.rerun()
189
+
190
 
191
  # --- Main Application Logic ---
192
  def main():
193
  initialize_session_state()
194
 
 
195
  st.markdown("""
196
  <style>
197
  .stSpinner { display: none !important; }
 
201
  </style>
202
  """, unsafe_allow_html=True)
203
 
 
 
 
 
 
204
  with st.sidebar:
205
  st.markdown("## 🌐 Language Settings")
206
  language_options = {'English': 'en', 'العربية': 'ar'}
 
211
  )
212
  st.session_state.language = language_options[selected_lang_display]
213
 
 
214
  st.markdown("## 🔤 Translation Settings")
215
  st.session_state.enable_translation = st.checkbox(
216
  "Enable AI Translation" if st.session_state.language == 'en' else "تفعيل الترجمة بالذكاء الاصطناعي",
 
220
 
221
  if st.session_state.enable_translation:
222
  target_lang_options = {
223
+ 'Arabic (العربية)': 'ar', 'English': 'en', 'French (Français)': 'fr', 'Spanish (Español)': 'es'
 
 
 
224
  }
225
  selected_target = st.selectbox(
226
  "Target Language" if st.session_state.language == 'en' else "اللغة المستهدفة",
227
+ options=list(target_lang_options.keys()), index=0
 
228
  )
229
  st.session_state.target_language = target_lang_options[selected_target]
230
 
 
 
 
 
231
  st.title("🎵 SyncMaster")
232
  if st.session_state.language == 'ar':
233
  st.markdown("### منصة المزامنة الذكية بين الصوت والنص")
 
234
  else:
235
  st.markdown("### The Intelligent Audio-Text Synchronization Platform")
 
236
 
237
  col1, col2, col3 = st.columns(3)
 
238
  with col1:
239
+ st.markdown(f"**{'✅' if st.session_state.step >= 1 else '1️⃣'} Step 1: Upload & Process**")
 
 
 
240
  with col2:
241
+ st.markdown(f"**{'✅' if st.session_state.step >= 2 else '2️⃣'} Step 2: Review & Customize**")
 
 
 
242
  with col3:
243
+ st.markdown(f"**{'✅' if st.session_state.step >= 3 else '3️⃣'} Step 3: Export**")
 
 
 
 
244
  st.divider()
245
 
246
  if AUDIO_PROCESSOR_CLASS is None:
 
266
  st.subheader("Upload an existing audio file")
267
  uploaded_file = st.file_uploader("Choose an audio file", type=['mp3', 'wav', 'm4a'], help="Supported formats: MP3, WAV, M4A")
268
  if uploaded_file:
269
+ st.session_state.audio_data = uploaded_file.getvalue()
270
+ st.success(f"File ready for processing: {uploaded_file.name}")
271
+ st.audio(st.session_state.audio_data)
272
  if st.button("🚀 Start AI Processing", type="primary", use_container_width=True):
273
+ run_audio_processing(st.session_state.audio_data, uploaded_file.name)
274
+ if st.session_state.audio_data:
275
+ if st.button("🔄 Use a Different File"):
276
  reset_session()
277
  st.rerun()
278
 
279
  with record_tab:
280
  st.subheader("Record audio directly from your microphone")
281
+ st.info("Click the microphone icon to start recording. Click the stop icon when you are finished. Processing will begin automatically.")
 
 
 
282
 
283
+ # Use the audio recorder component
284
+ wav_audio_data = st_audiorec()
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
285
 
286
+ # If a new recording is made, process it automatically
287
+ if wav_audio_data is not None and st.session_state.new_recording != wav_audio_data:
288
+ st.session_state.new_recording = wav_audio_data
289
+ # Immediately process the new recording
290
+ run_audio_processing(st.session_state.new_recording, "recorded_audio.wav")
291
 
292
  # --- Step 2: Review and Customize ---
293
  def step_2_review_and_customize():
 
297
  if st.button("← Back to Step 1"):
298
  st.session_state.step = 1; st.rerun()
299
  return
300
+
301
+ # Display translation results if available
302
+ if st.session_state.transcription_data.get('translation_success', False):
303
+ st.success(f"🌐 Translation completed! Detected language: {st.session_state.transcription_data.get('detected_language', 'N/A')}")
304
+ col1, col2 = st.columns(2)
305
+ with col1:
306
+ st.subheader("Original Text")
307
+ st.text_area("Original Transcription", value=st.session_state.transcription_data['text'], height=150, disabled=True)
308
+ with col2:
309
+ st.subheader(f"Translation ({st.session_state.target_language.upper()})")
310
+ st.text_area("Translated Text", value=st.session_state.transcription_data['translated_text'], height=150, disabled=True)
311
+
312
  col1, col2 = st.columns([3, 2])
313
  with col1:
314
  st.subheader("📝 Text Editor")
315
+ st.info("Edit the original transcribed text below. This text will be used for the final export.")
316
  edited_text = st.text_area("Transcribed Text", value=st.session_state.edited_text, height=300)
317
  st.session_state.edited_text = edited_text
318
  with col2:
 
356
  if st.button("🔄 Start Over"):
357
  reset_session(); st.rerun()
358
 
359
+ # --- MP3 Export Function ---
360
  def export_mp3():
361
  audio_path_for_export = None
362
  log_to_browser_console("--- INFO: Starting MP3 export process. ---")
363
  try:
364
+ with st.spinner(" Exporting MP3... Please wait, this may take a moment."):
365
  suffix = st.session_state.transcription_data['original_suffix']
366
  audio_bytes = st.session_state.transcription_data['audio_bytes']
367
+
 
368
  with tempfile.NamedTemporaryFile(delete=False, suffix=suffix) as tmp_audio_file:
369
  tmp_audio_file.write(audio_bytes)
370
  audio_path_for_export = tmp_audio_file.name
 
371
 
372
  embedder = MP3Embedder()
373
  word_timestamps = st.session_state.transcription_data['word_timestamps']
 
374
 
375
+ output_filename = f"synced_{Path(st.session_state.transcription_data.get('original_filename', 'audio')).stem}.mp3"
376
 
377
  output_path, log_messages = embedder.embed_sylt_lyrics(
378
  audio_path_for_export, word_timestamps,
 
387
  st.audio(audio_bytes_to_download, format='audio/mp3')
388
 
389
  verification = embedder.verify_sylt_embedding(output_path)
 
 
 
390
  if verification.get('has_sylt'):
391
  st.success(f"Successfully embedded {verification.get('sylt_entries', 0)} words!")
392
  else:
 
396
  output_filename, "audio/mpeg", use_container_width=True)
397
  else:
398
  st.error("Failed to create the final MP3 file.")
 
399
 
400
  except Exception as e:
401
  st.error(f"An unexpected error occurred during MP3 export: {e}")
 
403
  finally:
404
  if audio_path_for_export and os.path.exists(audio_path_for_export):
405
  os.unlink(audio_path_for_export)
 
406
 
407
  # --- Placeholder and Utility Functions ---
408
  def export_mp4():
409
+ with st.spinner("⏳ Preparing video export..."):
410
+ time.sleep(1) # Simulate work to provide feedback
411
  st.info("MP4 export functionality is not yet implemented.")
412
 
413
  def reset_session():
414
  """Resets the session state by clearing specific keys and re-initializing."""
415
  log_to_browser_console("--- INFO: Resetting session state. ---")
416
+ keys_to_clear = ['step', 'audio_data', 'transcription_data', 'edited_text', 'video_style', 'new_recording']
417
  for key in keys_to_clear:
418
  if key in st.session_state:
419
  del st.session_state[key]
 
421
 
422
  # --- Entry Point ---
423
  if __name__ == "__main__":
424
+ if check_api_key():
425
+ initialize_session_state()
426
+ main()
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
audio_processor.py CHANGED
@@ -17,26 +17,14 @@ import librosa
17
 
18
  import google.generativeai as genai
19
  from translator import AITranslator
 
20
 
21
  class AudioProcessor:
22
  def __init__(self):
23
- self.client = None
24
  self.translator = None
25
  self.init_error = None
26
- self._initialize_gemini()
27
  self._initialize_translator()
28
-
29
- def _initialize_gemini(self):
30
- try:
31
- load_dotenv()
32
- api_key = os.getenv("GEMINI_API_KEY")
33
- if not api_key:
34
- raise ValueError("GEMINI_API_KEY not found in environment variables.")
35
- self.client = genai.Client(api_key=api_key)
36
- except Exception as e:
37
- self.init_error = f"--- FATAL ERROR during Gemini Init: {str(e)} ---"
38
- self.client = None
39
-
40
  def _initialize_translator(self):
41
  """Initialize AI translator for multi-language support"""
42
  try:
@@ -51,31 +39,26 @@ class AudioProcessor:
51
  """
52
  Transcribes audio. Returns (text, error_message).
53
  """
54
- if self.init_error:
55
- return None, self.init_error
56
- if not self.client:
57
- return None, "--- ERROR: Gemini client is not available. ---"
58
 
59
  try:
60
  if not os.path.exists(audio_file_path):
61
  return None, f"--- ERROR: Audio file for transcription not found at: {audio_file_path} ---"
62
 
63
- with open(audio_file_path, 'rb') as f:
64
- audio_bytes = f.read()
65
 
66
- file_ext = os.path.splitext(audio_file_path)[1].lower()
67
- mime_type = {'mp3': 'audio/mpeg', 'wav': 'audio/wav', 'm4a': 'audio/mp4'}.get(file_ext, 'audio/mpeg')
68
 
69
- response = self.client.models.generate_content(
70
- model="gemini-2.5-flash",
71
- contents=[genai.Part.from_bytes(data=audio_bytes, mime_type=mime_type), "Transcribe this audio."]
72
- )
73
-
74
- if response and response.text:
75
  return response.text.strip(), None
76
  else:
77
  return None, "--- WARNING: Gemini returned an empty response for transcription. ---"
78
 
 
 
 
79
  except Exception as e:
80
  error_msg = f"--- FATAL ERROR during transcription: {traceback.format_exc()} ---"
81
  return None, error_msg
@@ -330,4 +313,4 @@ class AudioProcessor:
330
  else:
331
  logs.append("--- WARNING: Translator not available for batch translation ---")
332
 
333
- return results, logs
 
17
 
18
  import google.generativeai as genai
19
  from translator import AITranslator
20
+ from google.api_core import exceptions as google_exceptions
21
 
22
  class AudioProcessor:
23
  def __init__(self):
 
24
  self.translator = None
25
  self.init_error = None
 
26
  self._initialize_translator()
27
+
 
 
 
 
 
 
 
 
 
 
 
28
  def _initialize_translator(self):
29
  """Initialize AI translator for multi-language support"""
30
  try:
 
39
  """
40
  Transcribes audio. Returns (text, error_message).
41
  """
42
+ if not self.translator or not self.translator.model:
43
+ return None, "--- ERROR: Translator model is not available for transcription. ---"
 
 
44
 
45
  try:
46
  if not os.path.exists(audio_file_path):
47
  return None, f"--- ERROR: Audio file for transcription not found at: {audio_file_path} ---"
48
 
49
+ audio_file = genai.upload_file(path=audio_file_path)
 
50
 
51
+ prompt = "Transcribe this audio file accurately. Provide only the text content."
52
+ response = self.translator.model.generate_content([prompt, audio_file])
53
 
54
+ if response and hasattr(response, 'text') and response.text:
 
 
 
 
 
55
  return response.text.strip(), None
56
  else:
57
  return None, "--- WARNING: Gemini returned an empty response for transcription. ---"
58
 
59
+ except google_exceptions.ResourceExhausted:
60
+ error_msg = "--- QUOTA ERROR: You have exceeded the daily free usage limit for the AI service. Please wait for your quota to reset (usually within 24 hours) or upgrade your Google AI plan. ---"
61
+ return None, error_msg
62
  except Exception as e:
63
  error_msg = f"--- FATAL ERROR during transcription: {traceback.format_exc()} ---"
64
  return None, error_msg
 
313
  else:
314
  logs.append("--- WARNING: Translator not available for batch translation ---")
315
 
316
+ return results, logs
integrated_server.py DELETED
@@ -1,235 +0,0 @@
1
- """
2
- Integrated Server Module
3
- يدمج خادم Flask للتسجيل مع تطبيق Streamlit
4
- """
5
-
6
- import threading
7
- import time
8
- import logging
9
- import sys
10
- import subprocess
11
- import os
12
- import socket
13
- from pathlib import Path
14
-
15
- # Import configuration
16
- try:
17
- from app_config import config
18
- except ImportError:
19
- # Fallback configuration
20
- class FallbackConfig:
21
- RECORDER_PORT = 5001
22
- RECORDER_HOST = "localhost"
23
- USE_INTEGRATED_SERVER = True
24
- config = FallbackConfig()
25
-
26
- # Configure logging
27
- logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s')
28
-
29
- def is_port_in_use(port):
30
- """Check if a port is already in use"""
31
- with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
32
- try:
33
- s.bind(('localhost', port))
34
- return False
35
- except socket.error:
36
- return True
37
-
38
- class IntegratedServer:
39
- def __init__(self):
40
- self.flask_process = None
41
- self.flask_thread = None
42
- self.is_running = False
43
- self.port = config.RECORDER_PORT
44
- self.host = config.RECORDER_HOST
45
-
46
- def start_recorder_server_thread(self):
47
- """Start the recorder server in a separate thread"""
48
- try:
49
- # Check if port is already in use
50
- if is_port_in_use(self.port):
51
- logging.info(f"✅ Port {self.port} already in use, assuming recorder server is running")
52
- self.is_running = True
53
- return True
54
-
55
- # Import Flask app from recorder_server
56
- from recorder_server import app
57
-
58
- # Configure Flask to run without debug mode
59
- app.config['DEBUG'] = False
60
- app.config['TESTING'] = False
61
-
62
- # Run Flask app in thread
63
- def run_flask():
64
- try:
65
- app.run(
66
- host=self.host,
67
- port=self.port,
68
- debug=False,
69
- use_reloader=False,
70
- threaded=True
71
- )
72
- except Exception as e:
73
- logging.error(f"Error running Flask server: {e}")
74
-
75
- self.flask_thread = threading.Thread(target=run_flask, daemon=True)
76
- self.flask_thread.start()
77
-
78
- # Wait for server to start with shorter intervals
79
- max_retries = 15
80
- for i in range(max_retries):
81
- time.sleep(0.5) # Reduced from 1 second to 0.5 seconds
82
- if self.is_server_responding():
83
- self.is_running = True
84
- logging.info(f"✅ Recorder server started successfully on port {self.port}")
85
- return True
86
- if i < 5: # Only log first few attempts to reduce noise
87
- logging.info(f"⏳ Waiting for recorder server to start... ({i+1}/{max_retries})")
88
-
89
- logging.warning("⚠️ Recorder server thread started but not responding")
90
- return False
91
-
92
- except Exception as e:
93
- logging.error(f"❌ Failed to start recorder server thread: {e}")
94
- return False
95
-
96
- def start_recorder_server_process(self):
97
- """Start the recorder server as a separate process (fallback method)"""
98
- try:
99
- # Check if port is already in use
100
- if is_port_in_use(self.port):
101
- logging.info(f"✅ Port {self.port} already in use, assuming recorder server is running")
102
- self.is_running = True
103
- return True
104
-
105
- # Start recorder server as subprocess
106
- self.flask_process = subprocess.Popen(
107
- [sys.executable, 'recorder_server.py'],
108
- stdout=subprocess.PIPE,
109
- stderr=subprocess.PIPE,
110
- cwd=os.getcwd()
111
- )
112
-
113
- # Wait for server to start with shorter intervals
114
- max_retries = 20
115
- for i in range(max_retries):
116
- time.sleep(0.5) # Reduced from 1 second to 0.5 seconds
117
-
118
- # Check if process is still running
119
- if self.flask_process.poll() is not None:
120
- logging.error("❌ Recorder server process terminated")
121
- return False
122
-
123
- # Check if server is responding
124
- if self.is_server_responding():
125
- self.is_running = True
126
- logging.info(f"✅ Recorder server started successfully as subprocess on port {self.port}")
127
- return True
128
-
129
- if i < 5: # Only log first few attempts
130
- logging.info(f"⏳ Waiting for recorder server to respond... ({i+1}/{max_retries})")
131
-
132
- logging.warning("⚠️ Recorder server process started but not responding")
133
- return False
134
-
135
- except Exception as e:
136
- logging.error(f"❌ Failed to start recorder server process: {e}")
137
- return False
138
-
139
- def start_recorder_server(self):
140
- """Start recorder server using the best available method"""
141
- logging.info("🚀 Starting integrated recorder server...")
142
-
143
- # If already running, don't start again
144
- if self.is_running and self.is_server_responding():
145
- logging.info("✅ Recorder server already running and responding")
146
- return True
147
-
148
- # Try thread method first (cleaner for integration)
149
- if self.start_recorder_server_thread():
150
- logging.info("✅ Recorder server started using thread method")
151
- return True
152
-
153
- # Fallback to process method
154
- logging.info("⚠️ Thread method failed, trying process method...")
155
- if self.start_recorder_server_process():
156
- logging.info("✅ Recorder server started using process method")
157
- return True
158
-
159
- logging.error("❌ Failed to start recorder server using any method")
160
- return False
161
-
162
- def stop_recorder_server(self):
163
- """Stop the recorder server"""
164
- try:
165
- if self.flask_process and self.flask_process.poll() is None:
166
- self.flask_process.terminate()
167
- try:
168
- self.flask_process.wait(timeout=5)
169
- logging.info("✅ Recorder server process terminated")
170
- except subprocess.TimeoutExpired:
171
- self.flask_process.kill()
172
- logging.info("⚠️ Recorder server process killed")
173
-
174
- self.is_running = False
175
-
176
- except Exception as e:
177
- logging.error(f"❌ Error stopping recorder server: {e}")
178
-
179
- def is_server_responding(self):
180
- """Check if the recorder server is responding"""
181
- try:
182
- import requests
183
- # Very fast check - reduce timeout further
184
- response = requests.get(f'http://{self.host}:{self.port}/record', timeout=0.5)
185
- return response.status_code == 200
186
- except:
187
- return False
188
-
189
- def is_server_running(self):
190
- """Check if the recorder server is running"""
191
- return self.is_running and self.is_server_responding()
192
-
193
- # Global instance
194
- integrated_server = IntegratedServer()
195
-
196
- def ensure_recorder_server():
197
- """Ensure recorder server is running, start if not"""
198
- try:
199
- # Quick check first - if server responds immediately, no delay
200
- if integrated_server.is_server_responding():
201
- logging.info("✅ Recorder server already running")
202
- integrated_server.is_running = True
203
- return True
204
-
205
- # Only start if not detected
206
- logging.info("🔄 Recorder server not detected, starting...")
207
- result = integrated_server.start_recorder_server()
208
- return result
209
- except Exception as e:
210
- logging.error(f"❌ Error in ensure_recorder_server: {e}")
211
- return False
212
-
213
- def stop_all_servers():
214
- """Stop all integrated servers"""
215
- integrated_server.stop_recorder_server()
216
-
217
- # Auto-start when module is imported (but not in main)
218
- if __name__ != "__main__":
219
- # Only auto-start if not running as main module and not already started
220
- try:
221
- if not integrated_server.is_running:
222
- ensure_recorder_server()
223
- except Exception as e:
224
- logging.error(f"❌ Error during auto-start: {e}")
225
-
226
- if __name__ == "__main__":
227
- # Test the integrated server
228
- print("Testing Integrated Server...")
229
- success = ensure_recorder_server()
230
- if success:
231
- print("✅ Server test successful")
232
- time.sleep(2)
233
- stop_all_servers()
234
- else:
235
- print("❌ Server test failed")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
recorder_server.py DELETED
@@ -1,452 +0,0 @@
1
- from flask import Flask, request, jsonify
2
- import os
3
- import tempfile
4
- from pathlib import Path
5
- from audio_processor import AudioProcessor
6
- import logging
7
- import json
8
- import traceback
9
- from flask_cors import CORS
10
- import soundfile as sf
11
- import librosa
12
- from translator import get_translator, get_translation
13
- from database import NotesDatabase
14
- from summarizer import LectureSummarizer
15
-
16
- app = Flask(__name__)
17
- # إعداد CORS بشكل صحيح لتجنب تكرار headers
18
- CORS(app,
19
- origins="*",
20
- methods=['GET', 'POST', 'PUT', 'DELETE', 'OPTIONS'],
21
- allow_headers=['Content-Type', 'Authorization']
22
- )
23
-
24
- # Configure logging
25
- logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s')
26
-
27
- # Initialize database and summarizer
28
- db = NotesDatabase()
29
- summarizer = LectureSummarizer()
30
-
31
- @app.route('/record', methods=['GET', 'POST', 'OPTIONS'])
32
- def record():
33
- if request.method == 'OPTIONS':
34
- # Handle CORS preflight request - flask-cors ستتولى headers
35
- return '', 204
36
-
37
- if request.method == 'GET':
38
- logging.info("--- RECORD ENDPOINT: Health check request received. ---")
39
- return jsonify({'status': 'ok', 'message': 'Recorder server is running.'})
40
-
41
- logging.info("--- RECORD ENDPOINT: POST request received. ---")
42
- logging.info(f"Request Headers: {request.headers}")
43
-
44
- if 'audio_data' not in request.files:
45
- logging.error("--- RECORD ENDPOINT: 'audio_data' not in request files. ---")
46
- return jsonify({'success': False, 'error': 'No audio file found'})
47
-
48
- audio_file = request.files['audio_data']
49
- markers = json.loads(request.form.get('markers', '[]'))
50
- target_language = request.form.get('target_language', 'ar') # Default to Arabic
51
- enable_translation = request.form.get('enable_translation', 'true').lower() == 'true'
52
-
53
- print(f"🔍 RECORD ENDPOINT DEBUG:")
54
- print(f"- enable_translation: {enable_translation}")
55
- print(f"- target_language: {target_language}")
56
-
57
- logging.info(f"--- RECORD ENDPOINT: Received file: {audio_file.filename} ---")
58
- logging.info(f"--- RECORD ENDPOINT: Target language: {target_language} ---")
59
- logging.info(f"--- RECORD ENDPOINT: Translation enabled: {enable_translation} ---")
60
-
61
- if markers:
62
- logging.info(f"--- RECORD ENDPOINT: Received {len(markers)} markers: {markers} ---")
63
-
64
- tmp_file_path = None
65
- try:
66
- with tempfile.NamedTemporaryFile(delete=False, suffix=".wav") as tmp_file:
67
- audio_file.save(tmp_file.name)
68
- tmp_file_path = tmp_file.name
69
- logging.info(f"--- RECORD ENDPOINT: Audio saved to temporary file: {tmp_file_path} ---")
70
-
71
- # --- File Standardization Step ---
72
- try:
73
- logging.info(f"--- RECORD ENDPOINT: Standardizing audio file: {tmp_file_path} ---")
74
- y, sr = librosa.load(tmp_file_path, sr=None, mono=True)
75
- sf.write(tmp_file_path, y, sr)
76
- logging.info(f"--- RECORD ENDPOINT: Audio file successfully standardized. ---")
77
- except Exception as e:
78
- logging.warning(f"--- RECORD ENDPOINT: Could not standardize audio file, proceeding with original. Error: {e} ---")
79
- # --- End Standardization ---
80
-
81
- processor = AudioProcessor()
82
-
83
- # Choose processing method based on translation requirement
84
- if enable_translation:
85
- logging.info("--- RECORD ENDPOINT: Calling enhanced processor with translation ---")
86
- result_data, processor_logs = processor.get_word_timestamps_with_translation(
87
- tmp_file_path, target_language
88
- )
89
-
90
- for log_msg in processor_logs:
91
- logging.info(f"--- AUDIO_PROCESSOR LOG: {log_msg} ---")
92
-
93
- if not result_data or not result_data.get('original_text'):
94
- logging.error("--- RECORD ENDPOINT: Failed to generate transcription with translation. ---")
95
- return jsonify({
96
- 'success': False,
97
- 'error': 'Could not generate transcription with translation.',
98
- 'logs': processor_logs
99
- })
100
-
101
- # Prepare enhanced response with translation
102
- full_text = result_data['original_text']
103
- translated_text = result_data['translated_text']
104
- word_timestamps = result_data['word_timestamps']
105
- translated_timestamps = result_data.get('translated_timestamps', [])
106
-
107
- logging.info(f"--- RECORD ENDPOINT: Original text: '{full_text[:100]}...'")
108
- logging.info(f"--- RECORD ENDPOINT: Translated text: '{translated_text[:100]}...'")
109
-
110
- else:
111
- # Standard processing without translation
112
- logging.info("--- RECORD ENDPOINT: Calling standard audio_processor.get_word_timestamps ---")
113
- word_timestamps, processor_logs = processor.get_word_timestamps(tmp_file_path)
114
-
115
- for log_msg in processor_logs:
116
- logging.info(f"--- AUDIO_PROCESSOR LOG: {log_msg} ---")
117
-
118
- if not word_timestamps:
119
- logging.error("--- RECORD ENDPOINT: Failed to generate timestamps. ---")
120
- return jsonify({
121
- 'success': False,
122
- 'error': 'Could not generate timestamps.',
123
- 'logs': processor_logs
124
- })
125
-
126
- full_text = " ".join([d['word'] for d in word_timestamps])
127
- translated_text = full_text # No translation requested
128
- translated_timestamps = word_timestamps
129
- result_data = {
130
- 'original_text': full_text,
131
- 'translated_text': translated_text,
132
- 'word_timestamps': word_timestamps,
133
- 'translated_timestamps': translated_timestamps,
134
- 'translation_success': not enable_translation
135
- }
136
-
137
- logging.info(f"--- RECORD ENDPOINT: Transcription successful. Text: '{full_text[:100]}...'")
138
-
139
- # Read audio file for storage
140
- with open(tmp_file_path, 'rb') as f:
141
- audio_bytes = f.read()
142
-
143
- # Prepare comprehensive transcription data
144
- transcription_data = {
145
- 'original_text': result_data['original_text'],
146
- 'translated_text': result_data['translated_text'],
147
- 'word_timestamps': result_data['word_timestamps'],
148
- 'translated_timestamps': result_data.get('translated_timestamps', []),
149
- 'audio_bytes': audio_bytes.hex(),
150
- 'original_suffix': '.wav',
151
- 'markers': markers,
152
- 'target_language': target_language,
153
- 'translation_enabled': enable_translation,
154
- 'translation_success': result_data.get('translation_success', False),
155
- 'language_detected': result_data.get('language_detected', 'unknown')
156
- }
157
-
158
- # Save comprehensive data to file
159
- result_file_path = ""
160
- with tempfile.NamedTemporaryFile(mode='w', delete=False, suffix=".json", dir=".", encoding='utf-8') as json_file:
161
- json.dump(transcription_data, json_file, ensure_ascii=False, indent=2)
162
- result_file_path = json_file.name
163
- logging.info(f"--- RECORD ENDPOINT: Enhanced transcription data saved to {result_file_path} ---")
164
-
165
- # Prepare successful response
166
- response_data = {
167
- 'success': True,
168
- 'original_text': result_data['original_text'],
169
- 'translated_text': result_data['translated_text'],
170
- 'file_path': result_file_path,
171
- 'markers': markers,
172
- 'target_language': target_language,
173
- 'translation_enabled': enable_translation,
174
- 'translation_success': result_data.get('translation_success', False),
175
- 'language_detected': result_data.get('language_detected', 'unknown')
176
- }
177
-
178
- print(f"🔍 DEBUG RESPONSE DATA:")
179
- print(f"- success: {response_data['success']}")
180
- print(f"- original_text: {response_data['original_text']}")
181
- print(f"- translated_text: {response_data['translated_text']}")
182
- print(f"- target_language: {response_data['target_language']}")
183
- print(f"- translation_enabled: {response_data['translation_enabled']}")
184
- print(f"- translation_success: {response_data['translation_success']}")
185
- print(f"- language_detected: {response_data['language_detected']}")
186
-
187
- logging.info(f"--- RECORD ENDPOINT: Sending enhanced response with translation support ---")
188
- return jsonify(response_data)
189
-
190
- except Exception as e:
191
- logging.error(f"--- RECORD ENDPOINT FATAL ERROR: {traceback.format_exc()} ---")
192
- return jsonify({'success': False, 'error': str(e), 'trace': traceback.format_exc()}), 500
193
- finally:
194
- if tmp_file_path and os.path.exists(tmp_file_path):
195
- os.unlink(tmp_file_path)
196
-
197
- @app.route('/translate', methods=['POST', 'OPTIONS'])
198
- def translate_text():
199
- """Endpoint for standalone text translation"""
200
- if request.method == 'OPTIONS':
201
- # flask-cors ستتولى معالجة CORS headers
202
- return '', 204
203
-
204
- try:
205
- data = request.get_json()
206
- if not data or 'text' not in data:
207
- return jsonify({'success': False, 'error': 'No text provided for translation'})
208
-
209
- text = data['text']
210
- target_language = data.get('target_language', 'ar')
211
- source_language = data.get('source_language', 'auto')
212
-
213
- logging.info(f"--- TRANSLATE ENDPOINT: Translating text to {target_language} ---")
214
-
215
- translator = get_translator()
216
- if not translator:
217
- return jsonify({'success': False, 'error': 'Translation service not available'})
218
-
219
- translated_text, error = translator.translate_text(text, target_language, source_language)
220
-
221
- if translated_text:
222
- # Detect source language if auto
223
- detected_lang = 'unknown'
224
- if source_language == 'auto':
225
- detected_lang, _ = translator.detect_language(text)
226
-
227
- response_data = {
228
- 'success': True,
229
- 'original_text': text,
230
- 'translated_text': translated_text,
231
- 'source_language': detected_lang if source_language == 'auto' else source_language,
232
- 'target_language': target_language
233
- }
234
- logging.info(f"--- TRANSLATE ENDPOINT: Translation successful ---")
235
- return jsonify(response_data)
236
- else:
237
- logging.error(f"--- TRANSLATE ENDPOINT: Translation failed: {error} ---")
238
- return jsonify({'success': False, 'error': error})
239
-
240
- except Exception as e:
241
- logging.error(f"--- TRANSLATE ENDPOINT ERROR: {traceback.format_exc()} ---")
242
- return jsonify({'success': False, 'error': str(e)}), 500
243
-
244
- @app.route('/languages', methods=['GET'])
245
- def get_supported_languages():
246
- """Get list of supported languages"""
247
- try:
248
- translator = get_translator()
249
- if translator:
250
- languages = translator.get_supported_languages()
251
- return jsonify({'success': True, 'languages': languages})
252
- else:
253
- return jsonify({'success': False, 'error': 'Translation service not available'})
254
- except Exception as e:
255
- return jsonify({'success': False, 'error': str(e)}), 500
256
-
257
- @app.route('/ui-translations/<language>', methods=['GET'])
258
- def get_ui_translations(language):
259
- """Get UI translations for a specific language"""
260
- try:
261
- from translator import UI_TRANSLATIONS
262
- translations = UI_TRANSLATIONS.get(language, UI_TRANSLATIONS['en'])
263
- return jsonify({'success': True, 'translations': translations, 'language': language})
264
- except Exception as e:
265
- return jsonify({'success': False, 'error': str(e)}), 500
266
-
267
- # ======= وظائف الملخص والملاحظات الجديدة =======
268
-
269
- @app.route('/summarize', methods=['POST', 'OPTIONS'])
270
- def summarize_text():
271
- """إنشاء ملخص ذكي من النص"""
272
- logging.info(f"--- SUMMARIZE ENDPOINT: {request.method} request received ---")
273
- logging.info(f"--- SUMMARIZE ENDPOINT: Origin: {request.headers.get('Origin', 'None')} ---")
274
- logging.info(f"--- SUMMARIZE ENDPOINT: Headers: {dict(request.headers)} ---")
275
-
276
- if request.method == 'OPTIONS':
277
- logging.info("--- SUMMARIZE ENDPOINT: Handling OPTIONS preflight request ---")
278
- # لا حاجة لإعداد headers يدوياً، flask-cors ستتولى الأمر
279
- return '', 204
280
-
281
- try:
282
- data = request.get_json()
283
- if not data or 'text' not in data:
284
- return jsonify({'success': False, 'error': 'لم يتم توفير نص للتلخيص'})
285
-
286
- text = data['text']
287
- language = data.get('language', 'ar')
288
- subject = data.get('subject', '')
289
- summary_type = data.get('type', 'full') # full, key_points, questions
290
-
291
- logging.info(f"--- SUMMARIZE ENDPOINT: Creating {summary_type} summary ---")
292
-
293
- if summary_type == 'key_points':
294
- result, error = summarizer.extract_key_points(text, language)
295
- result_key = 'key_points'
296
- elif summary_type == 'questions':
297
- result, error = summarizer.generate_review_questions(text, language)
298
- result_key = 'questions'
299
- elif summary_type == 'study_notes':
300
- result, error = summarizer.generate_study_notes(text, subject, language)
301
- result_key = 'study_notes'
302
- else: # full summary
303
- result, error = summarizer.generate_summary(text, language)
304
- result_key = 'summary'
305
-
306
- if result:
307
- response_data = {
308
- 'success': True,
309
- result_key: result,
310
- 'type': summary_type,
311
- 'language': language
312
- }
313
- logging.info(f"--- SUMMARIZE ENDPOINT: {summary_type} summary created successfully ---")
314
- return jsonify(response_data)
315
- else:
316
- logging.error(f"--- SUMMARIZE ENDPOINT: Failed to create {summary_type}: {error} ---")
317
- return jsonify({'success': False, 'error': error})
318
-
319
- except Exception as e:
320
- logging.error(f"--- SUMMARIZE ENDPOINT ERROR: {traceback.format_exc()} ---")
321
- return jsonify({'success': False, 'error': str(e)}), 500
322
-
323
- @app.route('/test-summarize', methods=['GET'])
324
- def test_summarize():
325
- """اختبار بسيط لـ summarize endpoint"""
326
- return jsonify({
327
- 'success': True,
328
- 'message': 'Summarize endpoint is working!',
329
- 'test_summary': {
330
- 'main_summary': 'This is a test summary',
331
- 'key_points': ['Point 1', 'Point 2', 'Point 3'],
332
- 'review_questions': ['Question 1?', 'Question 2?'],
333
- 'study_notes': 'These are test study notes'
334
- }
335
- })
336
-
337
- @app.route('/notes', methods=['GET', 'POST', 'OPTIONS'])
338
- def handle_notes():
339
- """التعامل مع الملاحظات - عرض وحفظ"""
340
- if request.method == 'OPTIONS':
341
- # flask-cors ستتولى معالجة CORS headers
342
- return '', 204
343
-
344
- if request.method == 'GET':
345
- try:
346
- # البحث أو عرض جميع الملاحظات
347
- search_query = request.args.get('search', '')
348
- subject = request.args.get('subject', '')
349
- limit = int(request.args.get('limit', 20))
350
-
351
- if search_query:
352
- notes = db.search_notes(search_query)
353
- elif subject:
354
- notes = db.get_notes_by_subject(subject)
355
- else:
356
- notes = db.get_all_notes(limit)
357
-
358
- # إحصائيات سريعة
359
- subjects = db.get_subjects()
360
-
361
- response = jsonify({
362
- 'success': True,
363
- 'notes': notes,
364
- 'subjects': subjects,
365
- 'total': len(notes)
366
- })
367
- return response
368
-
369
- except Exception as e:
370
- response = jsonify({'success': False, 'error': str(e)})
371
- return response, 500
372
-
373
- if request.method == 'POST':
374
- try:
375
- data = request.get_json()
376
-
377
- # التحقق من البيانات المطلوبة
378
- if not data or 'original_text' not in data:
379
- return jsonify({'success': False, 'error': 'البيانات غير مكتملة'})
380
-
381
- # حفظ الملاحظة
382
- note_id = db.save_lecture_note(data)
383
-
384
- logging.info(f"--- NOTES ENDPOINT: Note saved with ID {note_id} ---")
385
-
386
- response = jsonify({
387
- 'success': True,
388
- 'note_id': note_id,
389
- 'message': 'تم حفظ الملاحظة بنجاح'
390
- })
391
- return response
392
-
393
- except Exception as e:
394
- logging.error(f"--- NOTES ENDPOINT ERROR: {traceback.format_exc()} ---")
395
- response = jsonify({'success': False, 'error': str(e)})
396
- return response, 500
397
-
398
- @app.route('/notes/<int:note_id>', methods=['GET', 'PUT', 'DELETE', 'OPTIONS'])
399
- def handle_single_note(note_id):
400
- """التعامل مع ملاحظة واحدة - عرض، تحديث، حذف"""
401
- if request.method == 'OPTIONS':
402
- # flask-cors ستتولى معالجة CORS headers
403
- return '', 204
404
-
405
- if request.method == 'GET':
406
- try:
407
- note = db.get_note_by_id(note_id)
408
- if note:
409
- return jsonify({'success': True, 'note': note})
410
- else:
411
- return jsonify({'success': False, 'error': 'الملاحظة غير موجودة'}), 404
412
-
413
- except Exception as e:
414
- return jsonify({'success': False, 'error': str(e)}), 500
415
-
416
- if request.method == 'PUT':
417
- try:
418
- data = request.get_json()
419
- if not data:
420
- return jsonify({'success': False, 'error': 'لم يتم توفير بيانات للتحديث'})
421
-
422
- success = db.update_note(note_id, data)
423
- if success:
424
- return jsonify({'success': True, 'message': 'تم تحديث الملاحظة بنجاح'})
425
- else:
426
- return jsonify({'success': False, 'error': 'فشل في تحديث الملاحظة'}), 400
427
-
428
- except Exception as e:
429
- return jsonify({'success': False, 'error': str(e)}), 500
430
-
431
- if request.method == 'DELETE':
432
- try:
433
- success = db.delete_note(note_id)
434
- if success:
435
- return jsonify({'success': True, 'message': 'تم حذف الملاحظة بنجاح'})
436
- else:
437
- return jsonify({'success': False, 'error': 'فشل في حذف الملاحظة'}), 400
438
-
439
- except Exception as e:
440
- return jsonify({'success': False, 'error': str(e)}), 500
441
-
442
- @app.route('/notes/subjects', methods=['GET'])
443
- def get_subjects():
444
- """استرجاع قائمة المواد الدراسية"""
445
- try:
446
- subjects = db.get_subjects()
447
- return jsonify({'success': True, 'subjects': subjects})
448
- except Exception as e:
449
- return jsonify({'success': False, 'error': str(e)}), 500
450
-
451
- if __name__ == '__main__':
452
- app.run(port=5001)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
summarizer.py CHANGED
@@ -21,7 +21,7 @@ class LectureSummarizer:
21
  Returns:
22
  Tuple of (summary, error_message)
23
  """
24
- if not self.translator or not self.translator.client:
25
  return None, "خدمة الذكاء الاصطناعي غير متوفرة"
26
 
27
  if not text or len(text.strip()) < 50:
@@ -30,12 +30,9 @@ class LectureSummarizer:
30
  try:
31
  prompt = self._create_summary_prompt(text, language)
32
 
33
- response = self.translator.client.models.generate_content(
34
- model="gemini-2.5-flash",
35
- contents=[prompt]
36
- )
37
 
38
- if response and response.text:
39
  summary = response.text.strip()
40
  summary = self._clean_summary_output(summary)
41
  return summary, None
@@ -56,18 +53,15 @@ class LectureSummarizer:
56
  Returns:
57
  Tuple of (key_points_list, error_message)
58
  """
59
- if not self.translator or not self.translator.client:
60
  return None, "خدمة الذكاء الاصطناعي غير متوفرة"
61
 
62
  try:
63
  prompt = self._create_key_points_prompt(text, language)
64
 
65
- response = self.translator.client.models.generate_content(
66
- model="gemini-2.5-flash",
67
- contents=[prompt]
68
- )
69
-
70
- if response and response.text:
71
  key_points_text = response.text.strip()
72
  key_points = self._parse_key_points(key_points_text)
73
  return key_points, None
@@ -130,18 +124,15 @@ class LectureSummarizer:
130
  Returns:
131
  Tuple of (questions_list, error_message)
132
  """
133
- if not self.translator or not self.translator.client:
134
  return None, "خدمة الذكاء الاصطناعي غير متوفرة"
135
 
136
  try:
137
  prompt = self._create_questions_prompt(text, language)
138
 
139
- response = self.translator.client.models.generate_content(
140
- model="gemini-2.5-flash",
141
- contents=[prompt]
142
- )
143
 
144
- if response and response.text:
145
  questions_text = response.text.strip()
146
  questions = self._parse_questions(questions_text)
147
  return questions, None
 
21
  Returns:
22
  Tuple of (summary, error_message)
23
  """
24
+ if not self.translator or not self.translator.model:
25
  return None, "خدمة الذكاء الاصطناعي غير متوفرة"
26
 
27
  if not text or len(text.strip()) < 50:
 
30
  try:
31
  prompt = self._create_summary_prompt(text, language)
32
 
33
+ response = self.translator.model.generate_content(prompt)
 
 
 
34
 
35
+ if response and hasattr(response, 'text') and response.text:
36
  summary = response.text.strip()
37
  summary = self._clean_summary_output(summary)
38
  return summary, None
 
53
  Returns:
54
  Tuple of (key_points_list, error_message)
55
  """
56
+ if not self.translator or not self.translator.model:
57
  return None, "خدمة الذكاء الاصطناعي غير متوفرة"
58
 
59
  try:
60
  prompt = self._create_key_points_prompt(text, language)
61
 
62
+ response = self.translator.model.generate_content(prompt)
63
+
64
+ if response and hasattr(response, 'text') and response.text:
 
 
 
65
  key_points_text = response.text.strip()
66
  key_points = self._parse_key_points(key_points_text)
67
  return key_points, None
 
124
  Returns:
125
  Tuple of (questions_list, error_message)
126
  """
127
+ if not self.translator or not self.translator.model:
128
  return None, "خدمة الذكاء الاصطناعي غير متوفرة"
129
 
130
  try:
131
  prompt = self._create_questions_prompt(text, language)
132
 
133
+ response = self.translator.model.generate_content(prompt)
 
 
 
134
 
135
+ if response and hasattr(response, 'text') and response.text:
136
  questions_text = response.text.strip()
137
  questions = self._parse_questions(questions_text)
138
  return questions, None
templates/recorder.html DELETED
@@ -1,1958 +0,0 @@
1
- <!DOCTYPE html>
2
- <html lang="en" dir="ltr">
3
- <head>
4
- <meta charset="UTF-8">
5
- <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
- <title>Lecture Recorder - SyncMaster</title>
7
- <style>
8
- :root {
9
- --primary-color: #2563eb;
10
- --primary-dark: #1d4ed8;
11
- --success-color: #16a34a;
12
- --danger-color: #dc2626;
13
- --warning-color: #ea580c;
14
- --neutral-100: #f5f5f5;
15
- --neutral-200: #e5e5e5;
16
- --neutral-300: #d4d4d8;
17
- --neutral-600: #525252;
18
- --neutral-700: #404040;
19
- --neutral-800: #262626;
20
- --neutral-900: #171717;
21
- --white: #ffffff;
22
- --text-primary: #0f172a;
23
- --text-secondary: #64748b;
24
- --border-radius: 8px;
25
- --shadow-sm: 0 1px 2px 0 rgb(0 0 0 / 0.05);
26
- --shadow-md: 0 4px 6px -1px rgb(0 0 0 / 0.1);
27
- --shadow-lg: 0 10px 15px -3px rgb(0 0 0 / 0.1);
28
- }
29
-
30
- /* RTL Support for Arabic */
31
- html[dir="rtl"] {
32
- direction: rtl;
33
- }
34
-
35
- html[dir="rtl"] .language-selector {
36
- left: 20px;
37
- right: auto;
38
- }
39
-
40
- * {
41
- box-sizing: border-box;
42
- }
43
-
44
- body {
45
- font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
46
- margin: 0;
47
- padding: 20px;
48
- min-height: 100vh;
49
- background: linear-gradient(135deg, #f1f5f9 0%, #e2e8f0 100%);
50
- color: var(--text-primary);
51
- display: flex;
52
- align-items: center;
53
- justify-content: center;
54
- }
55
-
56
- .recorder-container {
57
- background: var(--white);
58
- border-radius: 16px;
59
- box-shadow: var(--shadow-lg);
60
- width: 100%;
61
- max-width: 640px;
62
- padding: 32px;
63
- text-align: center;
64
- position: relative;
65
- }
66
-
67
- /* Language Selector */
68
- .language-selector {
69
- position: absolute;
70
- top: 20px;
71
- right: 20px;
72
- z-index: 100;
73
- }
74
-
75
- .language-selector select {
76
- padding: 8px 12px;
77
- border: 1px solid var(--neutral-300);
78
- border-radius: var(--border-radius);
79
- background: var(--white);
80
- color: var(--text-primary);
81
- font-size: 14px;
82
- font-weight: 500;
83
- cursor: pointer;
84
- outline: none;
85
- }
86
-
87
- .language-selector select:focus {
88
- border-color: var(--primary-color);
89
- box-shadow: 0 0 0 3px rgb(37 99 235 / 0.1);
90
- }
91
-
92
- /* Translation Controls */
93
- .translation-controls {
94
- margin: 24px 0;
95
- padding: 20px;
96
- background: var(--neutral-100);
97
- border-radius: var(--border-radius);
98
- border: 1px solid var(--neutral-200);
99
- }
100
-
101
- .translation-toggle {
102
- display: flex;
103
- align-items: center;
104
- justify-content: center;
105
- gap: 16px;
106
- margin-bottom: 12px;
107
- flex-wrap: wrap;
108
- }
109
-
110
- .toggle-switch {
111
- position: relative;
112
- width: 48px;
113
- height: 24px;
114
- }
115
-
116
- .toggle-switch input {
117
- opacity: 0;
118
- width: 0;
119
- height: 0;
120
- }
121
-
122
- .slider {
123
- position: absolute;
124
- cursor: pointer;
125
- top: 0;
126
- left: 0;
127
- right: 0;
128
- bottom: 0;
129
- background-color: var(--neutral-300);
130
- border-radius: 24px;
131
- transition: background-color 0.3s ease;
132
- }
133
-
134
- .slider:before {
135
- position: absolute;
136
- content: "";
137
- height: 18px;
138
- width: 18px;
139
- left: 3px;
140
- bottom: 3px;
141
- background-color: var(--white);
142
- border-radius: 50%;
143
- transition: transform 0.3s ease;
144
- box-shadow: var(--shadow-sm);
145
- }
146
-
147
- input:checked + .slider {
148
- background-color: var(--primary-color);
149
- }
150
-
151
- input:checked + .slider:before {
152
- transform: translateX(24px);
153
- }
154
-
155
- .target-language-select {
156
- padding: 8px 12px;
157
- border: 1px solid var(--neutral-300);
158
- border-radius: var(--border-radius);
159
- background: var(--white);
160
- min-width: 140px;
161
- font-size: 14px;
162
- color: var(--text-primary);
163
- }
164
-
165
- .target-language-select:focus {
166
- border-color: var(--primary-color);
167
- outline: none;
168
- box-shadow: 0 0 0 3px rgb(37 99 235 / 0.1);
169
- }
170
-
171
- h2 {
172
- margin-top: 0;
173
- margin-bottom: 8px;
174
- color: var(--text-primary);
175
- font-size: 28px;
176
- font-weight: 700;
177
- letter-spacing: -0.025em;
178
- }
179
-
180
- .subtitle {
181
- color: var(--text-secondary);
182
- margin-bottom: 32px;
183
- font-size: 16px;
184
- line-height: 1.5;
185
- }
186
-
187
- .button-group {
188
- margin-top: 24px;
189
- display: grid;
190
- grid-template-columns: repeat(auto-fit, minmax(140px, 1fr));
191
- gap: 12px;
192
- }
193
-
194
- .button {
195
- color: var(--white);
196
- border: none;
197
- padding: 12px 20px;
198
- font-size: 14px;
199
- font-weight: 600;
200
- border-radius: var(--border-radius);
201
- cursor: pointer;
202
- display: flex;
203
- align-items: center;
204
- justify-content: center;
205
- gap: 8px;
206
- min-height: 44px;
207
- text-transform: none;
208
- letter-spacing: normal;
209
- transition: none;
210
- }
211
-
212
- .button:disabled {
213
- opacity: 0.5;
214
- cursor: not-allowed;
215
- }
216
-
217
- .button:focus {
218
- outline: none;
219
- box-shadow: 0 0 0 3px rgb(0 0 0 / 0.1);
220
- }
221
-
222
- #recordButton {
223
- background: var(--success-color);
224
- }
225
-
226
- #stopButton {
227
- background: var(--danger-color);
228
- }
229
-
230
- #pauseButton {
231
- background: var(--warning-color);
232
- }
233
-
234
- #markButton {
235
- background: var(--primary-color);
236
- }
237
-
238
- #extractButton {
239
- background: var(--primary-color);
240
- }
241
-
242
- #rerecordButton {
243
- background: var(--neutral-600);
244
- }
245
-
246
- .status-display {
247
- margin: 24px 0;
248
- font-size: 16px;
249
- color: var(--text-secondary);
250
- display: flex;
251
- align-items: center;
252
- justify-content: center;
253
- gap: 12px;
254
- font-weight: 500;
255
- }
256
-
257
- .recording-indicator {
258
- width: 10px;
259
- height: 10px;
260
- background-color: var(--danger-color);
261
- border-radius: 50%;
262
- animation: pulse 2s infinite;
263
- }
264
-
265
- @keyframes pulse {
266
- 0%, 100% {
267
- opacity: 1;
268
- transform: scale(1);
269
- }
270
- 50% {
271
- opacity: 0.5;
272
- transform: scale(1.1);
273
- }
274
- }
275
-
276
- #timer {
277
- font-size: 36px;
278
- font-weight: 300;
279
- margin: 20px 0;
280
- color: var(--primary-color);
281
- font-family: 'SF Mono', 'Monaco', 'Inconsolata', 'Roboto Mono', monospace;
282
- letter-spacing: 0.05em;
283
- }
284
-
285
- #audio-level-container {
286
- width: 100%;
287
- height: 6px;
288
- background-color: var(--neutral-200);
289
- border-radius: 3px;
290
- overflow: hidden;
291
- margin: 20px 0;
292
- }
293
-
294
- #audio-level-indicator {
295
- width: 0%;
296
- height: 100%;
297
- background: linear-gradient(90deg, var(--success-color), var(--warning-color), var(--danger-color));
298
- transition: width 0.1s ease;
299
- }
300
-
301
- #review-section, #transcription-container {
302
- margin-top: 32px;
303
- text-align: left;
304
- }
305
-
306
- .section-title {
307
- font-size: 18px;
308
- font-weight: 600;
309
- margin-bottom: 16px;
310
- color: var(--text-primary);
311
- border-bottom: 2px solid var(--primary-color);
312
- padding-bottom: 8px;
313
- }
314
-
315
- textarea {
316
- width: 100%;
317
- border-radius: var(--border-radius);
318
- border: 1px solid var(--neutral-300);
319
- padding: 16px;
320
- font-family: inherit;
321
- resize: vertical;
322
- font-size: 14px;
323
- line-height: 1.6;
324
- color: var(--text-primary);
325
- background: var(--white);
326
- }
327
-
328
- textarea:focus {
329
- outline: none;
330
- border-color: var(--primary-color);
331
- box-shadow: 0 0 0 3px rgb(37 99 235 / 0.1);
332
- }
333
-
334
- .translation-result {
335
- margin-top: 20px;
336
- padding: 20px;
337
- background: var(--neutral-100);
338
- border-radius: var(--border-radius);
339
- border: 1px solid var(--neutral-200);
340
- }
341
-
342
- .translation-header {
343
- font-weight: 600;
344
- color: var(--primary-color);
345
- margin-bottom: 12px;
346
- font-size: 16px;
347
- }
348
-
349
- .language-info {
350
- margin-top: 12px;
351
- font-size: 13px;
352
- color: var(--text-secondary);
353
- padding: 8px 12px;
354
- background: var(--white);
355
- border-radius: var(--border-radius);
356
- border: 1px solid var(--neutral-200);
357
- }
358
-
359
- /* Loading Spinner */
360
- .loading-spinner {
361
- display: inline-block;
362
- width: 20px;
363
- height: 20px;
364
- border: 2px solid var(--neutral-300);
365
- border-top: 2px solid var(--primary-color);
366
- border-radius: 50%;
367
- animation: spin 1s linear infinite;
368
- }
369
-
370
- @keyframes spin {
371
- 0% { transform: rotate(0deg); }
372
- 100% { transform: rotate(360deg); }
373
- }
374
-
375
- /* Messages */
376
- .error-message {
377
- background: #fef2f2;
378
- color: var(--danger-color);
379
- padding: 12px 16px;
380
- border-radius: var(--border-radius);
381
- margin: 12px 0;
382
- border: 1px solid #fecaca;
383
- font-size: 14px;
384
- }
385
-
386
- .success-message {
387
- background: #f0fdf4;
388
- color: var(--success-color);
389
- padding: 12px 16px;
390
- border-radius: var(--border-radius);
391
- margin: 12px 0;
392
- border: 1px solid #bbf7d0;
393
- font-size: 14px;
394
- }
395
-
396
- .translation-status {
397
- font-size: 12px;
398
- padding: 6px 10px;
399
- border-radius: 4px;
400
- margin-top: 8px;
401
- font-weight: 500;
402
- }
403
-
404
- /* Responsive Design */
405
- @media (max-width: 768px) {
406
- body {
407
- padding: 12px;
408
- }
409
-
410
- .recorder-container {
411
- padding: 24px 20px;
412
- }
413
-
414
- .button-group {
415
- grid-template-columns: 1fr;
416
- gap: 8px;
417
- }
418
-
419
- #timer {
420
- font-size: 28px;
421
- }
422
-
423
- h2 {
424
- font-size: 24px;
425
- }
426
-
427
- .translation-toggle {
428
- flex-direction: column;
429
- gap: 12px;
430
- }
431
-
432
- .language-selector {
433
- position: static;
434
- margin-bottom: 16px;
435
- text-align: center;
436
- }
437
- }
438
-
439
- @media (max-width: 480px) {
440
- .recorder-container {
441
- padding: 20px 16px;
442
- }
443
-
444
- h2 {
445
- font-size: 20px;
446
- }
447
-
448
- .subtitle {
449
- font-size: 14px;
450
- }
451
-
452
- #timer {
453
- font-size: 24px;
454
- }
455
- }
456
-
457
- /* Focus indicators for accessibility */
458
- .button:focus,
459
- select:focus,
460
- input:focus {
461
- box-shadow: 0 0 0 3px rgb(37 99 235 / 0.1);
462
- }
463
-
464
- /* Summary and Notes Styles */
465
- .note-item {
466
- background: var(--white);
467
- border: 1px solid var(--neutral-200);
468
- border-radius: var(--border-radius);
469
- padding: 20px;
470
- margin-bottom: 16px;
471
- transition: all 0.2s ease;
472
- }
473
-
474
- .note-item:hover {
475
- border-color: var(--primary-color);
476
- box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
477
- }
478
-
479
- .note-header {
480
- display: flex;
481
- justify-content: space-between;
482
- align-items: flex-start;
483
- margin-bottom: 12px;
484
- }
485
-
486
- .note-header h4 {
487
- margin: 0;
488
- color: var(--text-primary);
489
- font-size: 16px;
490
- font-weight: 600;
491
- }
492
-
493
- .note-date {
494
- font-size: 12px;
495
- color: var(--text-secondary);
496
- white-space: nowrap;
497
- }
498
-
499
- .note-summary {
500
- color: var(--text-secondary);
501
- font-size: 14px;
502
- line-height: 1.5;
503
- margin-bottom: 12px;
504
- }
505
-
506
- .note-tags {
507
- margin-bottom: 12px;
508
- }
509
-
510
- .tag {
511
- display: inline-block;
512
- background: var(--primary-color);
513
- color: white;
514
- padding: 4px 8px;
515
- border-radius: 12px;
516
- font-size: 12px;
517
- margin-right: 6px;
518
- margin-bottom: 4px;
519
- }
520
-
521
- .note-actions {
522
- display: flex;
523
- gap: 8px;
524
- }
525
-
526
- .btn-secondary {
527
- background: var(--neutral-200);
528
- color: var(--text-primary);
529
- border: 1px solid var(--neutral-300);
530
- padding: 6px 12px;
531
- border-radius: 4px;
532
- font-size: 12px;
533
- cursor: pointer;
534
- transition: all 0.2s ease;
535
- }
536
-
537
- .btn-secondary:hover {
538
- background: var(--neutral-300);
539
- }
540
-
541
- .btn-danger {
542
- background: #dc3545;
543
- color: white;
544
- border: 1px solid #dc3545;
545
- padding: 6px 12px;
546
- border-radius: 4px;
547
- font-size: 12px;
548
- cursor: pointer;
549
- transition: all 0.2s ease;
550
- }
551
-
552
- .btn-danger:hover {
553
- background: #c82333;
554
- border-color: #c82333;
555
- }
556
-
557
- /* Modal Styles */
558
- .note-modal {
559
- position: fixed;
560
- top: 0;
561
- left: 0;
562
- width: 100%;
563
- height: 100%;
564
- background: rgba(0, 0, 0, 0.5);
565
- display: flex;
566
- justify-content: center;
567
- align-items: center;
568
- z-index: 1000;
569
- padding: 20px;
570
- }
571
-
572
- .note-modal-content {
573
- background: var(--white);
574
- border-radius: var(--border-radius);
575
- max-width: 800px;
576
- width: 100%;
577
- max-height: 90vh;
578
- overflow-y: auto;
579
- box-shadow: 0 10px 30px rgba(0, 0, 0, 0.3);
580
- }
581
-
582
- .note-modal-header {
583
- display: flex;
584
- justify-content: space-between;
585
- align-items: center;
586
- padding: 20px 24px;
587
- border-bottom: 1px solid var(--neutral-200);
588
- position: sticky;
589
- top: 0;
590
- background: var(--white);
591
- z-index: 1;
592
- }
593
-
594
- .note-modal-header h2 {
595
- margin: 0;
596
- color: var(--text-primary);
597
- font-size: 20px;
598
- }
599
-
600
- .close-btn {
601
- background: none;
602
- border: none;
603
- font-size: 24px;
604
- cursor: pointer;
605
- color: var(--text-secondary);
606
- padding: 0;
607
- width: 30px;
608
- height: 30px;
609
- display: flex;
610
- align-items: center;
611
- justify-content: center;
612
- border-radius: 50%;
613
- transition: all 0.2s ease;
614
- }
615
-
616
- .close-btn:hover {
617
- background: var(--neutral-100);
618
- color: var(--text-primary);
619
- }
620
-
621
- .note-modal-body {
622
- padding: 24px;
623
- }
624
-
625
- .note-section {
626
- margin-bottom: 24px;
627
- }
628
-
629
- .note-section h3 {
630
- color: var(--primary-color);
631
- font-size: 16px;
632
- font-weight: 600;
633
- margin-bottom: 12px;
634
- border-bottom: 1px solid var(--neutral-200);
635
- padding-bottom: 8px;
636
- }
637
-
638
- .note-section p {
639
- color: var(--text-primary);
640
- line-height: 1.6;
641
- margin: 0;
642
- }
643
-
644
- .note-section ul {
645
- margin: 0;
646
- padding-left: 20px;
647
- }
648
-
649
- .note-section li {
650
- color: var(--text-primary);
651
- line-height: 1.6;
652
- margin-bottom: 8px;
653
- }
654
-
655
- /* Search Styles */
656
- .search-container {
657
- margin-bottom: 20px;
658
- }
659
-
660
- .search-container input {
661
- width: 100%;
662
- padding: 12px 16px;
663
- border: 1px solid var(--neutral-300);
664
- border-radius: var(--border-radius);
665
- font-size: 14px;
666
- background: var(--white);
667
- color: var(--text-primary);
668
- }
669
-
670
- .search-container input:focus {
671
- outline: none;
672
- border-color: var(--primary-color);
673
- box-shadow: 0 0 0 3px rgb(37 99 235 / 0.1);
674
- }
675
-
676
- /* Empty state */
677
- .text-center {
678
- text-align: center;
679
- }
680
-
681
- .text-muted {
682
- color: var(--text-secondary);
683
- }
684
-
685
- /* Form Styles for Summary Section */
686
- .form-group {
687
- margin-bottom: 16px;
688
- }
689
-
690
- .form-group label {
691
- display: block;
692
- margin-bottom: 6px;
693
- font-weight: 500;
694
- color: var(--text-primary);
695
- }
696
-
697
- .form-group input,
698
- .form-group textarea {
699
- width: 100%;
700
- padding: 10px 12px;
701
- border: 1px solid var(--neutral-300);
702
- border-radius: 4px;
703
- font-size: 14px;
704
- background: var(--white);
705
- color: var(--text-primary);
706
- }
707
-
708
- .form-group input:focus,
709
- .form-group textarea:focus {
710
- outline: none;
711
- border-color: var(--primary-color);
712
- box-shadow: 0 0 0 3px rgb(37 99 235 / 0.1);
713
- }
714
-
715
- /* Summary Content Styling */
716
- .summary-content {
717
- background: var(--neutral-50);
718
- border: 1px solid var(--neutral-200);
719
- border-radius: var(--border-radius);
720
- padding: 16px;
721
- margin-bottom: 16px;
722
- }
723
-
724
- .summary-content h4 {
725
- color: var(--primary-color);
726
- margin-bottom: 12px;
727
- font-size: 16px;
728
- }
729
-
730
- .summary-content ul {
731
- margin: 0;
732
- padding-left: 20px;
733
- }
734
-
735
- .summary-content li {
736
- margin-bottom: 8px;
737
- line-height: 1.5;
738
- }
739
- </style>
740
- </head>
741
- <body>
742
- <div class="recorder-container">
743
- <!-- Language Selector -->
744
- <div class="language-selector">
745
- <select id="languageSelect" aria-label="Select Language">
746
- <option value="en">English</option>
747
- <option value="ar">العربية</option>
748
- </select>
749
- </div>
750
-
751
- <h2 id="main-title">Lecture Recorder</h2>
752
- <p class="subtitle" id="subtitle">Record and transcribe your lectures with AI-powered translation</p>
753
-
754
- <!-- Translation Controls -->
755
- <div class="translation-controls">
756
- <div class="translation-toggle">
757
- <label for="translationToggle" id="translation-label">Enable Translation:</label>
758
- <label class="toggle-switch">
759
- <input type="checkbox" id="translationToggle" checked>
760
- <span class="slider"></span>
761
- </label>
762
- <select id="targetLanguage" class="target-language-select">
763
- <option value="ar">العربية (Arabic)</option>
764
- <option value="en">English</option>
765
- <option value="fr">Français</option>
766
- <option value="es">Español</option>
767
- </select>
768
- </div>
769
- </div>
770
-
771
- <div id="recording-controls">
772
- <div class="status-display">
773
- <div id="recording-indicator" class="recording-indicator" style="display: none;"></div>
774
- <span id="status">Ready to Record</span>
775
- </div>
776
- <div id="timer">00:00:00</div>
777
- <div id="audio-level-container">
778
- <div id="audio-level-indicator"></div>
779
- </div>
780
- <div class="button-group">
781
- <button id="recordButton" class="button" aria-label="Start Recording">
782
- <span>🎙️</span>
783
- <span id="record-text">Start</span>
784
- </button>
785
- <button id="pauseButton" class="button" disabled aria-label="Pause Recording">
786
- <span>⏸️</span>
787
- <span id="pause-text">Pause</span>
788
- </button>
789
- <button id="stopButton" class="button" disabled aria-label="Stop Recording">
790
- <span>⏹️</span>
791
- <span id="stop-text">Stop</span>
792
- </button>
793
- <button id="markButton" class="button" disabled aria-label="Mark Important Point">
794
- <span>📌</span>
795
- <span id="mark-text">Mark Important</span>
796
- </button>
797
- </div>
798
- </div>
799
-
800
- <div id="review-section" style="display: none;">
801
- <h3 class="section-title" id="review-title">Review Recording</h3>
802
- <audio id="audio-playback" controls style="width: 100%;"></audio>
803
- <div class="button-group">
804
- <button id="extractButton" class="button" aria-label="Extract Text">
805
- <span>📄</span>
806
- <span id="extract-text">Extract Text</span>
807
- </button>
808
- <button id="rerecordButton" class="button" aria-label="Record Again">
809
- <span>🔄</span>
810
- <span id="rerecord-text">Re-record</span>
811
- </button>
812
- </div>
813
- </div>
814
-
815
- <div id="transcription-container" style="display: none;">
816
- <h3 class="section-title" id="transcription-title">Processing Result</h3>
817
-
818
- <!-- Original Transcription -->
819
- <div class="transcription-section">
820
- <h4 id="original-text-label">Original Text:</h4>
821
- <textarea id="original-transcription-box" rows="6" readonly></textarea>
822
- </div>
823
-
824
- <!-- Translation Result -->
825
- <div id="translation-section" class="translation-result" style="display: none;">
826
- <h4 class="translation-header" id="translated-text-label">Arabic Translation:</h4>
827
- <textarea id="translated-transcription-box" rows="6" readonly></textarea>
828
-
829
- <!-- Language Info -->
830
- <div class="language-info" style="margin-top: 10px; font-size: 0.9em; color: #6c757d;">
831
- <span id="detected-language">Detected Language: Unknown</span> →
832
- <span id="target-language-info">Arabic</span>
833
- </div>
834
- </div>
835
-
836
- <!-- Markers Section -->
837
- <div id="markers-section" style="display: none; margin-top: 20px;">
838
- <h4 id="markers-label">Important Markers:</h4>
839
- <div id="markers-list" style="background: #f8f9fa; padding: 10px; border-radius: 6px;"></div>
840
- </div>
841
-
842
- <!-- Summary Action Button (Always Visible) -->
843
- <div id="summary-action-section" style="display: none; margin-top: 24px; text-align: center;">
844
- <button id="generateSummaryBtn" class="button" style="padding: 12px 24px; font-size: 16px; min-height: auto; background: var(--primary-color); color: white;">
845
- <span>🤖</span>
846
- <span>Generate Smart Lecture Summary</span>
847
- </button>
848
- </div>
849
-
850
- <!-- Smart Summary Section -->
851
- <div id="summary-section" style="display: none; margin-top: 24px;">
852
- <div class="section-title">
853
- <span>📝 Smart Summary</span>
854
- </div>
855
-
856
- <!-- Summary Content -->
857
- <div id="summary-content" style="background: var(--neutral-100); border-radius: var(--border-radius); padding: 16px; margin-top: 12px;">
858
-
859
- <!-- Summary Text -->
860
- <div id="summary-text-section" style="display: none;">
861
- <h5 style="color: var(--primary-color); margin: 0 0 8px 0; font-size: 14px;">📋 Summary:</h5>
862
- <div id="summary-text" style="background: var(--white); padding: 12px; border-radius: 6px; border: 1px solid var(--neutral-200); line-height: 1.6; font-size: 14px;"></div>
863
- </div>
864
-
865
- <!-- Key Points -->
866
- <div id="key-points-section" style="display: none; margin-top: 16px;">
867
- <h5 style="color: var(--primary-color); margin: 0 0 8px 0; font-size: 14px;">🎯 Key Points:</h5>
868
- <ul id="key-points-list" style="background: var(--white); padding: 12px; border-radius: 6px; border: 1px solid var(--neutral-200); margin: 0;"></ul>
869
- </div>
870
-
871
- <!-- Review Questions -->
872
- <div id="questions-section" style="display: none; margin-top: 16px;">
873
- <h5 style="color: var(--primary-color); margin: 0 0 8px 0; font-size: 14px;">❓ Review Questions:</h5>
874
- <ol id="questions-list" style="background: var(--white); padding: 12px; border-radius: 6px; border: 1px solid var(--neutral-200); margin: 0;"></ol>
875
- </div>
876
-
877
- <!-- Save Note Section -->
878
- <div id="save-note-section" style="display: none; margin-top: 16px; padding-top: 16px; border-top: 1px solid var(--neutral-200);">
879
- <h5 style="color: var(--primary-color); margin: 0 0 8px 0; font-size: 14px;">💾 Save to Notes:</h5>
880
- <div style="display: grid; grid-template-columns: 1fr 1fr; gap: 8px; margin-bottom: 12px;">
881
- <input type="text" id="noteTitle" placeholder="Note title..." style="padding: 8px; border: 1px solid var(--neutral-300); border-radius: 4px; font-size: 14px;">
882
- <input type="text" id="noteTags" placeholder="Tags (comma separated)..." style="padding: 8px; border: 1px solid var(--neutral-300); border-radius: 4px; font-size: 14px;">
883
- </div>
884
- <button id="saveNoteBtn" class="button" style="width: 100%; padding: 8px; font-size: 14px; min-height: auto;">
885
- <span>💾</span>
886
- <span>Save Note</span>
887
- </button>
888
- </div>
889
- </div>
890
- </div>
891
-
892
- <!-- Notes Library Section -->
893
- <div id="notes-library-section" style="display: none; margin-top: 24px;">
894
- <div class="section-title" style="display: flex; align-items: center; justify-content: space-between;">
895
- <span>📚 مكتبة المذكرات</span>
896
- <div style="display: flex; gap: 8px;">
897
- <button id="refreshNotesBtn" class="button" style="padding: 6px 12px; font-size: 12px; min-height: auto;">
898
- <span>🔄</span>
899
- <span>تحديث</span>
900
- </button>
901
- <button id="toggleNotesBtn" class="button" style="padding: 6px 12px; font-size: 12px; min-height: auto;">
902
- <span>📖</span>
903
- <span>عرض المذكرات</span>
904
- </button>
905
- </div>
906
- </div>
907
-
908
- <!-- Search and Filter -->
909
- <div id="notes-controls" style="display: none; margin-top: 12px; padding: 16px; background: var(--neutral-100); border-radius: var(--border-radius);">
910
- <div style="display: grid; grid-template-columns: 1fr auto; gap: 8px; margin-bottom: 12px;">
911
- <input type="text" id="notes-search" placeholder="البحث في المذكرات..." style="padding: 8px; border: 1px solid var(--neutral-300); border-radius: 4px; font-size: 14px;">
912
- <select id="subject-filter" style="padding: 8px; border: 1px solid var(--neutral-300); border-radius: 4px; font-size: 14px; min-width: 120px;">
913
- <option value="">جميع المواد</option>
914
- </select>
915
- </div>
916
- <button id="searchNotesBtn" class="button" style="padding: 6px 16px; font-size: 12px; min-height: auto;">
917
- <span>🔍</span>
918
- <span>بحث</span>
919
- </button>
920
- </div>
921
-
922
- <!-- Notes List -->
923
- <div id="notes-list" style="display: none; margin-top: 12px; max-height: 400px; overflow-y: auto;">
924
- <!-- Notes will be loaded here -->
925
- </div>
926
- </div>
927
- </div>
928
-
929
- <!-- Loading Indicator -->
930
- <div id="loading-indicator" style="display: none; margin: 20px 0;">
931
- <div class="loading-spinner"></div>
932
- <span id="loading-text" style="margin-left: 10px;">Processing...</span>
933
- </div>
934
-
935
- <!-- Error/Success Messages -->
936
- <div id="message-container"></div>
937
- </div>
938
-
939
- <script>
940
- // --- Global Variables ---
941
- let currentLanguage = 'en';
942
- let translations = {};
943
-
944
- // --- DOM Elements ---
945
- const elements = {
946
- recordButton: document.getElementById('recordButton'),
947
- stopButton: document.getElementById('stopButton'),
948
- pauseButton: document.getElementById('pauseButton'),
949
- markButton: document.getElementById('markButton'),
950
- extractButton: document.getElementById('extractButton'),
951
- rerecordButton: document.getElementById('rerecordButton'),
952
- status: document.getElementById('status'),
953
- timer: document.getElementById('timer'),
954
- recordingIndicator: document.getElementById('recording-indicator'),
955
- audioLevelIndicator: document.getElementById('audio-level-indicator'),
956
- reviewSection: document.getElementById('review-section'),
957
- audioPlayback: document.getElementById('audio-playback'),
958
- transcriptionContainer: document.getElementById('transcription-container'),
959
- originalTranscriptionBox: document.getElementById('original-transcription-box'),
960
- translatedTranscriptionBox: document.getElementById('translated-transcription-box'),
961
- recordingControls: document.getElementById('recording-controls'),
962
- languageSelect: document.getElementById('languageSelect'),
963
- translationToggle: document.getElementById('translationToggle'),
964
- targetLanguage: document.getElementById('targetLanguage'),
965
- translationSection: document.getElementById('translation-section'),
966
- markersSection: document.getElementById('markers-section'),
967
- markersList: document.getElementById('markers-list'),
968
- loadingIndicator: document.getElementById('loading-indicator'),
969
- messageContainer: document.getElementById('message-container')
970
- };
971
-
972
- // --- State ---
973
- let state = {
974
- mediaRecorder: null,
975
- audioChunks: [],
976
- audioBlob: null,
977
- timerInterval: null,
978
- seconds: 0,
979
- microphoneStream: null,
980
- markers: [],
981
- currentResult: null
982
- };
983
-
984
- // --- Event Listeners ---
985
- elements.recordButton.addEventListener('click', startRecording);
986
- elements.stopButton.addEventListener('click', stopRecording);
987
- elements.pauseButton.addEventListener('click', togglePause);
988
- elements.markButton.addEventListener('click', markTimestamp);
989
- elements.extractButton.addEventListener('click', processAudio);
990
- elements.rerecordButton.addEventListener('click', resetRecorder);
991
- elements.languageSelect.addEventListener('change', changeLanguage);
992
- elements.translationToggle.addEventListener('change', toggleTranslationUI);
993
- elements.targetLanguage.addEventListener('change', updateTargetLanguageInfo);
994
-
995
- // --- Translation System ---
996
- async function loadTranslations(language) {
997
- try {
998
- const response = await fetch(`http://localhost:5001/ui-translations/${language}`);
999
- if (response.ok) {
1000
- const data = await response.json();
1001
- if (data.success) {
1002
- translations = data.translations;
1003
- applyTranslations();
1004
-
1005
- // Update HTML direction for RTL languages
1006
- if (language === 'ar') {
1007
- document.documentElement.setAttribute('dir', 'rtl');
1008
- document.documentElement.setAttribute('lang', 'ar');
1009
- } else {
1010
- document.documentElement.setAttribute('dir', 'ltr');
1011
- document.documentElement.setAttribute('lang', language);
1012
- }
1013
- }
1014
- }
1015
- } catch (error) {
1016
- console.error('Failed to load translations:', error);
1017
- // Fallback to default English
1018
- translations = getDefaultTranslations();
1019
- applyTranslations();
1020
- }
1021
- }
1022
-
1023
- function getDefaultTranslations() {
1024
- return {
1025
- 'start_recording': 'Start Recording',
1026
- 'stop_recording': 'Stop Recording',
1027
- 'pause_recording': 'Pause Recording',
1028
- 'resume_recording': 'Resume Recording',
1029
- 'mark_important': 'Mark Important',
1030
- 'extract_text': 'Extract Text',
1031
- 'rerecord': 'Re-record',
1032
- 'processing': 'Processing...',
1033
- 'ready_to_record': 'Ready to Record',
1034
- 'recording': 'Recording...',
1035
- 'paused': 'Paused',
1036
- 'review_recording': 'Review your recording',
1037
- 'processing_complete': 'Processing Complete!',
1038
- 'microphone_permission': 'Microphone permission denied.',
1039
- 'browser_not_supported': 'Your browser does not support audio recording.',
1040
- 'transcription': 'Transcription',
1041
- 'translation': 'Translation',
1042
- 'markers': 'Important Markers',
1043
- 'error_occurred': 'An error occurred',
1044
- 'success': 'Success'
1045
- };
1046
- }
1047
-
1048
- function applyTranslations() {
1049
- // Update text elements
1050
- const textMappings = {
1051
- 'main-title': 'Lecture Recorder',
1052
- 'subtitle': 'Record and transcribe your lectures with AI-powered translation',
1053
- 'translation-label': 'Enable Translation:',
1054
- 'record-text': translations.start_recording || 'Start',
1055
- 'pause-text': translations.pause_recording || 'Pause',
1056
- 'stop-text': translations.stop_recording || 'Stop',
1057
- 'mark-text': translations.mark_important || 'Mark Important',
1058
- 'extract-text': translations.extract_text || 'Extract Text',
1059
- 'rerecord-text': translations.rerecord || 'Re-record',
1060
- 'review-title': translations.review_recording || 'Review Recording',
1061
- 'transcription-title': translations.processing_complete || 'Processing Result',
1062
- 'original-text-label': 'Original Text:',
1063
- 'translated-text-label': getTargetLanguageName() + ' Translation:',
1064
- 'markers-label': translations.markers || 'Important Markers'
1065
- };
1066
-
1067
- Object.entries(textMappings).forEach(([id, text]) => {
1068
- const element = document.getElementById(id);
1069
- if (element) {
1070
- element.textContent = text;
1071
- }
1072
- });
1073
- }
1074
-
1075
- function getTargetLanguageName() {
1076
- const langMap = {
1077
- 'ar': 'Arabic (العربية)',
1078
- 'en': 'English',
1079
- 'fr': 'French (Français)',
1080
- 'es': 'Spanish (Español)'
1081
- };
1082
- return langMap[elements.targetLanguage.value] || 'Translation';
1083
- }
1084
-
1085
- async function changeLanguage() {
1086
- currentLanguage = elements.languageSelect.value;
1087
- await loadTranslations(currentLanguage);
1088
- }
1089
-
1090
- function toggleTranslationUI() {
1091
- const isEnabled = elements.translationToggle.checked;
1092
- elements.targetLanguage.disabled = !isEnabled;
1093
- updateTargetLanguageInfo();
1094
-
1095
- // Show/hide translation section preview
1096
- updateTranslationSectionVisibility();
1097
- }
1098
-
1099
- function updateTranslationSectionVisibility() {
1100
- // Always prepare translation section when translation is enabled
1101
- if (elements.translationToggle.checked) {
1102
- // Make sure translation section is ready to be shown
1103
- const translatedLabelElement = document.getElementById('translated-text-label');
1104
- if (translatedLabelElement) {
1105
- translatedLabelElement.textContent = getTargetLanguageName() + ' Translation:';
1106
- }
1107
- }
1108
- }
1109
-
1110
- function updateTargetLanguageInfo() {
1111
- const targetLangElement = document.getElementById('target-language-info');
1112
- if (targetLangElement) {
1113
- targetLangElement.textContent = getTargetLanguageName();
1114
- }
1115
-
1116
- const translatedLabelElement = document.getElementById('translated-text-label');
1117
- if (translatedLabelElement) {
1118
- translatedLabelElement.textContent = getTargetLanguageName() + ' Translation:';
1119
- }
1120
- }
1121
- // --- Core Recording Functions ---
1122
- async function startRecording() {
1123
- if (!navigator.mediaDevices || !navigator.mediaDevices.getUserMedia) {
1124
- showMessage(translations.browser_not_supported || 'Your browser does not support audio recording.', 'error');
1125
- return;
1126
- }
1127
- try {
1128
- state.microphoneStream = await navigator.mediaDevices.getUserMedia({ audio: true });
1129
- updateUIMode('recording');
1130
-
1131
- state.audioChunks = [];
1132
- state.markers = [];
1133
- state.mediaRecorder = new MediaRecorder(state.microphoneStream);
1134
- state.mediaRecorder.ondataavailable = event => state.audioChunks.push(event.data);
1135
- state.mediaRecorder.onstop = handleRecordingStop;
1136
-
1137
- state.mediaRecorder.start();
1138
- startTimer();
1139
- visualizeAudio(state.microphoneStream);
1140
- } catch (error) {
1141
- showMessage(translations.microphone_permission || 'Microphone permission denied.', 'error');
1142
- console.error('Error accessing microphone:', error);
1143
- }
1144
- }
1145
-
1146
- function stopRecording() {
1147
- if (state.mediaRecorder && state.mediaRecorder.state !== 'inactive') {
1148
- state.mediaRecorder.stop();
1149
- }
1150
- }
1151
-
1152
- function handleRecordingStop() {
1153
- state.audioBlob = new Blob(state.audioChunks, { type: 'audio/wav' });
1154
- elements.audioPlayback.src = URL.createObjectURL(state.audioBlob);
1155
- state.microphoneStream.getTracks().forEach(track => track.stop());
1156
- clearInterval(state.timerInterval);
1157
- updateUIMode('review');
1158
- }
1159
-
1160
- function togglePause() {
1161
- if (!state.mediaRecorder) return;
1162
- if (state.mediaRecorder.state === 'recording') {
1163
- state.mediaRecorder.pause();
1164
- updateUIMode('paused');
1165
- } else if (state.mediaRecorder.state === 'paused') {
1166
- state.mediaRecorder.resume();
1167
- updateUIMode('recording');
1168
- }
1169
- }
1170
-
1171
- function markTimestamp() {
1172
- state.markers.push(state.seconds);
1173
- const markCount = state.markers.length;
1174
- elements.markButton.innerHTML = `<span>📌</span><span>Marked (${markCount})</span>`;
1175
- setTimeout(() => {
1176
- elements.markButton.innerHTML = `<span>📌</span><span>${translations.mark_important || 'Mark Important'}</span>`;
1177
- }, 1000);
1178
- }
1179
-
1180
- async function processAudio() {
1181
- if (!state.audioBlob) return;
1182
- updateUIMode('processing');
1183
-
1184
- const formData = new FormData();
1185
- formData.append('audio_data', state.audioBlob, 'recording.wav');
1186
- formData.append('markers', JSON.stringify(state.markers));
1187
- formData.append('enable_translation', elements.translationToggle.checked.toString());
1188
- formData.append('target_language', elements.targetLanguage.value);
1189
-
1190
- try {
1191
- // Try multiple server URLs in case of port conflicts
1192
- const serverUrls = [
1193
- 'http://localhost:5001/record',
1194
- 'http://127.0.0.1:5001/record',
1195
- 'http://localhost:5000/record'
1196
- ];
1197
-
1198
- let response = null;
1199
- let lastError = null;
1200
-
1201
- for (const url of serverUrls) {
1202
- try {
1203
- console.log(`Trying server URL: ${url}`);
1204
- response = await fetch(url, {
1205
- method: 'POST',
1206
- body: formData,
1207
- timeout: 30000 // 30 seconds timeout
1208
- });
1209
-
1210
- if (response.ok) {
1211
- console.log(`Successfully connected to: ${url}`);
1212
- break;
1213
- }
1214
- } catch (error) {
1215
- console.log(`Failed to connect to ${url}:`, error.message);
1216
- lastError = error;
1217
- continue;
1218
- }
1219
- }
1220
-
1221
- if (!response || !response.ok) {
1222
- throw new Error(`Server connection failed. Last error: ${lastError?.message || 'Unknown error'}`);
1223
- }
1224
-
1225
- const result = await response.json();
1226
-
1227
- // Debug logging for translation analysis
1228
- console.log('🔍 SERVER RESPONSE ANALYSIS:');
1229
- console.log('- Success:', result.success);
1230
- console.log('- Translation enabled:', result.translation_enabled);
1231
- console.log('- Translation success:', result.translation_success);
1232
- console.log('- Original text:', result.original_text?.substring(0, 50) + '...');
1233
- console.log('- Translated text:', result.translated_text?.substring(0, 50) + '...');
1234
- console.log('- Target language:', result.target_language);
1235
- console.log('- Detected language:', result.language_detected);
1236
- console.log('- Are texts different?:', result.translated_text !== result.original_text);
1237
-
1238
- if (result.success) {
1239
- state.currentResult = result;
1240
- console.log('💾 STORED RESULT IN STATE:');
1241
- console.log('- Complete result object:', result);
1242
- console.log('- Available keys:', Object.keys(result));
1243
- console.log('- state.currentResult set to:', state.currentResult);
1244
- displayResults(result);
1245
- updateUIMode('result');
1246
- showMessage(translations.success || 'Success!', 'success');
1247
- } else {
1248
- throw new Error(result.error || 'Processing failed');
1249
- }
1250
- } catch (error) {
1251
- updateUIMode('review');
1252
- const errorMessage = `${translations.error_occurred || 'Error'}: ${error.message}`;
1253
- showMessage(errorMessage, 'error');
1254
- console.error("Processing Error:", error);
1255
-
1256
- // Show detailed error for debugging
1257
- const detailedError = document.createElement('div');
1258
- detailedError.className = 'error-message';
1259
- detailedError.innerHTML = `
1260
- <strong>Connection Error Details:</strong><br>
1261
- • Make sure the recorder server is running on port 5001<br>
1262
- • Try running: <code>python recorder_server.py</code><br>
1263
- • Check if port 5001 is available<br>
1264
- • Error: ${error.message}
1265
- `;
1266
- elements.messageContainer.appendChild(detailedError);
1267
- }
1268
- }
1269
-
1270
- function displayResults(result) {
1271
- console.log('🎯 DISPLAY RESULTS FUNCTION:');
1272
- console.log('- Translation enabled:', result.translation_enabled);
1273
- console.log('- Has translated text:', !!result.translated_text);
1274
- console.log('- Translation section element:', !!elements.translationSection);
1275
-
1276
- // Display original transcription
1277
- elements.originalTranscriptionBox.value = result.original_text || result.text || '';
1278
-
1279
- // Always show translation section if translation toggle is enabled or we have translated text
1280
- const shouldShowTranslation = (
1281
- elements.translationToggle.checked ||
1282
- (result.translated_text && result.translated_text.trim())
1283
- );
1284
-
1285
- console.log('- Should show translation:', shouldShowTranslation);
1286
- console.log('- Translation toggle checked:', elements.translationToggle.checked);
1287
- console.log('- Has translated text content:', !!(result.translated_text && result.translated_text.trim()));
1288
-
1289
- if (shouldShowTranslation) {
1290
- console.log('✅ Showing translation section');
1291
-
1292
- // Set translated text or show placeholder if no translation
1293
- if (result.translated_text && result.translated_text.trim()) {
1294
- elements.translatedTranscriptionBox.value = result.translated_text;
1295
- } else {
1296
- elements.translatedTranscriptionBox.value = result.original_text || 'Translation in progress...';
1297
- }
1298
-
1299
- elements.translationSection.style.display = 'block';
1300
-
1301
- // Update language info
1302
- const detectedLangElement = document.getElementById('detected-language');
1303
- const targetLangElement = document.getElementById('target-language-info');
1304
- if (detectedLangElement) {
1305
- const detectedLang = result.language_detected || 'Unknown';
1306
- detectedLangElement.textContent = `Detected Language: ${detectedLang}`;
1307
- }
1308
- if (targetLangElement) {
1309
- targetLangElement.textContent = getTargetLanguageName();
1310
- }
1311
-
1312
- // Add status indicator for translation quality
1313
- const translationStatus = document.createElement('div');
1314
- translationStatus.style.cssText = 'margin-top: 5px; font-size: 0.8em; padding: 3px 8px; border-radius: 4px;';
1315
-
1316
- if (result.translation_success === true) {
1317
- translationStatus.style.backgroundColor = '#d4edda';
1318
- translationStatus.style.color = '#155724';
1319
- translationStatus.textContent = '✓ Translation successful';
1320
- } else if (!result.translated_text || result.translated_text === result.original_text) {
1321
- translationStatus.style.backgroundColor = '#fff3cd';
1322
- translationStatus.style.color = '#856404';
1323
- translationStatus.textContent = '⚠ Translation failed - showing original text';
1324
- } else {
1325
- translationStatus.style.backgroundColor = '#cce7ff';
1326
- translationStatus.style.color = '#004085';
1327
- translationStatus.textContent = '🔄 Translation completed';
1328
- }
1329
-
1330
- // Remove existing status if any
1331
- const existingStatus = elements.translationSection.querySelector('.translation-status');
1332
- if (existingStatus) {
1333
- existingStatus.remove();
1334
- }
1335
-
1336
- // Add status indicator
1337
- translationStatus.className = 'translation-status';
1338
- elements.translationSection.appendChild(translationStatus);
1339
-
1340
- } else {
1341
- console.log('❌ Hiding translation section - conditions not met');
1342
- elements.translationSection.style.display = 'none';
1343
- }
1344
-
1345
- // Display markers if available
1346
- if (result.markers && result.markers.length > 0) {
1347
- displayMarkers(result.markers);
1348
- elements.markersSection.style.display = 'block';
1349
- } else {
1350
- elements.markersSection.style.display = 'none';
1351
- }
1352
-
1353
- // Show summary button after successful transcription
1354
- const summaryActionSection = document.getElementById('summary-action-section');
1355
- if (summaryActionSection) {
1356
- summaryActionSection.style.display = 'block';
1357
- }
1358
- }
1359
-
1360
- function displayMarkers(markers) {
1361
- const markersList = elements.markersList;
1362
- markersList.innerHTML = '';
1363
-
1364
- markers.forEach((timestamp, index) => {
1365
- const markerElement = document.createElement('span');
1366
- markerElement.style.cssText = 'display: inline-block; margin: 2px 5px; padding: 2px 8px; background: #007bff; color: white; border-radius: 12px; font-size: 0.85em;';
1367
- markerElement.textContent = `${index + 1}: ${formatTime(timestamp)}`;
1368
- markersList.appendChild(markerElement);
1369
- });
1370
- }
1371
-
1372
- function resetRecorder() {
1373
- state = {
1374
- ...state,
1375
- audioChunks: [],
1376
- audioBlob: null,
1377
- seconds: 0,
1378
- markers: [],
1379
- currentResult: null,
1380
- currentSummary: null
1381
- };
1382
- clearInterval(state.timerInterval);
1383
- elements.translationSection.style.display = 'none';
1384
- elements.markersSection.style.display = 'none';
1385
-
1386
- // Hide summary sections
1387
- const summaryActionSection = document.getElementById('summary-action-section');
1388
- const summarySection = document.getElementById('summary-section');
1389
- if (summaryActionSection) summaryActionSection.style.display = 'none';
1390
- if (summarySection) summarySection.style.display = 'none';
1391
-
1392
- updateUIMode('idle');
1393
- }
1394
-
1395
- function updateUIMode(mode) {
1396
- const isRecording = mode === 'recording';
1397
- const isPaused = mode === 'paused';
1398
- const isReview = mode === 'review';
1399
- const isProcessing = mode === 'processing';
1400
- const isResult = mode === 'result';
1401
- const isIdle = mode === 'idle';
1402
-
1403
- // Button states
1404
- elements.recordButton.disabled = !isIdle;
1405
- elements.stopButton.disabled = !isRecording && !isPaused;
1406
- elements.pauseButton.disabled = !isRecording && !isPaused;
1407
- elements.markButton.disabled = !isRecording && !isPaused;
1408
- elements.extractButton.disabled = !isReview;
1409
- elements.rerecordButton.disabled = !isReview && !isResult;
1410
-
1411
- // UI Visibility
1412
- elements.recordingControls.style.display = (isReview || isResult || isProcessing) ? 'none' : 'block';
1413
- elements.reviewSection.style.display = (isReview || isResult) ? 'block' : 'none';
1414
- elements.transcriptionContainer.style.display = isResult ? 'block' : 'none';
1415
- elements.loadingIndicator.style.display = isProcessing ? 'block' : 'none';
1416
-
1417
- // Recording indicator
1418
- elements.recordingIndicator.style.display = isRecording ? 'block' : 'none';
1419
-
1420
- // Button text updates
1421
- const pauseTextElement = document.getElementById('pause-text');
1422
- if (pauseTextElement) {
1423
- pauseTextElement.textContent = isPaused ?
1424
- (translations.resume_recording || 'Resume') :
1425
- (translations.pause_recording || 'Pause');
1426
- }
1427
-
1428
- // Status updates
1429
- if (isIdle) {
1430
- elements.status.textContent = translations.ready_to_record || 'Ready to Record';
1431
- elements.timer.textContent = '00:00:00';
1432
- elements.audioLevelIndicator.style.width = '0%';
1433
- } else if (isRecording) {
1434
- elements.status.textContent = translations.recording || 'Recording...';
1435
- } else if (isPaused) {
1436
- elements.status.textContent = translations.paused || 'Paused';
1437
- } else if (isReview) {
1438
- elements.status.textContent = translations.review_recording || 'Review your recording';
1439
- } else if (isProcessing) {
1440
- elements.status.textContent = translations.processing || 'Processing... please wait';
1441
- const loadingTextElement = document.getElementById('loading-text');
1442
- if (loadingTextElement) {
1443
- loadingTextElement.textContent = translations.processing || 'Processing...';
1444
- }
1445
- } else if (isResult) {
1446
- elements.status.textContent = translations.processing_complete || 'Processing Complete!';
1447
- }
1448
- }
1449
-
1450
- function startTimer() {
1451
- state.seconds = 0;
1452
- state.timerInterval = setInterval(() => {
1453
- state.seconds++;
1454
- elements.timer.textContent = formatTime(state.seconds);
1455
- }, 1000);
1456
- }
1457
-
1458
- function formatTime(totalSeconds) {
1459
- const hours = Math.floor(totalSeconds / 3600).toString().padStart(2, '0');
1460
- const minutes = Math.floor((totalSeconds % 3600) / 60).toString().padStart(2, '0');
1461
- const seconds = (totalSeconds % 60).toString().padStart(2, '0');
1462
- return `${hours}:${minutes}:${seconds}`;
1463
- }
1464
-
1465
- function visualizeAudio(stream) {
1466
- const audioContext = new (window.AudioContext || window.webkitAudioContext)();
1467
- const analyser = audioContext.createAnalyser();
1468
- const microphone = audioContext.createMediaStreamSource(stream);
1469
- microphone.connect(analyser);
1470
- analyser.fftSize = 256;
1471
- const dataArray = new Uint8Array(analyser.frequencyBinCount);
1472
-
1473
- function draw() {
1474
- if (state.mediaRecorder && state.mediaRecorder.state === 'inactive') return;
1475
- requestAnimationFrame(draw);
1476
- analyser.getByteFrequencyData(dataArray);
1477
- const avg = dataArray.reduce((a, b) => a + b, 0) / dataArray.length;
1478
- elements.audioLevelIndicator.style.width = `${(avg / 128) * 100}%`;
1479
- }
1480
- draw();
1481
- }
1482
-
1483
- function showMessage(message, type = 'info') {
1484
- const messageElement = document.createElement('div');
1485
- messageElement.className = `${type}-message`;
1486
- messageElement.textContent = message;
1487
-
1488
- elements.messageContainer.innerHTML = '';
1489
- elements.messageContainer.appendChild(messageElement);
1490
-
1491
- // Auto-hide after 5 seconds
1492
- setTimeout(() => {
1493
- if (elements.messageContainer.contains(messageElement)) {
1494
- elements.messageContainer.removeChild(messageElement);
1495
- }
1496
- }, 5000);
1497
- }
1498
-
1499
- // --- Keyboard Shortcuts ---
1500
- document.addEventListener('keydown', function(event) {
1501
- if (event.target.tagName === 'TEXTAREA' || event.target.tagName === 'INPUT') return;
1502
-
1503
- switch(event.code) {
1504
- case 'Space':
1505
- event.preventDefault();
1506
- if (!elements.recordButton.disabled) {
1507
- startRecording();
1508
- } else if (!elements.stopButton.disabled) {
1509
- stopRecording();
1510
- }
1511
- break;
1512
- case 'KeyM':
1513
- if (!elements.markButton.disabled) {
1514
- markTimestamp();
1515
- }
1516
- break;
1517
- case 'KeyP':
1518
- if (!elements.pauseButton.disabled) {
1519
- togglePause();
1520
- }
1521
- break;
1522
- case 'KeyR':
1523
- if (!elements.rerecordButton.disabled) {
1524
- resetRecorder();
1525
- }
1526
- break;
1527
- }
1528
- });
1529
-
1530
- // --- Server Status Check ---
1531
- async function checkServerStatus() {
1532
- const statusIndicator = document.createElement('div');
1533
- statusIndicator.id = 'server-status';
1534
- statusIndicator.style.cssText = `
1535
- position: fixed;
1536
- top: 10px;
1537
- left: 10px;
1538
- padding: 8px 12px;
1539
- border-radius: 6px;
1540
- font-size: 12px;
1541
- font-weight: bold;
1542
- z-index: 1000;
1543
- transition: all 0.3s;
1544
- `;
1545
- document.body.appendChild(statusIndicator);
1546
-
1547
- try {
1548
- const response = await fetch('http://localhost:5001/record', { method: 'GET' });
1549
- if (response.ok) {
1550
- statusIndicator.textContent = '🟢 Server Connected';
1551
- statusIndicator.style.background = '#d4edda';
1552
- statusIndicator.style.color = '#155724';
1553
- statusIndicator.style.border = '1px solid #c3e6cb';
1554
- } else {
1555
- throw new Error('Server not responding');
1556
- }
1557
- } catch (error) {
1558
- statusIndicator.textContent = '🔴 Server Disconnected';
1559
- statusIndicator.style.background = '#f8d7da';
1560
- statusIndicator.style.color = '#721c24';
1561
- statusIndicator.style.border = '1px solid #f5c6cb';
1562
-
1563
- // Show connection instructions
1564
- setTimeout(() => {
1565
- if (statusIndicator.parentNode) {
1566
- statusIndicator.innerHTML = `
1567
- 🔴 Server Offline<br>
1568
- <small>Run: python recorder_server.py</small>
1569
- `;
1570
- }
1571
- }, 2000);
1572
- }
1573
- }
1574
-
1575
- // --- Summary and Notes System ---
1576
- async function generateSummary() {
1577
- console.log('🤖 GENERATE SUMMARY DEBUG:');
1578
- console.log('- state.currentResult:', state.currentResult);
1579
- console.log('- original_text:', state.currentResult?.original_text);
1580
- console.log('- text:', state.currentResult?.text);
1581
- console.log('- translated_text:', state.currentResult?.translated_text);
1582
-
1583
- if (!state.currentResult) {
1584
- console.log('❌ No currentResult found');
1585
- showMessage('لا توجد نتائج متاحة للملخص', 'error');
1586
- return;
1587
- }
1588
-
1589
- const textToSummarize = state.currentResult.original_text || state.currentResult.text || state.currentResult.translated_text;
1590
-
1591
- if (!textToSummarize || textToSummarize.trim() === '') {
1592
- console.log('❌ No text available for summarization');
1593
- console.log('Available properties:', Object.keys(state.currentResult));
1594
- showMessage('لا يوجد نص متاح للملخص', 'error');
1595
- return;
1596
- }
1597
-
1598
- console.log('✅ Text to summarize:', textToSummarize.substring(0, 100) + '...');
1599
-
1600
- const summaryBtn = document.getElementById('generateSummaryBtn');
1601
- const originalContent = summaryBtn.innerHTML;
1602
- summaryBtn.disabled = true;
1603
- summaryBtn.innerHTML = '<span>⏳</span><span>جاري إنشاء الملخص...</span>';
1604
-
1605
- try {
1606
- console.log('📤 Sending summarization request...');
1607
- console.log('📤 Request payload:', {
1608
- text: textToSummarize.substring(0, 100) + '...',
1609
- language: elements.targetLanguage.value || 'arabic'
1610
- });
1611
-
1612
- const response = await fetch('http://localhost:5001/summarize', {
1613
- method: 'POST',
1614
- headers: {
1615
- 'Content-Type': 'application/json',
1616
- 'Accept': 'application/json'
1617
- },
1618
- body: JSON.stringify({
1619
- text: textToSummarize,
1620
- language: elements.targetLanguage.value || 'arabic',
1621
- type: 'full'
1622
- })
1623
- });
1624
-
1625
- console.log('📥 Response status:', response.status);
1626
- console.log('📥 Response headers:', response.headers);
1627
-
1628
- if (response.ok) {
1629
- const data = await response.json();
1630
- console.log('📥 Summarization response:', data);
1631
-
1632
- if (data.success) {
1633
- // التعامل مع الاستجابة الجديدة
1634
- if (data.summary) {
1635
- displaySummaryResults(data.summary);
1636
- showMessage('تم إنشاء الملخص بنجاح!', 'success');
1637
- } else {
1638
- console.error('❌ No summary in response:', data);
1639
- showMessage('لم يتم العثور على ملخص في الاستجابة', 'error');
1640
- }
1641
- } else {
1642
- throw new Error(data.error || 'فشل في إنشاء الملخص');
1643
- }
1644
- } else {
1645
- const errorText = await response.text();
1646
- console.error('❌ HTTP Error:', response.status, errorText);
1647
- throw new Error(`خطأ في الشبكة: ${response.status} - ${errorText}`);
1648
- }
1649
- } catch (error) {
1650
- console.error('❌ Summary generation error:', error);
1651
-
1652
- // رسائل خطأ محددة
1653
- let errorMessage = 'فشل في إنشاء الملخص';
1654
- if (error.name === 'TypeError' && error.message.includes('fetch')) {
1655
- errorMessage = 'لا يمكن الاتصال بخادم التلخيص - تأكد من تشغيل الخادم';
1656
- } else if (error.message.includes('CORS')) {
1657
- errorMessage = 'مشكلة في إعدادات CORS - يجب إعادة تشغيل الخادم';
1658
- } else {
1659
- errorMessage = error.message;
1660
- }
1661
-
1662
- showMessage(errorMessage, 'error');
1663
- } finally {
1664
- summaryBtn.disabled = false;
1665
- summaryBtn.innerHTML = originalContent;
1666
- }
1667
- }
1668
-
1669
- function displaySummaryResults(summary) {
1670
- console.log('📝 Displaying summary results:', summary);
1671
-
1672
- // Show the summary section
1673
- document.getElementById('summary-section').style.display = 'block';
1674
-
1675
- // التعامل مع تنسيق الاستجابة - قد يكون النص مباشرة أو كائن
1676
- let summaryText = '';
1677
- if (typeof summary === 'string') {
1678
- summaryText = summary;
1679
- } else if (summary && summary.main_summary) {
1680
- summaryText = summary.main_summary;
1681
- } else if (summary && typeof summary === 'object') {
1682
- // إذا كانت الاستجابة كائن، نأخذ المحتوى النصي
1683
- summaryText = JSON.stringify(summary, null, 2);
1684
- }
1685
-
1686
- // Display main summary
1687
- const summaryTextSection = document.getElementById('summary-text-section');
1688
- const summaryTextElement = document.getElementById('summary-text');
1689
- if (summaryTextSection && summaryTextElement && summaryText) {
1690
- summaryTextElement.innerHTML = summaryText.replace(/\n/g, '<br>');
1691
- summaryTextSection.style.display = 'block';
1692
- }
1693
-
1694
- // Display key points (إذا كانت متوفرة)
1695
- const keyPointsSection = document.getElementById('key-points-section');
1696
- const keyPointsList = document.getElementById('key-points-list');
1697
- if (keyPointsSection && keyPointsList && summary && summary.key_points && Array.isArray(summary.key_points)) {
1698
- keyPointsList.innerHTML = summary.key_points.map(point => `<li>${point}</li>`).join('');
1699
- keyPointsSection.style.display = 'block';
1700
- }
1701
-
1702
- // Display review questions (إذا كانت متوفرة)
1703
- const questionsSection = document.getElementById('questions-section');
1704
- const questionsList = document.getElementById('questions-list');
1705
- if (questionsSection && questionsList && summary && summary.review_questions && Array.isArray(summary.review_questions)) {
1706
- questionsList.innerHTML = summary.review_questions.map(question => `<li>${question}</li>`).join('');
1707
- questionsSection.style.display = 'block';
1708
- }
1709
-
1710
- // Show save note section
1711
- const saveNoteSection = document.getElementById('save-note-section');
1712
- if (saveNoteSection) {
1713
- saveNoteSection.style.display = 'block';
1714
- }
1715
-
1716
- // Store current summary in state for saving
1717
- state.currentSummary = summary;
1718
- }
1719
-
1720
- async function saveNote() {
1721
- if (!state.currentSummary) {
1722
- showMessage('لا يوجد ملخص متاح للحفظ', 'error');
1723
- return;
1724
- }
1725
-
1726
- const title = document.getElementById('noteTitle').value.trim();
1727
- const tags = document.getElementById('noteTags').value.trim();
1728
-
1729
- if (!title) {
1730
- showMessage('يرجى إدخال عنوان للمذكرة', 'error');
1731
- return;
1732
- }
1733
-
1734
- const saveBtn = document.getElementById('saveNoteBtn');
1735
- const originalText = saveBtn.textContent;
1736
- saveBtn.disabled = true;
1737
- saveBtn.textContent = 'جاري الحفظ...';
1738
-
1739
- try {
1740
- const contentToSave = state.currentResult.original_text || state.currentResult.text;
1741
- const response = await fetch('http://localhost:5001/notes', {
1742
- method: 'POST',
1743
- headers: { 'Content-Type': 'application/json' },
1744
- body: JSON.stringify({
1745
- title: title,
1746
- content: contentToSave,
1747
- summary: state.currentSummary,
1748
- tags: tags ? tags.split(',').map(tag => tag.trim()) : [],
1749
- markers: state.markers
1750
- })
1751
- });
1752
-
1753
- if (response.ok) {
1754
- const data = await response.json();
1755
- if (data.success) {
1756
- showMessage('تم حفظ المذكرة بنجاح!', 'success');
1757
- // Clear form
1758
- document.getElementById('noteTitle').value = '';
1759
- document.getElementById('noteTags').value = '';
1760
- // Refresh notes list
1761
- loadNotesList();
1762
- } else {
1763
- throw new Error(data.error || 'Failed to save note');
1764
- }
1765
- } else {
1766
- throw new Error('Network error while saving note');
1767
- }
1768
- } catch (error) {
1769
- console.error('Note saving error:', error);
1770
- showMessage('فشل في حفظ المذكرة: ' + error.message, 'error');
1771
- } finally {
1772
- saveBtn.disabled = false;
1773
- saveBtn.textContent = originalText;
1774
- }
1775
- }
1776
-
1777
- async function loadNotesList() {
1778
- try {
1779
- const response = await fetch('http://localhost:5001/notes');
1780
- if (response.ok) {
1781
- const data = await response.json();
1782
- if (data.success) {
1783
- displayNotesList(data.notes);
1784
- }
1785
- }
1786
- } catch (error) {
1787
- console.error('Failed to load notes:', error);
1788
- }
1789
- }
1790
-
1791
- function displayNotesList(notes) {
1792
- const notesContainer = document.getElementById('notesContainer');
1793
-
1794
- if (notes.length === 0) {
1795
- notesContainer.innerHTML = '<p class="text-center text-muted">No saved notes yet</p>';
1796
- return;
1797
- }
1798
-
1799
- notesContainer.innerHTML = notes.map(note => `
1800
- <div class="note-item" data-note-id="${note.id}">
1801
- <div class="note-header">
1802
- <h4>${note.title}</h4>
1803
- <span class="note-date">${new Date(note.created_at).toLocaleDateString()}</span>
1804
- </div>
1805
- <div class="note-summary">
1806
- ${note.summary ? note.summary.main_summary.substring(0, 150) + '...' : 'No summary available'}
1807
- </div>
1808
- <div class="note-tags">
1809
- ${note.tags ? note.tags.map(tag => `<span class="tag">${tag}</span>`).join('') : ''}
1810
- </div>
1811
- <div class="note-actions">
1812
- <button onclick="viewNote(${note.id})" class="btn-secondary">View</button>
1813
- <button onclick="deleteNote(${note.id})" class="btn-danger">Delete</button>
1814
- </div>
1815
- </div>
1816
- `).join('');
1817
- }
1818
-
1819
- async function viewNote(noteId) {
1820
- try {
1821
- const response = await fetch(`http://localhost:5001/notes/${noteId}`);
1822
- if (response.ok) {
1823
- const data = await response.json();
1824
- if (data.success) {
1825
- displayNoteDetails(data.note);
1826
- }
1827
- }
1828
- } catch (error) {
1829
- console.error('Failed to load note:', error);
1830
- showMessage('Failed to load note details', 'error');
1831
- }
1832
- }
1833
-
1834
- function displayNoteDetails(note) {
1835
- // Create modal or expand view for note details
1836
- const modal = document.createElement('div');
1837
- modal.className = 'note-modal';
1838
- modal.innerHTML = `
1839
- <div class="note-modal-content">
1840
- <div class="note-modal-header">
1841
- <h2>${note.title}</h2>
1842
- <button onclick="closeNoteModal()" class="close-btn">&times;</button>
1843
- </div>
1844
- <div class="note-modal-body">
1845
- <div class="note-section">
1846
- <h3>Summary</h3>
1847
- <p>${note.summary ? note.summary.main_summary : 'No summary available'}</p>
1848
- </div>
1849
- ${note.summary && note.summary.key_points ? `
1850
- <div class="note-section">
1851
- <h3>Key Points</h3>
1852
- <ul>${note.summary.key_points.map(point => `<li>${point}</li>`).join('')}</ul>
1853
- </div>
1854
- ` : ''}
1855
- <div class="note-section">
1856
- <h3>Full Transcription</h3>
1857
- <p>${note.content}</p>
1858
- </div>
1859
- ${note.tags && note.tags.length > 0 ? `
1860
- <div class="note-section">
1861
- <h3>Tags</h3>
1862
- <div class="note-tags">
1863
- ${note.tags.map(tag => `<span class="tag">${tag}</span>`).join('')}
1864
- </div>
1865
- </div>
1866
- ` : ''}
1867
- </div>
1868
- </div>
1869
- `;
1870
- document.body.appendChild(modal);
1871
- }
1872
-
1873
- function closeNoteModal() {
1874
- const modal = document.querySelector('.note-modal');
1875
- if (modal) {
1876
- modal.remove();
1877
- }
1878
- }
1879
-
1880
- async function deleteNote(noteId) {
1881
- if (!confirm('Are you sure you want to delete this note?')) {
1882
- return;
1883
- }
1884
-
1885
- try {
1886
- const response = await fetch(`http://localhost:5001/notes/${noteId}`, {
1887
- method: 'DELETE'
1888
- });
1889
-
1890
- if (response.ok) {
1891
- const data = await response.json();
1892
- if (data.success) {
1893
- showMessage('Note deleted successfully', 'success');
1894
- loadNotesList();
1895
- } else {
1896
- throw new Error(data.error || 'Failed to delete note');
1897
- }
1898
- } else {
1899
- throw new Error('Network error while deleting note');
1900
- }
1901
- } catch (error) {
1902
- console.error('Delete note error:', error);
1903
- showMessage('Failed to delete note: ' + error.message, 'error');
1904
- }
1905
- }
1906
-
1907
- function searchNotes() {
1908
- const searchTerm = document.getElementById('notesSearch').value.toLowerCase();
1909
- const noteItems = document.querySelectorAll('.note-item');
1910
-
1911
- noteItems.forEach(item => {
1912
- const title = item.querySelector('h4').textContent.toLowerCase();
1913
- const summary = item.querySelector('.note-summary').textContent.toLowerCase();
1914
- const tags = item.querySelector('.note-tags').textContent.toLowerCase();
1915
-
1916
- if (title.includes(searchTerm) || summary.includes(searchTerm) || tags.includes(searchTerm)) {
1917
- item.style.display = 'block';
1918
- } else {
1919
- item.style.display = 'none';
1920
- }
1921
- });
1922
- }
1923
-
1924
- // --- Initialization ---
1925
- async function initialize() {
1926
- await loadTranslations(currentLanguage);
1927
- await checkServerStatus();
1928
- toggleTranslationUI();
1929
- updateTargetLanguageInfo();
1930
- resetRecorder();
1931
-
1932
- // Load existing notes
1933
- loadNotesList();
1934
-
1935
- // Add event listeners for summary and notes
1936
- const generateSummaryBtn = document.getElementById('generateSummaryBtn');
1937
- const saveNoteBtn = document.getElementById('saveNoteBtn');
1938
- const notesSearch = document.getElementById('notesSearch');
1939
-
1940
- if (generateSummaryBtn) {
1941
- generateSummaryBtn.addEventListener('click', generateSummary);
1942
- }
1943
- if (saveNoteBtn) {
1944
- saveNoteBtn.addEventListener('click', saveNote);
1945
- }
1946
- if (notesSearch) {
1947
- notesSearch.addEventListener('input', searchNotes);
1948
- }
1949
-
1950
- // Check server status every 30 seconds
1951
- setInterval(checkServerStatus, 30000);
1952
- }
1953
-
1954
- // Start the application
1955
- initialize();
1956
- </script>
1957
- </body>
1958
- </html>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
translator.py CHANGED
@@ -6,6 +6,7 @@ import json
6
  import traceback
7
  from typing import Dict, List, Optional, Tuple
8
  import google.generativeai as genai
 
9
 
10
  class AITranslator:
11
  """
@@ -13,7 +14,7 @@ class AITranslator:
13
  """
14
 
15
  def __init__(self):
16
- self.client = None
17
  self.init_error = None
18
  self.supported_languages = {
19
  'ar': 'Arabic (العربية)',
@@ -28,16 +29,20 @@ class AITranslator:
28
  self._initialize_gemini()
29
 
30
  def _initialize_gemini(self):
31
- """Initialize Gemini AI client"""
32
  try:
33
  load_dotenv()
34
  api_key = os.getenv("GEMINI_API_KEY")
35
  if not api_key:
36
- raise ValueError("GEMINI_API_KEY not found in environment variables.")
37
- self.client = genai.Client(api_key=api_key)
 
 
 
 
38
  except Exception as e:
39
  self.init_error = f"FATAL ERROR during Gemini Init: {str(e)}"
40
- self.client = None
41
 
42
  def translate_text(self, text: str, target_language: str = 'ar', source_language: str = 'auto') -> Tuple[Optional[str], Optional[str]]:
43
  """
@@ -54,27 +59,21 @@ class AITranslator:
54
  if self.init_error:
55
  return None, self.init_error
56
 
57
- if not self.client:
58
- return None, "ERROR: Gemini client is not available."
59
 
60
  if not text or not text.strip():
61
  return None, "ERROR: Empty text provided for translation."
62
 
63
  try:
64
- # Get language name for better prompting
65
  target_lang_name = self.supported_languages.get(target_language, target_language)
66
-
67
- # Create specialized prompt for better translation quality
68
  prompt = self._create_translation_prompt(text, target_lang_name, target_language)
69
 
70
- response = self.client.models.generate_content(
71
- model="gemini-2.5-flash",
72
- contents=[prompt]
73
- )
74
 
75
- if response and response.text:
76
  translated_text = response.text.strip()
77
- # Clean up any markdown formatting that might be added
78
  translated_text = self._clean_translation_output(translated_text)
79
  return translated_text, None
80
  else:
@@ -196,8 +195,8 @@ Text to translate:
196
  Returns:
197
  Tuple of (language_code, error_message)
198
  """
199
- if not self.client:
200
- return None, "ERROR: Gemini client not available"
201
 
202
  try:
203
  prompt = f"""
@@ -215,12 +214,9 @@ Detect the language of the following text and return ONLY the language code:
215
  Text: {text[:200]}...
216
  """
217
 
218
- response = self.client.models.generate_content(
219
- model="gemini-2.5-flash",
220
- contents=[prompt]
221
- )
222
 
223
- if response and response.text:
224
  detected_lang = response.text.strip().lower()
225
  return detected_lang, None
226
  else:
@@ -339,12 +335,10 @@ def get_translation(key: str, language: str = 'en') -> str:
339
  return UI_TRANSLATIONS.get(language, {}).get(key, UI_TRANSLATIONS['en'].get(key, key))
340
 
341
 
342
- # Initialize global translator instance
343
- translator_instance = None
344
-
345
  def get_translator():
346
- """Get singleton translator instance"""
347
- global translator_instance
348
- if translator_instance is None:
349
- translator_instance = AITranslator()
350
- return translator_instance
 
6
  import traceback
7
  from typing import Dict, List, Optional, Tuple
8
  import google.generativeai as genai
9
+ import streamlit as st
10
 
11
  class AITranslator:
12
  """
 
14
  """
15
 
16
  def __init__(self):
17
+ self.model = None
18
  self.init_error = None
19
  self.supported_languages = {
20
  'ar': 'Arabic (العربية)',
 
29
  self._initialize_gemini()
30
 
31
  def _initialize_gemini(self):
32
+ """Initialize Gemini AI client using the new API structure."""
33
  try:
34
  load_dotenv()
35
  api_key = os.getenv("GEMINI_API_KEY")
36
  if not api_key:
37
+ # This error is handled by the main app now, but we keep a fallback.
38
+ self.init_error = "GEMINI_API_KEY not found."
39
+ return
40
+
41
+ genai.configure(api_key=api_key)
42
+ self.model = genai.GenerativeModel('gemini-1.5-flash')
43
  except Exception as e:
44
  self.init_error = f"FATAL ERROR during Gemini Init: {str(e)}"
45
+ self.model = None
46
 
47
  def translate_text(self, text: str, target_language: str = 'ar', source_language: str = 'auto') -> Tuple[Optional[str], Optional[str]]:
48
  """
 
59
  if self.init_error:
60
  return None, self.init_error
61
 
62
+ if not self.model:
63
+ return None, "ERROR: Gemini model is not available."
64
 
65
  if not text or not text.strip():
66
  return None, "ERROR: Empty text provided for translation."
67
 
68
  try:
 
69
  target_lang_name = self.supported_languages.get(target_language, target_language)
 
 
70
  prompt = self._create_translation_prompt(text, target_lang_name, target_language)
71
 
72
+ # Use the new generate_content method on the model instance
73
+ response = self.model.generate_content(prompt)
 
 
74
 
75
+ if response and hasattr(response, 'text') and response.text:
76
  translated_text = response.text.strip()
 
77
  translated_text = self._clean_translation_output(translated_text)
78
  return translated_text, None
79
  else:
 
195
  Returns:
196
  Tuple of (language_code, error_message)
197
  """
198
+ if not self.model:
199
+ return None, "ERROR: Gemini model not available"
200
 
201
  try:
202
  prompt = f"""
 
214
  Text: {text[:200]}...
215
  """
216
 
217
+ response = self.model.generate_content(prompt)
 
 
 
218
 
219
+ if response and hasattr(response, 'text') and response.text:
220
  detected_lang = response.text.strip().lower()
221
  return detected_lang, None
222
  else:
 
335
  return UI_TRANSLATIONS.get(language, {}).get(key, UI_TRANSLATIONS['en'].get(key, key))
336
 
337
 
338
+ @st.cache_resource
 
 
339
  def get_translator():
340
+ """
341
+ Get a singleton translator instance using Streamlit's resource caching.
342
+ This ensures the model is initialized only once per session.
343
+ """
344
+ return AITranslator()