Spaces:
Sleeping
Sleeping
Commit ·
e879d8d
1
Parent(s): 44a460d
fix
Browse files- app.py +152 -254
- audio_processor.py +12 -29
- integrated_server.py +0 -235
- recorder_server.py +0 -452
- summarizer.py +10 -19
- templates/recorder.html +0 -1958
- translator.py +25 -31
app.py
CHANGED
|
@@ -1,4 +1,4 @@
|
|
| 1 |
-
# app.py -
|
| 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
|
| 46 |
-
from translator import get_translator,
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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 '
|
| 90 |
-
st.session_state.
|
| 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.
|
| 215 |
-
st.success(f"File
|
| 216 |
-
st.audio(
|
| 217 |
if st.button("🚀 Start AI Processing", type="primary", use_container_width=True):
|
| 218 |
-
|
| 219 |
-
if st.session_state.
|
| 220 |
-
if st.button("🔄
|
| 221 |
reset_session()
|
| 222 |
st.rerun()
|
| 223 |
|
| 224 |
with record_tab:
|
| 225 |
st.subheader("Record audio directly from your microphone")
|
| 226 |
-
st.info("
|
| 227 |
-
|
| 228 |
-
# Show the recorder component
|
| 229 |
-
show_recorder_page()
|
| 230 |
|
| 231 |
-
|
| 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
|
| 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("
|
| 436 |
suffix = st.session_state.transcription_data['original_suffix']
|
| 437 |
audio_bytes = st.session_state.transcription_data['audio_bytes']
|
| 438 |
-
|
| 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.
|
| 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', '
|
| 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 |
-
|
| 502 |
-
|
| 503 |
-
|
| 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.
|
| 55 |
-
return None,
|
| 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 |
-
|
| 64 |
-
audio_bytes = f.read()
|
| 65 |
|
| 66 |
-
|
| 67 |
-
|
| 68 |
|
| 69 |
-
response
|
| 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.
|
| 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.
|
| 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.
|
| 60 |
return None, "خدمة الذكاء الاصطناعي غير متوفرة"
|
| 61 |
|
| 62 |
try:
|
| 63 |
prompt = self._create_key_points_prompt(text, language)
|
| 64 |
|
| 65 |
-
response = self.translator.
|
| 66 |
-
|
| 67 |
-
|
| 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.
|
| 134 |
return None, "خدمة الذكاء الاصطناعي غير متوفرة"
|
| 135 |
|
| 136 |
try:
|
| 137 |
prompt = self._create_questions_prompt(text, language)
|
| 138 |
|
| 139 |
-
response = self.translator.
|
| 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">×</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.
|
| 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 |
-
|
| 37 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 38 |
except Exception as e:
|
| 39 |
self.init_error = f"FATAL ERROR during Gemini Init: {str(e)}"
|
| 40 |
-
self.
|
| 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.
|
| 58 |
-
return None, "ERROR: Gemini
|
| 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 |
-
|
| 71 |
-
|
| 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.
|
| 200 |
-
return None, "ERROR: Gemini
|
| 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.
|
| 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 |
-
|
| 343 |
-
translator_instance = None
|
| 344 |
-
|
| 345 |
def get_translator():
|
| 346 |
-
"""
|
| 347 |
-
|
| 348 |
-
|
| 349 |
-
|
| 350 |
-
return
|
|
|
|
| 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()
|