STTR
commited on
Commit
·
a29fadd
1
Parent(s):
2ceddcf
Switch to Flask with custom dark theme UI
Browse files- Dockerfile +21 -0
- README.md +13 -69
- app.py +70 -277
- requirements.txt +5 -7
- static/enhanced.css +620 -0
- static/enhanced.js +217 -0
- static/favicon.ico +0 -0
- static/manifest.json +17 -0
- static/script.js +1982 -0
- templates/admin.html +357 -0
- templates/index.html +914 -0
- templates/index_new.html +727 -0
Dockerfile
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
FROM python:3.10-slim
|
| 2 |
+
|
| 3 |
+
WORKDIR /app
|
| 4 |
+
|
| 5 |
+
# Install system dependencies
|
| 6 |
+
RUN apt-get update && apt-get install -y \
|
| 7 |
+
git \
|
| 8 |
+
&& rm -rf /var/lib/apt/lists/*
|
| 9 |
+
|
| 10 |
+
# Copy requirements
|
| 11 |
+
COPY requirements.txt .
|
| 12 |
+
RUN pip install --no-cache-dir -r requirements.txt
|
| 13 |
+
|
| 14 |
+
# Copy app files
|
| 15 |
+
COPY . .
|
| 16 |
+
|
| 17 |
+
# Expose port
|
| 18 |
+
EXPOSE 7860
|
| 19 |
+
|
| 20 |
+
# Run Flask app
|
| 21 |
+
CMD ["python", "app.py"]
|
README.md
CHANGED
|
@@ -3,80 +3,24 @@ title: Instant Translat - AI Voice Translation
|
|
| 3 |
emoji: 🌍
|
| 4 |
colorFrom: purple
|
| 5 |
colorTo: blue
|
| 6 |
-
sdk:
|
| 7 |
-
sdk_version: "4.44.0"
|
| 8 |
-
app_file: app.py
|
| 9 |
pinned: true
|
| 10 |
license: mit
|
| 11 |
-
hardware: t4-small
|
| 12 |
---
|
| 13 |
|
| 14 |
# 🌍 Instant Translat - AI Voice Translation
|
| 15 |
|
| 16 |
-
**Real-time voice translation with
|
| 17 |
|
| 18 |
-
##
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 19 |
|
| 20 |
-
|
| 21 |
-
-
|
| 22 |
-
-
|
| 23 |
-
-
|
| 24 |
-
-
|
| 25 |
-
|
| 26 |
-
## 🌍 Supported Languages
|
| 27 |
-
|
| 28 |
-
- 🇲🇦 **Moroccan Arabic (Darija)** - الدارجة المغربية
|
| 29 |
-
- 🇸🇦 Arabic (MSA)
|
| 30 |
-
- 🇫🇷 French
|
| 31 |
-
- 🇬🇧 English
|
| 32 |
-
- 🇪🇸 Spanish
|
| 33 |
-
- 🇩🇪 German
|
| 34 |
-
- 🇮🇹 Italian
|
| 35 |
-
- 🇵🇹 Portuguese
|
| 36 |
-
- 🇨🇳 Chinese
|
| 37 |
-
- 🇯🇵 Japanese
|
| 38 |
-
- 🇰🇷 Korean
|
| 39 |
-
- 🇷🇺 Russian
|
| 40 |
-
- And 190+ more languages!
|
| 41 |
-
|
| 42 |
-
## 🎯 How to Use
|
| 43 |
-
|
| 44 |
-
1. **Select Languages**: Choose your source and target languages
|
| 45 |
-
2. **Record**: Click the microphone button and speak clearly
|
| 46 |
-
3. **Translate**: Click "Translate" button
|
| 47 |
-
4. **Listen**: Hear the translation with natural voice
|
| 48 |
-
5. **Voice Clone**: Enable to hear translation in your own voice!
|
| 49 |
-
|
| 50 |
-
## 🔧 Technology
|
| 51 |
-
|
| 52 |
-
- **STT**: Meta's SeamlessM4T v2 Large
|
| 53 |
-
- **Translation**: Meta's NLLB-200
|
| 54 |
-
- **TTS**: Fish Audio S1
|
| 55 |
-
- **Voice Cloning**: Fish Audio API
|
| 56 |
-
- **Framework**: Gradio + PyTorch
|
| 57 |
-
|
| 58 |
-
## 🔒 Privacy & Security
|
| 59 |
-
|
| 60 |
-
- ✅ No data stored
|
| 61 |
-
- ✅ Real-time processing
|
| 62 |
-
- ✅ Secure API calls
|
| 63 |
-
- ✅ Open source
|
| 64 |
-
|
| 65 |
-
## 📱 Use Cases
|
| 66 |
-
|
| 67 |
-
- 🗣️ Real-time conversations
|
| 68 |
-
- 📚 Language learning
|
| 69 |
-
- 🌐 Travel assistance
|
| 70 |
-
- 💼 Business meetings
|
| 71 |
-
- 🎓 Education
|
| 72 |
-
|
| 73 |
-
## 🚀 Coming Soon
|
| 74 |
-
|
| 75 |
-
- 💳 Premium features with Apple Pay & Google Pay
|
| 76 |
-
- 📱 Mobile app (iOS & Android)
|
| 77 |
-
- 🎯 More languages
|
| 78 |
-
- 🔊 More voice options
|
| 79 |
-
|
| 80 |
-
---
|
| 81 |
-
|
| 82 |
-
**Made with ❤️ using Meta AI models**
|
|
|
|
| 3 |
emoji: 🌍
|
| 4 |
colorFrom: purple
|
| 5 |
colorTo: blue
|
| 6 |
+
sdk: docker
|
|
|
|
|
|
|
| 7 |
pinned: true
|
| 8 |
license: mit
|
|
|
|
| 9 |
---
|
| 10 |
|
| 11 |
# 🌍 Instant Translat - AI Voice Translation
|
| 12 |
|
| 13 |
+
**Real-time voice translation with beautiful dark theme UI**
|
| 14 |
|
| 15 |
+
## Features
|
| 16 |
+
- 🎤 Speech-to-Text (SeamlessM4T v2 Large)
|
| 17 |
+
- 🌍 Translation (NLLB-200 - 200 languages + Darija)
|
| 18 |
+
- 🔊 Text-to-Speech (Fish Audio S1)
|
| 19 |
+
- 🎭 Voice Cloning
|
| 20 |
+
- 🌙 Beautiful Dark Theme UI
|
| 21 |
|
| 22 |
+
## Technology
|
| 23 |
+
- Flask backend
|
| 24 |
+
- Custom dark theme interface
|
| 25 |
+
- Meta AI models
|
| 26 |
+
- Fish Audio TTS
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
app.py
CHANGED
|
@@ -1,4 +1,4 @@
|
|
| 1 |
-
import
|
| 2 |
from transformers import (
|
| 3 |
AutoProcessor,
|
| 4 |
SeamlessM4Tv2ForSpeechToText,
|
|
@@ -7,9 +7,13 @@ from transformers import (
|
|
| 7 |
)
|
| 8 |
import torch
|
| 9 |
import numpy as np
|
|
|
|
|
|
|
| 10 |
import requests
|
| 11 |
import os
|
| 12 |
|
|
|
|
|
|
|
| 13 |
# ============================================================
|
| 14 |
# Device Setup
|
| 15 |
# ============================================================
|
|
@@ -34,62 +38,42 @@ nllb_model = AutoModelForSeq2SeqLM.from_pretrained(NLLB_MODEL)
|
|
| 34 |
nllb_model = nllb_model.to(device).eval()
|
| 35 |
print("✅ NLLB-200 loaded!")
|
| 36 |
|
| 37 |
-
#
|
| 38 |
-
|
| 39 |
-
# ============================================================
|
| 40 |
-
NLLB_LANGS = {
|
| 41 |
-
"🇲🇦 Moroccan Arabic (Darija)": "ary_Arab",
|
| 42 |
-
"🇸🇦 Arabic": "arb_Arab",
|
| 43 |
-
"🇫🇷 French": "fra_Latn",
|
| 44 |
-
"🇬🇧 English": "eng_Latn",
|
| 45 |
-
"🇪🇸 Spanish": "spa_Latn",
|
| 46 |
-
"🇩🇪 German": "deu_Latn",
|
| 47 |
-
"🇮🇹 Italian": "ita_Latn",
|
| 48 |
-
"🇵🇹 Portuguese": "por_Latn",
|
| 49 |
-
"🇨🇳 Chinese": "zho_Hans",
|
| 50 |
-
"🇯🇵 Japanese": "jpn_Jpan",
|
| 51 |
-
"🇰🇷 Korean": "kor_Hang",
|
| 52 |
-
"🇷🇺 Russian": "rus_Cyrl",
|
| 53 |
-
}
|
| 54 |
|
| 55 |
-
|
| 56 |
-
|
| 57 |
-
|
| 58 |
-
|
| 59 |
-
|
| 60 |
-
|
| 61 |
-
|
| 62 |
-
"🇮🇹 Italian": "ita",
|
| 63 |
-
"🇵🇹 Portuguese": "por",
|
| 64 |
-
"🇨🇳 Chinese": "cmn",
|
| 65 |
-
"🇯🇵 Japanese": "jpn",
|
| 66 |
-
"🇰🇷 Korean": "kor",
|
| 67 |
-
"🇷🇺 Russian": "rus",
|
| 68 |
}
|
| 69 |
|
| 70 |
-
|
| 71 |
-
|
| 72 |
-
|
| 73 |
-
# Functions
|
| 74 |
-
# ============================================================
|
| 75 |
|
| 76 |
-
|
| 77 |
-
|
| 78 |
-
if audio is None:
|
| 79 |
-
return None, "❌ Please record audio first"
|
| 80 |
-
|
| 81 |
try:
|
| 82 |
-
|
| 83 |
-
|
| 84 |
-
|
| 85 |
-
|
| 86 |
-
|
| 87 |
-
|
| 88 |
-
|
| 89 |
-
|
| 90 |
-
|
| 91 |
-
|
| 92 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 93 |
inputs = stt_processor(
|
| 94 |
audios=audio_data,
|
| 95 |
sampling_rate=sample_rate,
|
|
@@ -105,9 +89,9 @@ def translate_audio(audio, source_lang, target_lang, enable_voice_clone):
|
|
| 105 |
|
| 106 |
transcript = stt_processor.decode(output_tokens[0].tolist(), skip_special_tokens=True)
|
| 107 |
|
| 108 |
-
#
|
| 109 |
-
src_nllb =
|
| 110 |
-
tgt_nllb =
|
| 111 |
|
| 112 |
nllb_tokenizer.src_lang = src_nllb
|
| 113 |
inputs = nllb_tokenizer(transcript, return_tensors="pt", padding=True, truncation=True, max_length=512).to(device)
|
|
@@ -124,242 +108,51 @@ def translate_audio(audio, source_lang, target_lang, enable_voice_clone):
|
|
| 124 |
|
| 125 |
translation = nllb_tokenizer.decode(outputs[0], skip_special_tokens=True)
|
| 126 |
|
| 127 |
-
#
|
| 128 |
-
|
| 129 |
if FISH_AUDIO_API_KEY:
|
| 130 |
-
tts_audio = generate_tts(translation
|
| 131 |
-
|
| 132 |
-
|
| 133 |
-
|
| 134 |
-
|
| 135 |
-
|
| 136 |
-
|
| 137 |
-
|
| 138 |
-
|
| 139 |
-
|
| 140 |
-
|
| 141 |
-
|
| 142 |
-
"""
|
| 143 |
-
|
| 144 |
-
return tts_audio, result_text
|
| 145 |
|
| 146 |
except Exception as e:
|
| 147 |
-
return
|
| 148 |
|
| 149 |
-
def generate_tts(text
|
| 150 |
"""Generate TTS using Fish Audio"""
|
| 151 |
if not FISH_AUDIO_API_KEY:
|
| 152 |
return None
|
| 153 |
|
| 154 |
try:
|
| 155 |
headers = {'Authorization': f'Bearer {FISH_AUDIO_API_KEY}'}
|
| 156 |
-
|
| 157 |
-
|
| 158 |
-
|
| 159 |
-
|
| 160 |
-
|
| 161 |
-
|
| 162 |
-
|
| 163 |
-
|
| 164 |
-
|
| 165 |
-
|
| 166 |
-
|
| 167 |
-
|
| 168 |
-
data = {
|
| 169 |
-
'text': text,
|
| 170 |
-
'format': 'mp3',
|
| 171 |
-
'mp3_bitrate': '192',
|
| 172 |
-
'latency': 'balanced',
|
| 173 |
-
'normalize': 'true',
|
| 174 |
-
}
|
| 175 |
-
|
| 176 |
-
response = requests.post(
|
| 177 |
-
'https://api.fish.audio/v1/tts',
|
| 178 |
-
headers=headers,
|
| 179 |
-
files=files,
|
| 180 |
-
data=data,
|
| 181 |
-
timeout=120
|
| 182 |
-
)
|
| 183 |
-
|
| 184 |
-
os.remove(audio_path)
|
| 185 |
-
else:
|
| 186 |
-
payload = {
|
| 187 |
-
'text': text,
|
| 188 |
-
'format': 'mp3',
|
| 189 |
-
'mp3_bitrate': 192,
|
| 190 |
-
}
|
| 191 |
-
|
| 192 |
-
response = requests.post(
|
| 193 |
-
'https://api.fish.audio/v1/tts',
|
| 194 |
-
headers=headers,
|
| 195 |
-
json=payload,
|
| 196 |
-
timeout=60
|
| 197 |
-
)
|
| 198 |
|
| 199 |
if response.status_code == 200:
|
| 200 |
-
|
| 201 |
-
with tempfile.NamedTemporaryFile(suffix='.mp3', delete=False) as f:
|
| 202 |
-
f.write(response.content)
|
| 203 |
-
return f.name
|
| 204 |
|
| 205 |
return None
|
| 206 |
except:
|
| 207 |
return None
|
| 208 |
|
| 209 |
-
|
| 210 |
-
|
| 211 |
-
# ============================================================
|
| 212 |
-
|
| 213 |
-
custom_css = """
|
| 214 |
-
/* Modern Gradient Background */
|
| 215 |
-
.gradio-container {
|
| 216 |
-
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%) !important;
|
| 217 |
-
font-family: 'Inter', sans-serif;
|
| 218 |
-
}
|
| 219 |
-
|
| 220 |
-
/* Card Style */
|
| 221 |
-
.contain {
|
| 222 |
-
background: rgba(255, 255, 255, 0.95) !important;
|
| 223 |
-
border-radius: 20px !important;
|
| 224 |
-
box-shadow: 0 20px 60px rgba(0, 0, 0, 0.3) !important;
|
| 225 |
-
padding: 30px !important;
|
| 226 |
-
backdrop-filter: blur(10px) !important;
|
| 227 |
-
}
|
| 228 |
-
|
| 229 |
-
/* Buttons */
|
| 230 |
-
.primary {
|
| 231 |
-
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%) !important;
|
| 232 |
-
border: none !important;
|
| 233 |
-
border-radius: 12px !important;
|
| 234 |
-
padding: 15px 30px !important;
|
| 235 |
-
font-weight: 600 !important;
|
| 236 |
-
font-size: 1.1em !important;
|
| 237 |
-
transition: all 0.3s ease !important;
|
| 238 |
-
}
|
| 239 |
-
|
| 240 |
-
.primary:hover {
|
| 241 |
-
transform: translateY(-2px) !important;
|
| 242 |
-
box-shadow: 0 10px 25px rgba(102, 126, 234, 0.4) !important;
|
| 243 |
-
}
|
| 244 |
-
|
| 245 |
-
/* Input Fields */
|
| 246 |
-
.input-audio, .dropdown {
|
| 247 |
-
border-radius: 12px !important;
|
| 248 |
-
border: 2px solid #e0e0e0 !important;
|
| 249 |
-
}
|
| 250 |
-
|
| 251 |
-
/* Headers */
|
| 252 |
-
h1, h2, h3 {
|
| 253 |
-
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
| 254 |
-
-webkit-background-clip: text;
|
| 255 |
-
-webkit-text-fill-color: transparent;
|
| 256 |
-
font-weight: 700;
|
| 257 |
-
}
|
| 258 |
-
|
| 259 |
-
/* Markdown Content */
|
| 260 |
-
.markdown-text {
|
| 261 |
-
line-height: 1.8;
|
| 262 |
-
}
|
| 263 |
-
"""
|
| 264 |
-
|
| 265 |
-
# ============================================================
|
| 266 |
-
# Gradio Interface with Custom Theme
|
| 267 |
-
# ============================================================
|
| 268 |
-
|
| 269 |
-
theme = gr.themes.Soft(
|
| 270 |
-
primary_hue="purple",
|
| 271 |
-
secondary_hue="pink",
|
| 272 |
-
neutral_hue="slate",
|
| 273 |
-
font=gr.themes.GoogleFont("Inter"),
|
| 274 |
-
).set(
|
| 275 |
-
button_primary_background_fill="*primary_500",
|
| 276 |
-
button_primary_background_fill_hover="*primary_600",
|
| 277 |
-
button_primary_text_color="white",
|
| 278 |
-
)
|
| 279 |
-
|
| 280 |
-
with gr.Blocks(theme=theme, css=custom_css, title="Instant Translat") as demo:
|
| 281 |
-
gr.Markdown("""
|
| 282 |
-
# 🌍 Instant Translat
|
| 283 |
-
### AI-Powered Voice Translation in 200+ Languages
|
| 284 |
-
|
| 285 |
-
Translate your voice instantly with cutting-edge AI. Supports Moroccan Darija and 200+ languages!
|
| 286 |
-
""")
|
| 287 |
-
|
| 288 |
-
with gr.Row():
|
| 289 |
-
with gr.Column(scale=1):
|
| 290 |
-
gr.Markdown("### 🎤 Input")
|
| 291 |
-
|
| 292 |
-
audio_input = gr.Audio(
|
| 293 |
-
label="Record Your Voice",
|
| 294 |
-
type="numpy",
|
| 295 |
-
sources=["microphone"],
|
| 296 |
-
elem_classes="input-audio"
|
| 297 |
-
)
|
| 298 |
-
|
| 299 |
-
with gr.Row():
|
| 300 |
-
source_lang = gr.Dropdown(
|
| 301 |
-
choices=list(NLLB_LANGS.keys()),
|
| 302 |
-
value="🇲🇦 Moroccan Arabic (Darija)",
|
| 303 |
-
label="🗣️ From",
|
| 304 |
-
elem_classes="dropdown"
|
| 305 |
-
)
|
| 306 |
-
|
| 307 |
-
target_lang = gr.Dropdown(
|
| 308 |
-
choices=list(NLLB_LANGS.keys()),
|
| 309 |
-
value="🇬🇧 English",
|
| 310 |
-
label="🎯 To",
|
| 311 |
-
elem_classes="dropdown"
|
| 312 |
-
)
|
| 313 |
-
|
| 314 |
-
voice_clone = gr.Checkbox(
|
| 315 |
-
label="🎭 Clone My Voice",
|
| 316 |
-
value=True,
|
| 317 |
-
info="Hear translation in your own voice"
|
| 318 |
-
)
|
| 319 |
-
|
| 320 |
-
translate_btn = gr.Button(
|
| 321 |
-
"🌍 Translate Now",
|
| 322 |
-
variant="primary",
|
| 323 |
-
size="lg",
|
| 324 |
-
elem_classes="primary"
|
| 325 |
-
)
|
| 326 |
-
|
| 327 |
-
with gr.Column(scale=1):
|
| 328 |
-
gr.Markdown("### 🔊 Output")
|
| 329 |
-
|
| 330 |
-
audio_output = gr.Audio(
|
| 331 |
-
label="Translation Audio",
|
| 332 |
-
type="filepath"
|
| 333 |
-
)
|
| 334 |
-
|
| 335 |
-
text_output = gr.HTML(label="Translation Text")
|
| 336 |
-
|
| 337 |
-
translate_btn.click(
|
| 338 |
-
translate_audio,
|
| 339 |
-
inputs=[audio_input, source_lang, target_lang, voice_clone],
|
| 340 |
-
outputs=[audio_output, text_output]
|
| 341 |
-
)
|
| 342 |
-
|
| 343 |
-
gr.Markdown("""
|
| 344 |
-
---
|
| 345 |
-
|
| 346 |
-
## ✨ Features
|
| 347 |
-
|
| 348 |
-
- 🎤 **Speech Recognition** - Powered by Meta's SeamlessM4T v2 Large
|
| 349 |
-
- 🌍 **Translation** - 200+ languages with NLLB-200
|
| 350 |
-
- 🔊 **Natural Voice** - Fish Audio S1 TTS
|
| 351 |
-
- 🎭 **Voice Cloning** - Hear translation in your voice
|
| 352 |
-
|
| 353 |
-
## 🌍 Popular Languages
|
| 354 |
-
|
| 355 |
-
🇲🇦 Moroccan Darija • 🇸🇦 Arabic • 🇫🇷 French • 🇬🇧 English • 🇪🇸 Spanish • 🇩🇪 German • 🇮🇹 Italian • 🇵🇹 Portuguese • 🇨🇳 Chinese • 🇯🇵 Japanese • 🇰🇷 Korean • 🇷🇺 Russian
|
| 356 |
-
|
| 357 |
-
---
|
| 358 |
-
|
| 359 |
-
<div style="text-align: center; padding: 20px;">
|
| 360 |
-
<p style="color: #666;">Made with ❤️ using Meta AI • Powered by HuggingFace</p>
|
| 361 |
-
</div>
|
| 362 |
-
""")
|
| 363 |
-
|
| 364 |
-
if __name__ == "__main__":
|
| 365 |
-
demo.launch()
|
|
|
|
| 1 |
+
from flask import Flask, render_template, request, jsonify
|
| 2 |
from transformers import (
|
| 3 |
AutoProcessor,
|
| 4 |
SeamlessM4Tv2ForSpeechToText,
|
|
|
|
| 7 |
)
|
| 8 |
import torch
|
| 9 |
import numpy as np
|
| 10 |
+
import base64
|
| 11 |
+
import io
|
| 12 |
import requests
|
| 13 |
import os
|
| 14 |
|
| 15 |
+
app = Flask(__name__)
|
| 16 |
+
|
| 17 |
# ============================================================
|
| 18 |
# Device Setup
|
| 19 |
# ============================================================
|
|
|
|
| 38 |
nllb_model = nllb_model.to(device).eval()
|
| 39 |
print("✅ NLLB-200 loaded!")
|
| 40 |
|
| 41 |
+
# Fish Audio API
|
| 42 |
+
FISH_AUDIO_API_KEY = os.environ.get('FISH_AUDIO_API_KEY', '')
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 43 |
|
| 44 |
+
# Language mapping
|
| 45 |
+
LANG_MAP = {
|
| 46 |
+
'ar-SA': ('arb', 'ary_Arab'),
|
| 47 |
+
'fr-FR': ('fra', 'fra_Latn'),
|
| 48 |
+
'en-US': ('eng', 'eng_Latn'),
|
| 49 |
+
'es-ES': ('spa', 'spa_Latn'),
|
| 50 |
+
'de-DE': ('deu', 'deu_Latn'),
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 51 |
}
|
| 52 |
|
| 53 |
+
@app.route('/')
|
| 54 |
+
def index():
|
| 55 |
+
return render_template('index.html')
|
|
|
|
|
|
|
| 56 |
|
| 57 |
+
@app.route('/process_audio', methods=['POST'])
|
| 58 |
+
def process_audio():
|
|
|
|
|
|
|
|
|
|
| 59 |
try:
|
| 60 |
+
data = request.json
|
| 61 |
+
audio_b64 = data.get('audio')
|
| 62 |
+
source_lang = data.get('source_lang', 'ar-SA')
|
| 63 |
+
target_lang = data.get('target_lang', 'en-US')
|
| 64 |
+
|
| 65 |
+
# Decode audio
|
| 66 |
+
audio_bytes = base64.b64decode(audio_b64)
|
| 67 |
+
|
| 68 |
+
# Convert to numpy
|
| 69 |
+
import scipy.io.wavfile as wavfile
|
| 70 |
+
sample_rate, audio_data = wavfile.read(io.BytesIO(audio_bytes))
|
| 71 |
+
audio_data = audio_data.astype(np.float32)
|
| 72 |
+
if np.abs(audio_data).max() > 1.0:
|
| 73 |
+
audio_data = audio_data / 32768.0
|
| 74 |
+
|
| 75 |
+
# STT
|
| 76 |
+
src_code = LANG_MAP.get(source_lang, ('eng', 'eng_Latn'))[0]
|
| 77 |
inputs = stt_processor(
|
| 78 |
audios=audio_data,
|
| 79 |
sampling_rate=sample_rate,
|
|
|
|
| 89 |
|
| 90 |
transcript = stt_processor.decode(output_tokens[0].tolist(), skip_special_tokens=True)
|
| 91 |
|
| 92 |
+
# Translation
|
| 93 |
+
src_nllb = LANG_MAP.get(source_lang, ('eng', 'eng_Latn'))[1]
|
| 94 |
+
tgt_nllb = LANG_MAP.get(target_lang, ('eng', 'eng_Latn'))[1]
|
| 95 |
|
| 96 |
nllb_tokenizer.src_lang = src_nllb
|
| 97 |
inputs = nllb_tokenizer(transcript, return_tensors="pt", padding=True, truncation=True, max_length=512).to(device)
|
|
|
|
| 108 |
|
| 109 |
translation = nllb_tokenizer.decode(outputs[0], skip_special_tokens=True)
|
| 110 |
|
| 111 |
+
# TTS
|
| 112 |
+
tts_audio_b64 = None
|
| 113 |
if FISH_AUDIO_API_KEY:
|
| 114 |
+
tts_audio = generate_tts(translation)
|
| 115 |
+
if tts_audio:
|
| 116 |
+
tts_audio_b64 = base64.b64encode(tts_audio).decode('utf-8')
|
| 117 |
+
|
| 118 |
+
return jsonify({
|
| 119 |
+
'success': True,
|
| 120 |
+
'original': transcript,
|
| 121 |
+
'translation': translation,
|
| 122 |
+
'audio': tts_audio_b64,
|
| 123 |
+
'source_lang': source_lang,
|
| 124 |
+
'target_lang': target_lang
|
| 125 |
+
})
|
|
|
|
|
|
|
|
|
|
| 126 |
|
| 127 |
except Exception as e:
|
| 128 |
+
return jsonify({'success': False, 'error': str(e)}), 500
|
| 129 |
|
| 130 |
+
def generate_tts(text):
|
| 131 |
"""Generate TTS using Fish Audio"""
|
| 132 |
if not FISH_AUDIO_API_KEY:
|
| 133 |
return None
|
| 134 |
|
| 135 |
try:
|
| 136 |
headers = {'Authorization': f'Bearer {FISH_AUDIO_API_KEY}'}
|
| 137 |
+
payload = {
|
| 138 |
+
'text': text,
|
| 139 |
+
'format': 'mp3',
|
| 140 |
+
'mp3_bitrate': 192,
|
| 141 |
+
}
|
| 142 |
+
|
| 143 |
+
response = requests.post(
|
| 144 |
+
'https://api.fish.audio/v1/tts',
|
| 145 |
+
headers=headers,
|
| 146 |
+
json=payload,
|
| 147 |
+
timeout=60
|
| 148 |
+
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 149 |
|
| 150 |
if response.status_code == 200:
|
| 151 |
+
return response.content
|
|
|
|
|
|
|
|
|
|
| 152 |
|
| 153 |
return None
|
| 154 |
except:
|
| 155 |
return None
|
| 156 |
|
| 157 |
+
if __name__ == '__main__':
|
| 158 |
+
app.run(host='0.0.0.0', port=7860)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
requirements.txt
CHANGED
|
@@ -1,9 +1,7 @@
|
|
| 1 |
-
|
|
|
|
| 2 |
torch>=2.0.0
|
| 3 |
-
torchaudio
|
| 4 |
-
sentencepiece
|
| 5 |
-
protobuf
|
| 6 |
-
gradio>=4.0.0
|
| 7 |
-
numpy
|
| 8 |
scipy
|
| 9 |
-
|
|
|
|
|
|
|
|
|
| 1 |
+
Flask==3.0.0
|
| 2 |
+
transformers>=4.30.0
|
| 3 |
torch>=2.0.0
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 4 |
scipy
|
| 5 |
+
numpy
|
| 6 |
+
requests
|
| 7 |
+
Werkzeug==3.0.0
|
static/enhanced.css
ADDED
|
@@ -0,0 +1,620 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
/* 🌑 BABEL x GROK - Absolute Dark Theme */
|
| 2 |
+
:root {
|
| 3 |
+
--bg-app: #000000;
|
| 4 |
+
--bg-sidebar: #000000;
|
| 5 |
+
--bg-main: #000000;
|
| 6 |
+
--bg-surface: #121212;
|
| 7 |
+
--bg-input: #1a1a1a;
|
| 8 |
+
--bg-input-hover: #222222;
|
| 9 |
+
|
| 10 |
+
--text-primary: #ffffff;
|
| 11 |
+
--text-secondary: #a3a3a3;
|
| 12 |
+
--text-tertiary: #525252;
|
| 13 |
+
|
| 14 |
+
--border-subtle: #27272a;
|
| 15 |
+
--border-active: #3f3f46;
|
| 16 |
+
|
| 17 |
+
--accent: #ffffff;
|
| 18 |
+
--accent-hover: #e5e5e5;
|
| 19 |
+
|
| 20 |
+
--recording: #ef4444;
|
| 21 |
+
--sidebar-width: 260px;
|
| 22 |
+
--input-max-width: 720px;
|
| 23 |
+
}
|
| 24 |
+
|
| 25 |
+
* {
|
| 26 |
+
box-sizing: border-box;
|
| 27 |
+
margin: 0;
|
| 28 |
+
padding: 0;
|
| 29 |
+
-webkit-tap-highlight-color: transparent;
|
| 30 |
+
}
|
| 31 |
+
|
| 32 |
+
body {
|
| 33 |
+
background-color: var(--bg-app);
|
| 34 |
+
color: var(--text-primary);
|
| 35 |
+
font-family: 'Inter', system-ui, -apple-system, sans-serif;
|
| 36 |
+
height: 100vh;
|
| 37 |
+
overflow: hidden;
|
| 38 |
+
}
|
| 39 |
+
|
| 40 |
+
/* 🏗️ LAYOUT GRID */
|
| 41 |
+
.app-layout {
|
| 42 |
+
display: flex;
|
| 43 |
+
height: 100vh;
|
| 44 |
+
width: 100vw;
|
| 45 |
+
}
|
| 46 |
+
|
| 47 |
+
/* ▌ SIDEBAR */
|
| 48 |
+
.grok-sidebar {
|
| 49 |
+
width: var(--sidebar-width);
|
| 50 |
+
background-color: var(--bg-sidebar);
|
| 51 |
+
border-right: 1px solid var(--border-subtle);
|
| 52 |
+
display: flex;
|
| 53 |
+
flex-direction: column;
|
| 54 |
+
padding: 16px;
|
| 55 |
+
flex-shrink: 0;
|
| 56 |
+
transition: transform 0.3s ease;
|
| 57 |
+
z-index: 50;
|
| 58 |
+
}
|
| 59 |
+
|
| 60 |
+
.sidebar-top {
|
| 61 |
+
display: flex;
|
| 62 |
+
justify-content: space-between;
|
| 63 |
+
align-items: center;
|
| 64 |
+
margin-bottom: 24px;
|
| 65 |
+
padding: 0 8px;
|
| 66 |
+
}
|
| 67 |
+
|
| 68 |
+
.sidebar-logo {
|
| 69 |
+
font-size: 1.5rem;
|
| 70 |
+
font-weight: 700;
|
| 71 |
+
color: var(--text-primary);
|
| 72 |
+
}
|
| 73 |
+
|
| 74 |
+
.new-chat {
|
| 75 |
+
background: transparent;
|
| 76 |
+
border: none;
|
| 77 |
+
color: var(--text-secondary);
|
| 78 |
+
font-size: 1.2rem;
|
| 79 |
+
cursor: pointer;
|
| 80 |
+
padding: 8px;
|
| 81 |
+
border-radius: 8px;
|
| 82 |
+
transition: all 0.2s;
|
| 83 |
+
}
|
| 84 |
+
|
| 85 |
+
.new-chat:hover {
|
| 86 |
+
background: var(--bg-surface);
|
| 87 |
+
color: var(--text-primary);
|
| 88 |
+
}
|
| 89 |
+
|
| 90 |
+
.sidebar-menu {
|
| 91 |
+
display: flex;
|
| 92 |
+
flex-direction: column;
|
| 93 |
+
gap: 4px;
|
| 94 |
+
flex: 1;
|
| 95 |
+
}
|
| 96 |
+
|
| 97 |
+
.menu-item {
|
| 98 |
+
display: flex;
|
| 99 |
+
align-items: center;
|
| 100 |
+
gap: 12px;
|
| 101 |
+
padding: 10px 12px;
|
| 102 |
+
border-radius: 8px;
|
| 103 |
+
color: var(--text-secondary);
|
| 104 |
+
cursor: pointer;
|
| 105 |
+
transition: all 0.2s;
|
| 106 |
+
font-size: 0.95rem;
|
| 107 |
+
font-weight: 500;
|
| 108 |
+
}
|
| 109 |
+
|
| 110 |
+
.menu-item:hover,
|
| 111 |
+
.menu-item.active {
|
| 112 |
+
background-color: var(--bg-surface);
|
| 113 |
+
color: var(--text-primary);
|
| 114 |
+
}
|
| 115 |
+
|
| 116 |
+
.menu-item i {
|
| 117 |
+
width: 20px;
|
| 118 |
+
text-align: center;
|
| 119 |
+
}
|
| 120 |
+
|
| 121 |
+
.sidebar-footer {
|
| 122 |
+
padding-top: 16px;
|
| 123 |
+
border-top: 1px solid var(--border-subtle);
|
| 124 |
+
}
|
| 125 |
+
|
| 126 |
+
.user-profile {
|
| 127 |
+
display: flex;
|
| 128 |
+
align-items: center;
|
| 129 |
+
gap: 10px;
|
| 130 |
+
padding: 8px;
|
| 131 |
+
border-radius: 8px;
|
| 132 |
+
cursor: pointer;
|
| 133 |
+
}
|
| 134 |
+
|
| 135 |
+
.user-profile:hover {
|
| 136 |
+
background-color: var(--bg-surface);
|
| 137 |
+
}
|
| 138 |
+
|
| 139 |
+
.avatar {
|
| 140 |
+
width: 32px;
|
| 141 |
+
height: 32px;
|
| 142 |
+
background: #333;
|
| 143 |
+
border-radius: 50%;
|
| 144 |
+
display: flex;
|
| 145 |
+
align-items: center;
|
| 146 |
+
justify-content: center;
|
| 147 |
+
font-size: 0.9rem;
|
| 148 |
+
font-weight: 600;
|
| 149 |
+
}
|
| 150 |
+
|
| 151 |
+
.info {
|
| 152 |
+
display: flex;
|
| 153 |
+
flex-direction: column;
|
| 154 |
+
}
|
| 155 |
+
|
| 156 |
+
.info .name {
|
| 157 |
+
font-size: 0.9rem;
|
| 158 |
+
font-weight: 600;
|
| 159 |
+
}
|
| 160 |
+
|
| 161 |
+
.info .status {
|
| 162 |
+
font-size: 0.75rem;
|
| 163 |
+
color: var(--text-secondary);
|
| 164 |
+
}
|
| 165 |
+
|
| 166 |
+
/* ▌ MAIN CONTENT */
|
| 167 |
+
.grok-main {
|
| 168 |
+
flex: 1;
|
| 169 |
+
display: flex;
|
| 170 |
+
flex-direction: column;
|
| 171 |
+
position: relative;
|
| 172 |
+
background-color: var(--bg-main);
|
| 173 |
+
}
|
| 174 |
+
|
| 175 |
+
.main-header {
|
| 176 |
+
height: 60px;
|
| 177 |
+
display: flex;
|
| 178 |
+
align-items: center;
|
| 179 |
+
justify-content: center;
|
| 180 |
+
border-bottom: 1px solid transparent;
|
| 181 |
+
/* Hidden unless scrolled */
|
| 182 |
+
}
|
| 183 |
+
|
| 184 |
+
.model-badge {
|
| 185 |
+
color: var(--text-secondary);
|
| 186 |
+
font-size: 0.9rem;
|
| 187 |
+
display: flex;
|
| 188 |
+
align-items: center;
|
| 189 |
+
gap: 6px;
|
| 190 |
+
opacity: 0.7;
|
| 191 |
+
}
|
| 192 |
+
|
| 193 |
+
/* CHAT STAGE */
|
| 194 |
+
.chat-stage {
|
| 195 |
+
flex: 1;
|
| 196 |
+
overflow-y: auto;
|
| 197 |
+
padding: 20px;
|
| 198 |
+
padding-bottom: 180px;
|
| 199 |
+
/* Space for input bar */
|
| 200 |
+
display: flex;
|
| 201 |
+
flex-direction: column;
|
| 202 |
+
align-items: center;
|
| 203 |
+
}
|
| 204 |
+
|
| 205 |
+
.chat-container {
|
| 206 |
+
width: 100%;
|
| 207 |
+
max-width: var(--input-max-width);
|
| 208 |
+
display: flex;
|
| 209 |
+
flex-direction: column;
|
| 210 |
+
gap: 20px;
|
| 211 |
+
}
|
| 212 |
+
|
| 213 |
+
/* EMPTY STATE */
|
| 214 |
+
.empty-state {
|
| 215 |
+
display: flex;
|
| 216 |
+
flex-direction: column;
|
| 217 |
+
align-items: center;
|
| 218 |
+
justify-content: center;
|
| 219 |
+
margin-top: 10vh;
|
| 220 |
+
animation: fadeIn 0.5s ease;
|
| 221 |
+
}
|
| 222 |
+
|
| 223 |
+
.grok-logo-hero {
|
| 224 |
+
font-size: 4rem;
|
| 225 |
+
font-weight: 700;
|
| 226 |
+
color: var(--text-primary);
|
| 227 |
+
margin-bottom: 16px;
|
| 228 |
+
letter-spacing: -0.04em;
|
| 229 |
+
}
|
| 230 |
+
|
| 231 |
+
.grok-hero-text {
|
| 232 |
+
color: var(--text-secondary);
|
| 233 |
+
font-size: 1.2rem;
|
| 234 |
+
}
|
| 235 |
+
|
| 236 |
+
/* 🟢 INPUT AREA (The "Grok" Bar) */
|
| 237 |
+
.grok-input-wrapper {
|
| 238 |
+
position: absolute;
|
| 239 |
+
bottom: 0;
|
| 240 |
+
left: 0;
|
| 241 |
+
right: 0;
|
| 242 |
+
padding: 24px;
|
| 243 |
+
padding-bottom: 40px;
|
| 244 |
+
display: flex;
|
| 245 |
+
flex-direction: column;
|
| 246 |
+
align-items: center;
|
| 247 |
+
gap: 16px;
|
| 248 |
+
background: linear-gradient(to top, var(--bg-main) 80%, transparent);
|
| 249 |
+
}
|
| 250 |
+
|
| 251 |
+
.grok-input-bar {
|
| 252 |
+
width: 100%;
|
| 253 |
+
max-width: var(--input-max-width);
|
| 254 |
+
background-color: var(--bg-input);
|
| 255 |
+
border-radius: 28px;
|
| 256 |
+
/* Pill shape */
|
| 257 |
+
padding: 12px 16px;
|
| 258 |
+
display: flex;
|
| 259 |
+
align-items: center;
|
| 260 |
+
gap: 16px;
|
| 261 |
+
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.4);
|
| 262 |
+
border: 1px solid var(--border-subtle);
|
| 263 |
+
transition: all 0.2s;
|
| 264 |
+
min-height: 64px;
|
| 265 |
+
}
|
| 266 |
+
|
| 267 |
+
.grok-input-bar:focus-within {
|
| 268 |
+
border-color: var(--border-active);
|
| 269 |
+
background-color: var(--bg-input-hover);
|
| 270 |
+
}
|
| 271 |
+
|
| 272 |
+
.input-icon {
|
| 273 |
+
font-size: 1.2rem;
|
| 274 |
+
color: var(--text-secondary);
|
| 275 |
+
cursor: pointer;
|
| 276 |
+
width: 40px;
|
| 277 |
+
height: 40px;
|
| 278 |
+
display: flex;
|
| 279 |
+
align-items: center;
|
| 280 |
+
justify-content: center;
|
| 281 |
+
border-radius: 50%;
|
| 282 |
+
transition: all 0.2s;
|
| 283 |
+
}
|
| 284 |
+
|
| 285 |
+
.input-icon:hover {
|
| 286 |
+
background: rgba(255, 255, 255, 0.05);
|
| 287 |
+
color: var(--text-primary);
|
| 288 |
+
}
|
| 289 |
+
|
| 290 |
+
.input-status-area {
|
| 291 |
+
flex: 1;
|
| 292 |
+
display: flex;
|
| 293 |
+
flex-direction: column;
|
| 294 |
+
justify-content: center;
|
| 295 |
+
}
|
| 296 |
+
|
| 297 |
+
.status-text {
|
| 298 |
+
color: var(--text-secondary);
|
| 299 |
+
font-size: 1.1rem;
|
| 300 |
+
font-weight: 400;
|
| 301 |
+
}
|
| 302 |
+
|
| 303 |
+
/* ORB ACTION BUTTON */
|
| 304 |
+
.input-icon.action {
|
| 305 |
+
background-color: transparent;
|
| 306 |
+
color: var(--text-primary);
|
| 307 |
+
width: 48px;
|
| 308 |
+
height: 48px;
|
| 309 |
+
}
|
| 310 |
+
|
| 311 |
+
.input-icon.action:hover {
|
| 312 |
+
background-color: rgba(255, 255, 255, 0.1);
|
| 313 |
+
}
|
| 314 |
+
|
| 315 |
+
.input-icon.action.recording {
|
| 316 |
+
color: var(--recording);
|
| 317 |
+
animation: pulse-red 1.5s infinite;
|
| 318 |
+
}
|
| 319 |
+
|
| 320 |
+
/* PRO CHIPS (Below Input) */
|
| 321 |
+
.grok-chips {
|
| 322 |
+
display: flex;
|
| 323 |
+
align-items: center;
|
| 324 |
+
gap: 10px;
|
| 325 |
+
flex-wrap: wrap;
|
| 326 |
+
justify-content: center;
|
| 327 |
+
}
|
| 328 |
+
|
| 329 |
+
.chip-select-wrapper {
|
| 330 |
+
position: relative;
|
| 331 |
+
background: var(--bg-surface);
|
| 332 |
+
border-radius: 8px;
|
| 333 |
+
padding: 0 12px;
|
| 334 |
+
height: 36px;
|
| 335 |
+
display: flex;
|
| 336 |
+
align-items: center;
|
| 337 |
+
border: 1px solid var(--border-subtle);
|
| 338 |
+
min-width: 100px;
|
| 339 |
+
}
|
| 340 |
+
|
| 341 |
+
.chip-select-wrapper i {
|
| 342 |
+
font-size: 0.7rem;
|
| 343 |
+
color: var(--text-secondary);
|
| 344 |
+
pointer-events: none;
|
| 345 |
+
margin-left: 8px;
|
| 346 |
+
}
|
| 347 |
+
|
| 348 |
+
.chip-select {
|
| 349 |
+
appearance: none;
|
| 350 |
+
background: transparent;
|
| 351 |
+
border: none;
|
| 352 |
+
color: var(--text-primary);
|
| 353 |
+
font-size: 0.9rem;
|
| 354 |
+
font-family: inherit;
|
| 355 |
+
width: 100%;
|
| 356 |
+
cursor: pointer;
|
| 357 |
+
}
|
| 358 |
+
|
| 359 |
+
.chip-select option {
|
| 360 |
+
background: #121212;
|
| 361 |
+
color: white;
|
| 362 |
+
}
|
| 363 |
+
|
| 364 |
+
.chip-icon {
|
| 365 |
+
width: 36px;
|
| 366 |
+
height: 36px;
|
| 367 |
+
border-radius: 50%;
|
| 368 |
+
border: 1px solid var(--border-subtle);
|
| 369 |
+
background: var(--bg-surface);
|
| 370 |
+
color: var(--text-secondary);
|
| 371 |
+
display: flex;
|
| 372 |
+
align-items: center;
|
| 373 |
+
justify-content: center;
|
| 374 |
+
cursor: pointer;
|
| 375 |
+
font-size: 0.9rem;
|
| 376 |
+
}
|
| 377 |
+
|
| 378 |
+
.chip-pill {
|
| 379 |
+
height: 36px;
|
| 380 |
+
padding: 0 16px;
|
| 381 |
+
border-radius: 18px;
|
| 382 |
+
background: transparent;
|
| 383 |
+
border: 1px solid var(--border-subtle);
|
| 384 |
+
color: var(--text-secondary);
|
| 385 |
+
font-size: 0.9rem;
|
| 386 |
+
font-weight: 500;
|
| 387 |
+
cursor: pointer;
|
| 388 |
+
display: flex;
|
| 389 |
+
align-items: center;
|
| 390 |
+
gap: 8px;
|
| 391 |
+
transition: all 0.2s;
|
| 392 |
+
}
|
| 393 |
+
|
| 394 |
+
.chip-pill:hover {
|
| 395 |
+
border-color: var(--text-secondary);
|
| 396 |
+
color: var(--text-primary);
|
| 397 |
+
}
|
| 398 |
+
|
| 399 |
+
.chip-pill.active {
|
| 400 |
+
background: var(--text-primary);
|
| 401 |
+
color: var(--bg-app);
|
| 402 |
+
border-color: var(--text-primary);
|
| 403 |
+
}
|
| 404 |
+
|
| 405 |
+
.chip-divider {
|
| 406 |
+
width: 1px;
|
| 407 |
+
height: 20px;
|
| 408 |
+
background: var(--border-subtle);
|
| 409 |
+
margin: 0 8px;
|
| 410 |
+
}
|
| 411 |
+
|
| 412 |
+
/* MESSAGES */
|
| 413 |
+
.message-row {
|
| 414 |
+
display: flex;
|
| 415 |
+
width: 100%;
|
| 416 |
+
margin-bottom: 24px;
|
| 417 |
+
}
|
| 418 |
+
|
| 419 |
+
.user-row {
|
| 420 |
+
justify-content: flex-end;
|
| 421 |
+
}
|
| 422 |
+
|
| 423 |
+
.ai-row {
|
| 424 |
+
justify-content: flex-start;
|
| 425 |
+
}
|
| 426 |
+
|
| 427 |
+
.bubble {
|
| 428 |
+
max-width: 80%;
|
| 429 |
+
padding: 12px 18px;
|
| 430 |
+
border-radius: 18px;
|
| 431 |
+
font-size: 1rem;
|
| 432 |
+
line-height: 1.5;
|
| 433 |
+
}
|
| 434 |
+
|
| 435 |
+
.user-bubble {
|
| 436 |
+
background: var(--bg-surface);
|
| 437 |
+
color: var(--text-primary);
|
| 438 |
+
border: 1px solid var(--border-subtle);
|
| 439 |
+
}
|
| 440 |
+
|
| 441 |
+
.ai-bubble {
|
| 442 |
+
background: transparent;
|
| 443 |
+
color: var(--text-primary);
|
| 444 |
+
padding-left: 0;
|
| 445 |
+
}
|
| 446 |
+
|
| 447 |
+
/* BARS ANIMATION for Orb */
|
| 448 |
+
.bars {
|
| 449 |
+
display: flex;
|
| 450 |
+
align-items: center;
|
| 451 |
+
justify-content: center;
|
| 452 |
+
gap: 3px;
|
| 453 |
+
height: 20px;
|
| 454 |
+
}
|
| 455 |
+
|
| 456 |
+
.bar {
|
| 457 |
+
width: 3px;
|
| 458 |
+
height: 100%;
|
| 459 |
+
background: currentColor;
|
| 460 |
+
border-radius: 2px;
|
| 461 |
+
}
|
| 462 |
+
|
| 463 |
+
@keyframes pulse-red {
|
| 464 |
+
0% {
|
| 465 |
+
transform: scale(1);
|
| 466 |
+
opacity: 1;
|
| 467 |
+
}
|
| 468 |
+
|
| 469 |
+
50% {
|
| 470 |
+
transform: scale(1.1);
|
| 471 |
+
opacity: 0.8;
|
| 472 |
+
}
|
| 473 |
+
|
| 474 |
+
100% {
|
| 475 |
+
transform: scale(1);
|
| 476 |
+
opacity: 1;
|
| 477 |
+
}
|
| 478 |
+
}
|
| 479 |
+
|
| 480 |
+
/* 📱 MOBILE RESPONSIVE */
|
| 481 |
+
@media (max-width: 768px) {
|
| 482 |
+
.grok-sidebar {
|
| 483 |
+
position: fixed;
|
| 484 |
+
left: -100%;
|
| 485 |
+
/* Hide */
|
| 486 |
+
height: 100%;
|
| 487 |
+
background: rgba(0, 0, 0, 0.95);
|
| 488 |
+
backdrop-filter: blur(10px);
|
| 489 |
+
}
|
| 490 |
+
|
| 491 |
+
.grok-sidebar.open {
|
| 492 |
+
left: 0;
|
| 493 |
+
}
|
| 494 |
+
|
| 495 |
+
.main-header {
|
| 496 |
+
justify-content: flex-start;
|
| 497 |
+
padding-left: 20px;
|
| 498 |
+
}
|
| 499 |
+
|
| 500 |
+
.grok-input-bar {
|
| 501 |
+
border-radius: 24px;
|
| 502 |
+
}
|
| 503 |
+
|
| 504 |
+
.chip-pill span {
|
| 505 |
+
display: none;
|
| 506 |
+
/* Icon only on mobile */
|
| 507 |
+
}
|
| 508 |
+
}
|
| 509 |
+
/* MODAL SETTINGS (Grok Dark) */
|
| 510 |
+
.modal {
|
| 511 |
+
position: fixed;
|
| 512 |
+
inset: 0;
|
| 513 |
+
background: rgba(0,0,0,0.8);
|
| 514 |
+
display: none; /* JS toggles display: flex */
|
| 515 |
+
align-items: center;
|
| 516 |
+
justify-content: center;
|
| 517 |
+
z-index: 1000;
|
| 518 |
+
}
|
| 519 |
+
|
| 520 |
+
.modal-content {
|
| 521 |
+
background: #121212;
|
| 522 |
+
border: 1px solid #333;
|
| 523 |
+
border-radius: 16px;
|
| 524 |
+
padding: 24px;
|
| 525 |
+
width: 90%;
|
| 526 |
+
max-width: 500px;
|
| 527 |
+
color: white;
|
| 528 |
+
position: relative;
|
| 529 |
+
box-shadow: 0 20px 50px rgba(0,0,0,0.5);
|
| 530 |
+
}
|
| 531 |
+
|
| 532 |
+
.modal h2 { margin-bottom: 20px; font-size: 1.5rem; }
|
| 533 |
+
|
| 534 |
+
.form-group { margin-bottom: 16px; }
|
| 535 |
+
|
| 536 |
+
.form-group label {
|
| 537 |
+
display: block;
|
| 538 |
+
margin-bottom: 8px;
|
| 539 |
+
color: #a3a3a3;
|
| 540 |
+
font-size: 0.9rem;
|
| 541 |
+
}
|
| 542 |
+
|
| 543 |
+
.form-control {
|
| 544 |
+
width: 100%;
|
| 545 |
+
background: #1a1a1a;
|
| 546 |
+
border: 1px solid #333;
|
| 547 |
+
padding: 12px;
|
| 548 |
+
border-radius: 8px;
|
| 549 |
+
color: white;
|
| 550 |
+
font-size: 1rem;
|
| 551 |
+
}
|
| 552 |
+
|
| 553 |
+
.form-control:focus {
|
| 554 |
+
border-color: white;
|
| 555 |
+
outline: none;
|
| 556 |
+
}
|
| 557 |
+
|
| 558 |
+
.save-btn {
|
| 559 |
+
width: 100%;
|
| 560 |
+
background: white;
|
| 561 |
+
color: black;
|
| 562 |
+
font-weight: 700;
|
| 563 |
+
padding: 12px;
|
| 564 |
+
border-radius: 8px;
|
| 565 |
+
border: none;
|
| 566 |
+
cursor: pointer;
|
| 567 |
+
margin-top: 16px;
|
| 568 |
+
}
|
| 569 |
+
|
| 570 |
+
.close-modal-btn {
|
| 571 |
+
position: absolute;
|
| 572 |
+
top: 16px;
|
| 573 |
+
right: 16px;
|
| 574 |
+
background: transparent;
|
| 575 |
+
border: none;
|
| 576 |
+
color: #666;
|
| 577 |
+
font-size: 1.2rem;
|
| 578 |
+
cursor: pointer;
|
| 579 |
+
}
|
| 580 |
+
.close-modal-btn:hover { color: white; }
|
| 581 |
+
|
| 582 |
+
|
| 583 |
+
|
| 584 |
+
/* MICROPHONE ORB BUTTON */
|
| 585 |
+
#record-btn {
|
| 586 |
+
width: 56px;
|
| 587 |
+
height: 56px;
|
| 588 |
+
border-radius: 50%;
|
| 589 |
+
background: linear-gradient(135deg, #1a1a1a, #2d2d2d);
|
| 590 |
+
border: 2px solid #444;
|
| 591 |
+
cursor: pointer;
|
| 592 |
+
transition: all 0.3s ease;
|
| 593 |
+
}
|
| 594 |
+
|
| 595 |
+
#record-btn i {
|
| 596 |
+
font-size: 1.4rem;
|
| 597 |
+
color: #ffffff;
|
| 598 |
+
}
|
| 599 |
+
|
| 600 |
+
#record-btn:hover {
|
| 601 |
+
background: linear-gradient(135deg, #2d2d2d, #3d3d3d);
|
| 602 |
+
border-color: #666;
|
| 603 |
+
transform: scale(1.05);
|
| 604 |
+
}
|
| 605 |
+
|
| 606 |
+
#record-btn.recording {
|
| 607 |
+
background: linear-gradient(135deg, #dc2626, #ef4444) !important;
|
| 608 |
+
border-color: #f87171 !important;
|
| 609 |
+
animation: pulse-record 1.5s infinite;
|
| 610 |
+
}
|
| 611 |
+
|
| 612 |
+
#record-btn.recording i {
|
| 613 |
+
color: white;
|
| 614 |
+
}
|
| 615 |
+
|
| 616 |
+
@keyframes pulse-record {
|
| 617 |
+
0%, 100% { transform: scale(1); box-shadow: 0 0 0 0 rgba(239, 68, 68, 0.4); }
|
| 618 |
+
50% { transform: scale(1.05); box-shadow: 0 0 20px 5px rgba(239, 68, 68, 0.3); }
|
| 619 |
+
}
|
| 620 |
+
|
static/enhanced.js
ADDED
|
@@ -0,0 +1,217 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
// ENHANCED UI - Darija Cards + Audio Player Manager
|
| 2 |
+
|
| 3 |
+
// Parse Darija text into structured cards
|
| 4 |
+
function parseDarijaToCards(text) {
|
| 5 |
+
// Check if text contains Darija labels
|
| 6 |
+
const hasArabicLabel = text.includes('🇲🇦') || text.includes('الدارجة');
|
| 7 |
+
const hasArabiziLabel = text.includes('🔤') || text.includes('Darija (Arabizi)');
|
| 8 |
+
|
| 9 |
+
if (!hasArabicLabel && !hasArabiziLabel) {
|
| 10 |
+
// Regular text, return as-is
|
| 11 |
+
return `<div class="regular-text">${text.replace(/\n/g, '<br>')}</div>`;
|
| 12 |
+
}
|
| 13 |
+
|
| 14 |
+
// Split by the labels
|
| 15 |
+
const parts = text.split(/(?=🇲🇦)|(?=🔤)/);
|
| 16 |
+
|
| 17 |
+
let html = '<div class="darija-container">';
|
| 18 |
+
|
| 19 |
+
for (const part of parts) {
|
| 20 |
+
const trimmed = part.trim();
|
| 21 |
+
if (!trimmed) continue;
|
| 22 |
+
|
| 23 |
+
if (trimmed.startsWith('🇲🇦') || trimmed.includes('الدارجة')) {
|
| 24 |
+
// Arabic card
|
| 25 |
+
const content = trimmed
|
| 26 |
+
.replace(/🇲🇦.*?\n/, '')
|
| 27 |
+
.replace(/الدارجة.*?\n/, '')
|
| 28 |
+
.trim();
|
| 29 |
+
|
| 30 |
+
html += `
|
| 31 |
+
<div class="darija-card arabic">
|
| 32 |
+
<div class="darija-card-header">
|
| 33 |
+
<span class="emoji">🇲🇦</span>
|
| 34 |
+
<span>الدارجة (Arabic)</span>
|
| 35 |
+
</div>
|
| 36 |
+
<div class="darija-card-content">${content}</div>
|
| 37 |
+
</div>
|
| 38 |
+
`;
|
| 39 |
+
} else if (trimmed.startsWith('🔤') || trimmed.includes('Darija (Arabizi)')) {
|
| 40 |
+
// Arabizi card
|
| 41 |
+
const content = trimmed
|
| 42 |
+
.replace(/🔤.*?\n/, '')
|
| 43 |
+
.replace(/Darija.*?\n/, '')
|
| 44 |
+
.trim();
|
| 45 |
+
|
| 46 |
+
html += `
|
| 47 |
+
<div class="darija-card arabizi">
|
| 48 |
+
<div class="darija-card-header">
|
| 49 |
+
<span class="emoji">🔤</span>
|
| 50 |
+
<span>Darija (Arabizi)</span>
|
| 51 |
+
</div>
|
| 52 |
+
<div class="darija-card-content">${content}</div>
|
| 53 |
+
</div>
|
| 54 |
+
`;
|
| 55 |
+
}
|
| 56 |
+
}
|
| 57 |
+
|
| 58 |
+
html += '</div>';
|
| 59 |
+
return html;
|
| 60 |
+
}
|
| 61 |
+
|
| 62 |
+
// Audio Player Manager
|
| 63 |
+
class AudioPlayer {
|
| 64 |
+
constructor() {
|
| 65 |
+
// Create a DEDICATED audio element for enhanced player
|
| 66 |
+
this.audio = new Audio();
|
| 67 |
+
this.audio.id = 'enhanced-audio-element';
|
| 68 |
+
|
| 69 |
+
this.container = null;
|
| 70 |
+
this.playBtn = null;
|
| 71 |
+
this.waveform = null;
|
| 72 |
+
this.timeDisplay = null;
|
| 73 |
+
this.isPlaying = false;
|
| 74 |
+
this.currentAudioSrc = null;
|
| 75 |
+
|
| 76 |
+
this.createPlayer();
|
| 77 |
+
this.setupEventListeners();
|
| 78 |
+
}
|
| 79 |
+
|
| 80 |
+
createPlayer() {
|
| 81 |
+
// Create player HTML
|
| 82 |
+
const playerHTML = `
|
| 83 |
+
<div class="audio-player-container" id="enhanced-audio-player" style="display: none;">
|
| 84 |
+
<div class="audio-player">
|
| 85 |
+
<div class="player-controls">
|
| 86 |
+
<button class="play-btn" id="player-play-btn">
|
| 87 |
+
<i class="fa-solid fa-play"></i>
|
| 88 |
+
</button>
|
| 89 |
+
<div class="waveform" id="player-waveform">
|
| 90 |
+
<div class="wave-bar"></div>
|
| 91 |
+
<div class="wave-bar"></div>
|
| 92 |
+
<div class="wave-bar"></div>
|
| 93 |
+
<div class="wave-bar"></div>
|
| 94 |
+
<div class="wave-bar"></div>
|
| 95 |
+
</div>
|
| 96 |
+
</div>
|
| 97 |
+
<div class="player-actions">
|
| 98 |
+
<button class="action-btn" id="player-download-btn">
|
| 99 |
+
<i class="fa-solid fa-download"></i>
|
| 100 |
+
<span>Download</span>
|
| 101 |
+
</button>
|
| 102 |
+
<button class="action-btn" id="player-close-btn">
|
| 103 |
+
<i class="fa-solid fa-xmark"></i>
|
| 104 |
+
<span>Close</span>
|
| 105 |
+
</button>
|
| 106 |
+
</div>
|
| 107 |
+
<div class="time-display" id="player-time-display">0:00 / 0:00</div>
|
| 108 |
+
</div>
|
| 109 |
+
</div>
|
| 110 |
+
`;
|
| 111 |
+
|
| 112 |
+
// Insert into DOM
|
| 113 |
+
document.body.insertAdjacentHTML('beforeend', playerHTML);
|
| 114 |
+
|
| 115 |
+
// Get references
|
| 116 |
+
this.container = document.getElementById('enhanced-audio-player');
|
| 117 |
+
this.playBtn = document.getElementById('player-play-btn');
|
| 118 |
+
this.waveform = document.getElementById('player-waveform');
|
| 119 |
+
this.timeDisplay = document.getElementById('player-time-display');
|
| 120 |
+
|
| 121 |
+
document.getElementById('player-download-btn').onclick = () => this.download();
|
| 122 |
+
document.getElementById('player-close-btn').onclick = () => this.hide();
|
| 123 |
+
}
|
| 124 |
+
|
| 125 |
+
setupEventListeners() {
|
| 126 |
+
this.playBtn.onclick = () => this.togglePlay();
|
| 127 |
+
|
| 128 |
+
this.audio.addEventListener('play', () => {
|
| 129 |
+
this.isPlaying = true;
|
| 130 |
+
this.playBtn.innerHTML = '<i class="fa-solid fa-pause"></i>';
|
| 131 |
+
this.waveform.classList.add('playing');
|
| 132 |
+
});
|
| 133 |
+
|
| 134 |
+
this.audio.addEventListener('pause', () => {
|
| 135 |
+
this.isPlaying = false;
|
| 136 |
+
this.playBtn.innerHTML = '<i class="fa-solid fa-play"></i>';
|
| 137 |
+
this.waveform.classList.remove('playing');
|
| 138 |
+
});
|
| 139 |
+
|
| 140 |
+
this.audio.addEventListener('ended', () => {
|
| 141 |
+
this.isPlaying = false;
|
| 142 |
+
this.playBtn.innerHTML = '<i class="fa-solid fa-play"></i>';
|
| 143 |
+
this.waveform.classList.remove('playing');
|
| 144 |
+
});
|
| 145 |
+
|
| 146 |
+
this.audio.addEventListener('timeupdate', () => {
|
| 147 |
+
this.updateTimeDisplay();
|
| 148 |
+
});
|
| 149 |
+
}
|
| 150 |
+
|
| 151 |
+
show(audioSrc) {
|
| 152 |
+
if (audioSrc) {
|
| 153 |
+
this.currentAudioSrc = audioSrc;
|
| 154 |
+
this.audio.src = audioSrc;
|
| 155 |
+
}
|
| 156 |
+
|
| 157 |
+
this.container.style.display = 'block';
|
| 158 |
+
setTimeout(() => {
|
| 159 |
+
this.container.classList.add('visible');
|
| 160 |
+
}, 10);
|
| 161 |
+
|
| 162 |
+
// Auto-play
|
| 163 |
+
this.audio.play().catch(err => {
|
| 164 |
+
console.log('Auto-play blocked:', err);
|
| 165 |
+
});
|
| 166 |
+
}
|
| 167 |
+
|
| 168 |
+
hide() {
|
| 169 |
+
this.container.classList.remove('visible');
|
| 170 |
+
setTimeout(() => {
|
| 171 |
+
this.container.style.display = 'none';
|
| 172 |
+
}, 300);
|
| 173 |
+
this.audio.pause();
|
| 174 |
+
}
|
| 175 |
+
|
| 176 |
+
togglePlay() {
|
| 177 |
+
if (this.isPlaying) {
|
| 178 |
+
this.audio.pause();
|
| 179 |
+
} else {
|
| 180 |
+
this.audio.play();
|
| 181 |
+
}
|
| 182 |
+
}
|
| 183 |
+
|
| 184 |
+
updateTimeDisplay() {
|
| 185 |
+
const current = this.formatTime(this.audio.currentTime);
|
| 186 |
+
const duration = this.formatTime(this.audio.duration || 0);
|
| 187 |
+
this.timeDisplay.textContent = `${current} / ${duration}`;
|
| 188 |
+
}
|
| 189 |
+
|
| 190 |
+
formatTime(seconds) {
|
| 191 |
+
if (isNaN(seconds)) return '0:00';
|
| 192 |
+
const mins = Math.floor(seconds / 60);
|
| 193 |
+
const secs = Math.floor(seconds % 60);
|
| 194 |
+
return `${mins}:${secs.toString().padStart(2, '0')}`;
|
| 195 |
+
}
|
| 196 |
+
|
| 197 |
+
download() {
|
| 198 |
+
if (!this.currentAudioSrc) return;
|
| 199 |
+
|
| 200 |
+
const a = document.createElement('a');
|
| 201 |
+
a.href = this.currentAudioSrc;
|
| 202 |
+
a.download = `translation_${Date.now()}.mp3`;
|
| 203 |
+
document.body.appendChild(a);
|
| 204 |
+
a.click();
|
| 205 |
+
document.body.removeChild(a);
|
| 206 |
+
}
|
| 207 |
+
}
|
| 208 |
+
|
| 209 |
+
// Initialize on load
|
| 210 |
+
window.enhancedAudioPlayer = null;
|
| 211 |
+
|
| 212 |
+
document.addEventListener('DOMContentLoaded', () => {
|
| 213 |
+
window.enhancedAudioPlayer = new AudioPlayer();
|
| 214 |
+
});
|
| 215 |
+
|
| 216 |
+
// Export functions
|
| 217 |
+
window.parseDarijaToCards = parseDarijaToCards;
|
static/favicon.ico
ADDED
|
|
static/manifest.json
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"name": "Babel - Traducteur Vocal Instantané",
|
| 3 |
+
"short_name": "Babel",
|
| 4 |
+
"description": "Traduction vocale instantanée avec IA. Parlez, et le monde vous comprend.",
|
| 5 |
+
"start_url": "/",
|
| 6 |
+
"display": "standalone",
|
| 7 |
+
"background_color": "#0a0a0b",
|
| 8 |
+
"theme_color": "#0a0a0b",
|
| 9 |
+
"orientation": "portrait",
|
| 10 |
+
"icons": [
|
| 11 |
+
{
|
| 12 |
+
"src": "/static/favicon.ico",
|
| 13 |
+
"sizes": "64x64",
|
| 14 |
+
"type": "image/x-icon"
|
| 15 |
+
}
|
| 16 |
+
]
|
| 17 |
+
}
|
static/script.js
ADDED
|
@@ -0,0 +1,1982 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
// Babel - Main Script (Cleaned & Optimized)
|
| 2 |
+
|
| 3 |
+
let mediaRecorder;
|
| 4 |
+
let audioChunks = [];
|
| 5 |
+
let isRecording = false;
|
| 6 |
+
let audioContext;
|
| 7 |
+
let analyser;
|
| 8 |
+
let micSource;
|
| 9 |
+
let animationId;
|
| 10 |
+
let recognition;
|
| 11 |
+
let streamTimeout;
|
| 12 |
+
let globalStream = null; // 🎤 PERSISTENT MIC STREAM
|
| 13 |
+
let isRestarting = false; // Prevent double restart logic
|
| 14 |
+
let isProcessingAudio = false; // Prevent duplicate audio processing
|
| 15 |
+
let detectedLanguage = null; // Store detected language
|
| 16 |
+
let isTTSPlaying = false; // Track TTS playback state to prevent mic feedback
|
| 17 |
+
let textProcessingTriggered = false; // Track if text was already sent (prevents double-processing)
|
| 18 |
+
let silenceDetectionActive = true; // Control silence detection loop
|
| 19 |
+
let currentRecognitionLang = 'fr-FR'; // Track current recognition language for duplex mode
|
| 20 |
+
|
| 21 |
+
// Global mode variable
|
| 22 |
+
window.continuousMode = false;
|
| 23 |
+
window.lastBotAudio = null; // 🌍 Exposed for Replay Button
|
| 24 |
+
|
| 25 |
+
// 🆔 Cycle tracking to prevent ghost handler duplicates
|
| 26 |
+
let currentCycleId = 0;
|
| 27 |
+
|
| 28 |
+
// 🔊 SILENCE DETECTION THRESHOLDS (More sensitive to prevent hallucination)
|
| 29 |
+
// 🔊 VOLUME THRESHOLD Config moved to top of file
|
| 30 |
+
const VOLUME_THRESHOLD = 8; // Noise Gate: Very sensitive (was 10)
|
| 31 |
+
const SILENCE_LIMIT_MS = 5000; // 5.0s silence - VERY Generous pause time
|
| 32 |
+
const SILENCE_THRESHOLD = 8; // Legacy support
|
| 33 |
+
const MIN_RECORDING_TIME = 500; // Minimum 0.5 second recording
|
| 34 |
+
const MIN_SPEECH_VOLUME = 5; // Minimum average volume to consider as speech
|
| 35 |
+
const TYPING_SPEED_MS = 25;
|
| 36 |
+
|
| 37 |
+
|
| 38 |
+
|
| 39 |
+
// CHAT UI HELPERS
|
| 40 |
+
let recentMessages = new Set();
|
| 41 |
+
|
| 42 |
+
// 🛡️ WHISPER HALLUCINATION FILTER - Common false outputs when silence/noise
|
| 43 |
+
const HALLUCINATION_PHRASES = [
|
| 44 |
+
'thanks for watching',
|
| 45 |
+
'thank you for watching',
|
| 46 |
+
'subscribe',
|
| 47 |
+
'like and subscribe',
|
| 48 |
+
'see you next time',
|
| 49 |
+
'bye bye',
|
| 50 |
+
'goodbye',
|
| 51 |
+
'merci d\'avoir regardé',
|
| 52 |
+
'merci de votre attention',
|
| 53 |
+
'à bientôt',
|
| 54 |
+
'sous-titres',
|
| 55 |
+
'sous-titrage',
|
| 56 |
+
'subtitles by',
|
| 57 |
+
'transcribed by',
|
| 58 |
+
'music',
|
| 59 |
+
'applause',
|
| 60 |
+
'[music]',
|
| 61 |
+
'[applause]',
|
| 62 |
+
'...',
|
| 63 |
+
'you',
|
| 64 |
+
'the',
|
| 65 |
+
'i',
|
| 66 |
+
'a'
|
| 67 |
+
];
|
| 68 |
+
|
| 69 |
+
function isHallucination(text) {
|
| 70 |
+
if (!text) return true;
|
| 71 |
+
const cleaned = text.toLowerCase().trim();
|
| 72 |
+
|
| 73 |
+
// Too short = likely noise
|
| 74 |
+
if (cleaned.length < 3) return true;
|
| 75 |
+
|
| 76 |
+
// Check against known hallucinations
|
| 77 |
+
for (const phrase of HALLUCINATION_PHRASES) {
|
| 78 |
+
if (cleaned === phrase || cleaned.startsWith(phrase + '.') || cleaned.startsWith(phrase + '!')) {
|
| 79 |
+
console.log(`🚫 HALLUCINATION BLOCKED: "${text}"`);
|
| 80 |
+
return true;
|
| 81 |
+
}
|
| 82 |
+
}
|
| 83 |
+
|
| 84 |
+
// Single repeated character or word
|
| 85 |
+
if (/^(.)\1*$/.test(cleaned) || /^(\w+\s*)\1+$/.test(cleaned)) {
|
| 86 |
+
console.log(`🚫 REPEATED PATTERN BLOCKED: "${text}"`);
|
| 87 |
+
return true;
|
| 88 |
+
}
|
| 89 |
+
|
| 90 |
+
return false;
|
| 91 |
+
}
|
| 92 |
+
|
| 93 |
+
function createChatMessage(role, text, audioSrc = null, info = null, lang = null) {
|
| 94 |
+
const chatHistory = document.getElementById('chat-history');
|
| 95 |
+
if (!chatHistory) return;
|
| 96 |
+
|
| 97 |
+
// 🛡️ Block hallucinations at message level too
|
| 98 |
+
if (isHallucination(text)) {
|
| 99 |
+
console.log(`🚫 createChatMessage: Hallucination blocked: "${text}"`);
|
| 100 |
+
return;
|
| 101 |
+
}
|
| 102 |
+
|
| 103 |
+
// 🛡️ HOLY WAR VISUAL SHIELD (DEDUPLICATION)
|
| 104 |
+
// Prevent duplicate messages within 5 seconds
|
| 105 |
+
const normalizedText = text.trim().toLowerCase().substring(0, 100);
|
| 106 |
+
const messageHash = `${role}-${normalizedText}`;
|
| 107 |
+
if (recentMessages.has(messageHash)) {
|
| 108 |
+
console.log(`🛡️ VISUAL SHIELD: Blocked duplicate message: "${text.substring(0, 30)}..."`);
|
| 109 |
+
return;
|
| 110 |
+
}
|
| 111 |
+
recentMessages.add(messageHash);
|
| 112 |
+
setTimeout(() => recentMessages.delete(messageHash), 5000); // 5 seconds Blocking Period
|
| 113 |
+
|
| 114 |
+
const msgDiv = document.createElement('div');
|
| 115 |
+
msgDiv.className = `message ${role}-message`;
|
| 116 |
+
msgDiv.style.opacity = '0'; // For animation
|
| 117 |
+
msgDiv.style.cssText = `
|
| 118 |
+
background: ${role === 'user' ? 'rgba(30, 30, 35, 0.8)' : 'rgba(45, 45, 52, 0.8)'};
|
| 119 |
+
border-radius: 16px;
|
| 120 |
+
padding: 20px;
|
| 121 |
+
margin-bottom: 16px;
|
| 122 |
+
border: 1px solid ${role === 'user' ? 'rgba(60, 60, 70, 0.5)' : 'rgba(80, 80, 90, 0.5)'};
|
| 123 |
+
`;
|
| 124 |
+
|
| 125 |
+
// Language Badge (Always show)
|
| 126 |
+
const langBadge = document.createElement('div');
|
| 127 |
+
langBadge.className = 'lang-badge';
|
| 128 |
+
langBadge.style.cssText = `
|
| 129 |
+
display: inline-block;
|
| 130 |
+
background: ${role === 'user' ? 'rgba(60, 60, 70, 0.6)' : 'rgba(80, 80, 90, 0.6)'};
|
| 131 |
+
color: ${role === 'user' ? '#a0a0a8' : '#c0c0c8'};
|
| 132 |
+
padding: 6px 12px;
|
| 133 |
+
border-radius: 8px;
|
| 134 |
+
font-size: 0.75rem;
|
| 135 |
+
font-weight: 600;
|
| 136 |
+
text-transform: uppercase;
|
| 137 |
+
letter-spacing: 0.05em;
|
| 138 |
+
margin-bottom: 12px;
|
| 139 |
+
`;
|
| 140 |
+
|
| 141 |
+
// Determine language display
|
| 142 |
+
let langDisplay = lang || (role === 'user' ? 'Input' : 'Translation');
|
| 143 |
+
langBadge.innerText = `Language: ${langDisplay}`;
|
| 144 |
+
msgDiv.appendChild(langBadge);
|
| 145 |
+
|
| 146 |
+
// Text Content - Large and Clear
|
| 147 |
+
const textDiv = document.createElement('div');
|
| 148 |
+
textDiv.className = 'message-content';
|
| 149 |
+
textDiv.style.cssText = `
|
| 150 |
+
font-size: 1.25rem;
|
| 151 |
+
line-height: 1.7;
|
| 152 |
+
color: #ffffff;
|
| 153 |
+
font-weight: 400;
|
| 154 |
+
margin-top: 8px;
|
| 155 |
+
`;
|
| 156 |
+
textDiv.innerText = text;
|
| 157 |
+
msgDiv.appendChild(textDiv);
|
| 158 |
+
|
| 159 |
+
// Audio Player Integration (Only for Bot)
|
| 160 |
+
// 🛡️ FALLBACK: If audioSrc is missing, use Browser TTS!
|
| 161 |
+
if (role === 'bot') {
|
| 162 |
+
if (!audioSrc) {
|
| 163 |
+
console.warn("⚠️ No Audio from Server (API Limit/Error). Using Browser TTS Fallback.");
|
| 164 |
+
// Browser TTS Fallback
|
| 165 |
+
const utterance = new SpeechSynthesisUtterance(text);
|
| 166 |
+
// Try to set language (default to detected target or whatever)
|
| 167 |
+
// utterance.lang = 'en-US'; // Ideally passed in info
|
| 168 |
+
window.speechSynthesis.speak(utterance);
|
| 169 |
+
} else {
|
| 170 |
+
// Standard Server Audio
|
| 171 |
+
const audioContainer = document.createElement('div');
|
| 172 |
+
audioContainer.className = 'audio-container';
|
| 173 |
+
audioContainer.style.marginTop = '12px';
|
| 174 |
+
audioContainer.style.background = 'rgba(0,0,0,0.1)';
|
| 175 |
+
audioContainer.style.borderRadius = '8px';
|
| 176 |
+
audioContainer.style.padding = '8px';
|
| 177 |
+
audioContainer.style.display = 'flex';
|
| 178 |
+
audioContainer.style.alignItems = 'center';
|
| 179 |
+
audioContainer.style.gap = '10px';
|
| 180 |
+
|
| 181 |
+
const playBtn = document.createElement('button');
|
| 182 |
+
playBtn.innerHTML = '<i class="fa-solid fa-play"></i>';
|
| 183 |
+
playBtn.className = 'icon-btn'; // Re-use existing class
|
| 184 |
+
playBtn.style.width = '32px';
|
| 185 |
+
playBtn.style.height = '32px';
|
| 186 |
+
playBtn.style.background = '#fff';
|
| 187 |
+
playBtn.style.color = '#333';
|
| 188 |
+
|
| 189 |
+
// Waveform Visual (Fake/Static for aesthetics)
|
| 190 |
+
const waveDiv = document.createElement('div');
|
| 191 |
+
waveDiv.style.flex = '1';
|
| 192 |
+
waveDiv.style.height = '4px';
|
| 193 |
+
waveDiv.style.background = 'rgba(255,255,255,0.3)';
|
| 194 |
+
waveDiv.style.borderRadius = '2px';
|
| 195 |
+
waveDiv.style.position = 'relative';
|
| 196 |
+
|
| 197 |
+
const progressDiv = document.createElement('div');
|
| 198 |
+
progressDiv.style.width = '0%';
|
| 199 |
+
progressDiv.style.height = '100%';
|
| 200 |
+
progressDiv.style.background = '#fff';
|
| 201 |
+
progressDiv.style.borderRadius = '2px';
|
| 202 |
+
progressDiv.style.transition = 'width 0.1s linear';
|
| 203 |
+
waveDiv.appendChild(progressDiv);
|
| 204 |
+
|
| 205 |
+
// Audio Logic
|
| 206 |
+
const audio = new Audio(audioSrc);
|
| 207 |
+
audio.preload = 'auto'; // Force immediate buffer
|
| 208 |
+
|
| 209 |
+
// 🌍 Update Global Replay Reference
|
| 210 |
+
window.lastBotAudio = audio;
|
| 211 |
+
|
| 212 |
+
playBtn.onclick = () => {
|
| 213 |
+
if (audio.paused) {
|
| 214 |
+
audio.play();
|
| 215 |
+
playBtn.innerHTML = '<i class="fa-solid fa-pause"></i>';
|
| 216 |
+
} else {
|
| 217 |
+
audio.pause();
|
| 218 |
+
playBtn.innerHTML = '<i class="fa-solid fa-play"></i>';
|
| 219 |
+
}
|
| 220 |
+
};
|
| 221 |
+
|
| 222 |
+
// 🔇 CRITICAL: Pause speech recognition when TTS starts (prevent feedback loop)
|
| 223 |
+
audio.onplay = () => {
|
| 224 |
+
isTTSPlaying = true;
|
| 225 |
+
console.log('🔊 TTS Started - Pausing speech recognition to prevent feedback');
|
| 226 |
+
|
| 227 |
+
// Pause browser speech recognition if active
|
| 228 |
+
if (recognition) {
|
| 229 |
+
try {
|
| 230 |
+
recognition.stop();
|
| 231 |
+
console.log('⏸️ Paused speech recognition during TTS');
|
| 232 |
+
} catch (e) { }
|
| 233 |
+
}
|
| 234 |
+
|
| 235 |
+
// 🎯 DUPLEX MODE: MediaRecorder keeps running (don't pause it)
|
| 236 |
+
// We only pause speech recognition to avoid feedback
|
| 237 |
+
console.log('🎙️ MediaRecorder continues running during TTS');
|
| 238 |
+
};
|
| 239 |
+
|
| 240 |
+
audio.onended = () => {
|
| 241 |
+
playBtn.innerHTML = '<i class="fa-solid fa-play"></i>';
|
| 242 |
+
progressDiv.style.width = '0%';
|
| 243 |
+
|
| 244 |
+
// ▶️ CRITICAL: Resume after TTS
|
| 245 |
+
isTTSPlaying = false;
|
| 246 |
+
console.log('✅ TTS ended - Ready for next conversation');
|
| 247 |
+
|
| 248 |
+
// Update status for continuous mode
|
| 249 |
+
if (window.continuousMode) {
|
| 250 |
+
statusText.innerText = '💤 Prêt pour la suite...';
|
| 251 |
+
statusText.style.color = '#4a9b87';
|
| 252 |
+
console.log('🔄 Continuous mode active - system will listen automatically');
|
| 253 |
+
}
|
| 254 |
+
};
|
| 255 |
+
|
| 256 |
+
// 🚨 Error handling to prevent crashes
|
| 257 |
+
audio.onerror = (e) => {
|
| 258 |
+
console.error('❌ TTS playback error:', e);
|
| 259 |
+
isTTSPlaying = false;
|
| 260 |
+
playBtn.innerHTML = '<i class="fa-solid fa-play"></i>';
|
| 261 |
+
|
| 262 |
+
if (window.continuousMode) {
|
| 263 |
+
statusText.innerText = '⚠️ Erreur TTS - Prêt';
|
| 264 |
+
statusText.style.color = '#ff6b6b';
|
| 265 |
+
}
|
| 266 |
+
};
|
| 267 |
+
|
| 268 |
+
audio.ontimeupdate = () => {
|
| 269 |
+
const percent = (audio.currentTime / audio.duration) * 100;
|
| 270 |
+
progressDiv.style.width = `${percent}%`;
|
| 271 |
+
};
|
| 272 |
+
|
| 273 |
+
// 🚀 AUTO-PLAY + PRE-CHECK
|
| 274 |
+
// Ensure audio is playable immediately
|
| 275 |
+
audio.oncanplay = () => {
|
| 276 |
+
// Ready to start
|
| 277 |
+
};
|
| 278 |
+
|
| 279 |
+
audio.oncanplaythrough = () => {
|
| 280 |
+
// Fully ready
|
| 281 |
+
};
|
| 282 |
+
|
| 283 |
+
audioContainer.appendChild(playBtn);
|
| 284 |
+
audioContainer.appendChild(waveDiv);
|
| 285 |
+
msgDiv.appendChild(audioContainer);
|
| 286 |
+
|
| 287 |
+
// Latency Badge - REMOVED (cleaner UI)
|
| 288 |
+
// Users don't need to see engine names
|
| 289 |
+
|
| 290 |
+
// Immediate Trigger - Will auto-pause mic via onplay handler
|
| 291 |
+
const playPromise = audio.play();
|
| 292 |
+
if (playPromise !== undefined) {
|
| 293 |
+
playPromise.then(_ => {
|
| 294 |
+
playBtn.innerHTML = '<i class="fa-solid fa-pause"></i>';
|
| 295 |
+
}).catch(error => {
|
| 296 |
+
console.log("Auto-play blocked by browser policy:", error);
|
| 297 |
+
if (isTTSPlaying) {
|
| 298 |
+
// If blocked, we must ensure we don't get stuck in "TTS Playing" state
|
| 299 |
+
console.warn("⚠️ Autoplay blocked. Resetting state.");
|
| 300 |
+
isTTSPlaying = false;
|
| 301 |
+
playBtn.innerHTML = '<i class="fa-solid fa-play"></i>';
|
| 302 |
+
}
|
| 303 |
+
});
|
| 304 |
+
}
|
| 305 |
+
} // End of else (Server Audio)
|
| 306 |
+
} // End of if (Bot Role)
|
| 307 |
+
|
| 308 |
+
chatHistory.appendChild(msgDiv);
|
| 309 |
+
|
| 310 |
+
// AUTO-SCROLL: Scroll both containers to show latest message
|
| 311 |
+
const scrollToBottom = () => {
|
| 312 |
+
// Scroll chat history (if it becomes scrollable)
|
| 313 |
+
chatHistory.scrollTo({
|
| 314 |
+
top: chatHistory.scrollHeight,
|
| 315 |
+
behavior: 'smooth'
|
| 316 |
+
});
|
| 317 |
+
|
| 318 |
+
// 🚀 SUGAR: Scroll the Window (Main Stage is not scrollable)
|
| 319 |
+
window.scrollTo({
|
| 320 |
+
top: document.body.scrollHeight,
|
| 321 |
+
behavior: 'smooth'
|
| 322 |
+
});
|
| 323 |
+
};
|
| 324 |
+
|
| 325 |
+
// Immediate scroll
|
| 326 |
+
scrollToBottom();
|
| 327 |
+
|
| 328 |
+
// Scroll again after animation completes
|
| 329 |
+
setTimeout(scrollToBottom, 300);
|
| 330 |
+
setTimeout(scrollToBottom, 600);
|
| 331 |
+
|
| 332 |
+
// Fade In Animation
|
| 333 |
+
setTimeout(() => {
|
| 334 |
+
msgDiv.style.transition = 'opacity 0.3s ease, transform 0.3s ease';
|
| 335 |
+
msgDiv.style.transform = 'translateY(10px)';
|
| 336 |
+
requestAnimationFrame(() => {
|
| 337 |
+
msgDiv.style.opacity = '1';
|
| 338 |
+
msgDiv.style.transform = 'translateY(0)';
|
| 339 |
+
});
|
| 340 |
+
}, 50);
|
| 341 |
+
}
|
| 342 |
+
|
| 343 |
+
// DOM Elements - declared but not initialized yet
|
| 344 |
+
let recordBtn, statusText, settingsBtn, settingsModal, audioPlayer;
|
| 345 |
+
let originalTextField, translatedTextField, quickLangSelector, sourceLangSelector, aiModelSelector;
|
| 346 |
+
|
| 347 |
+
// 🔊 AUDIO UNLOCKER: Play silence on click to enable Autoplay
|
| 348 |
+
function unlockAudioContext() {
|
| 349 |
+
try {
|
| 350 |
+
const ctx = new (window.AudioContext || window.webkitAudioContext)();
|
| 351 |
+
const osc = ctx.createOscillator();
|
| 352 |
+
const gain = ctx.createGain();
|
| 353 |
+
gain.gain.value = 0.001;
|
| 354 |
+
osc.connect(gain);
|
| 355 |
+
gain.connect(ctx.destination);
|
| 356 |
+
osc.start(0);
|
| 357 |
+
setTimeout(() => { osc.stop(); ctx.close(); }, 100);
|
| 358 |
+
console.log("🔓 Audio Autoplay Unlocked");
|
| 359 |
+
} catch (e) {
|
| 360 |
+
console.log("Audio unlock not needed");
|
| 361 |
+
}
|
| 362 |
+
}
|
| 363 |
+
|
| 364 |
+
// ============================================================
|
| 365 |
+
// 🎯 INITIALIZE EVERYTHING WHEN DOM IS READY
|
| 366 |
+
// ============================================================
|
| 367 |
+
function initializeApp() {
|
| 368 |
+
console.log('🎯 initializeApp() called');
|
| 369 |
+
|
| 370 |
+
// 🚀 FULL AUTO CONFIGURATION (User Request)
|
| 371 |
+
if (!localStorage.getItem('googleKey')) {
|
| 372 |
+
console.log('💎 FULL AUTO: Injecting Google API Key...');
|
| 373 |
+
localStorage.setItem('googleKey', 'AIzaSyDB9wiqXsy1dG9OLU9r4Tar8oDdeVy4NOQ');
|
| 374 |
+
}
|
| 375 |
+
|
| 376 |
+
// Get DOM Elements
|
| 377 |
+
recordBtn = document.getElementById('record-btn');
|
| 378 |
+
statusText = document.getElementById('status-placeholder');
|
| 379 |
+
settingsBtn = document.getElementById('settings-trigger');
|
| 380 |
+
settingsModal = document.getElementById('settings-modal');
|
| 381 |
+
audioPlayer = document.getElementById('audio-player');
|
| 382 |
+
originalTextField = document.getElementById('original-text');
|
| 383 |
+
translatedTextField = document.getElementById('translated-text');
|
| 384 |
+
quickLangSelector = document.getElementById('target-lang-quick'); // 🔧 FIXED: Use correct ID
|
| 385 |
+
sourceLangSelector = document.getElementById('source-lang-selector');
|
| 386 |
+
aiModelSelector = document.getElementById('ai-model');
|
| 387 |
+
|
| 388 |
+
console.log('📦 DOM Elements loaded:');
|
| 389 |
+
console.log(' - recordBtn:', recordBtn ? '✅ FOUND' : '❌ NOT FOUND');
|
| 390 |
+
console.log(' - statusText:', statusText ? '✅ FOUND' : '❌ NOT FOUND');
|
| 391 |
+
|
| 392 |
+
if (!recordBtn) {
|
| 393 |
+
console.error('❌❌❌ CRITICAL: record-btn NOT FOUND IN DOM! ❌❌❌');
|
| 394 |
+
return;
|
| 395 |
+
}
|
| 396 |
+
|
| 397 |
+
// 🎙️ BUTTON CLICK HANDLER
|
| 398 |
+
console.log('🔧 Attaching click handler...');
|
| 399 |
+
|
| 400 |
+
recordBtn.onclick = async function (e) {
|
| 401 |
+
console.log('🔘🔘🔘 BUTTON CLICKED! 🔘🔘🔘');
|
| 402 |
+
e.preventDefault();
|
| 403 |
+
e.stopPropagation();
|
| 404 |
+
|
| 405 |
+
// Unlock audio
|
| 406 |
+
unlockAudioContext();
|
| 407 |
+
|
| 408 |
+
if (!window.continuousMode) {
|
| 409 |
+
// START
|
| 410 |
+
console.log('▶️ Starting continuous mode...');
|
| 411 |
+
window.continuousMode = true;
|
| 412 |
+
this.classList.add('active');
|
| 413 |
+
|
| 414 |
+
if (statusText) {
|
| 415 |
+
statusText.innerText = 'Écoute en continu...';
|
| 416 |
+
statusText.style.color = '#4a9b87';
|
| 417 |
+
}
|
| 418 |
+
|
| 419 |
+
try {
|
| 420 |
+
await listenContinuously();
|
| 421 |
+
} catch (error) {
|
| 422 |
+
console.error('❌ Error:', error);
|
| 423 |
+
window.continuousMode = false;
|
| 424 |
+
this.classList.remove('active');
|
| 425 |
+
if (statusText) {
|
| 426 |
+
statusText.innerText = 'Erreur: ' + error.message;
|
| 427 |
+
statusText.style.color = '#ff6b6b';
|
| 428 |
+
}
|
| 429 |
+
}
|
| 430 |
+
} else {
|
| 431 |
+
// STOP
|
| 432 |
+
console.log('⏹️ Stopping continuous mode...');
|
| 433 |
+
window.continuousMode = false;
|
| 434 |
+
this.classList.remove('active');
|
| 435 |
+
this.classList.remove('active-speech'); // ✅ FIXED: CSS class name
|
| 436 |
+
this.classList.remove('processing'); // ✅ FIXED: Remove processing state too
|
| 437 |
+
|
| 438 |
+
// Stop all components
|
| 439 |
+
try {
|
| 440 |
+
if (mediaRecorder && mediaRecorder.state !== 'inactive') {
|
| 441 |
+
mediaRecorder.stop();
|
| 442 |
+
}
|
| 443 |
+
if (recognition) {
|
| 444 |
+
recognition.stop();
|
| 445 |
+
recognition = null;
|
| 446 |
+
}
|
| 447 |
+
if (audioContext && audioContext.state !== 'closed') {
|
| 448 |
+
audioContext.close();
|
| 449 |
+
}
|
| 450 |
+
} catch (e) {
|
| 451 |
+
console.warn('Cleanup warning:', e);
|
| 452 |
+
}
|
| 453 |
+
|
| 454 |
+
// 🧹 CRITICAL: Full memory cleanup to keep system fast
|
| 455 |
+
console.log('🧹 Cleaning up memory and cache...');
|
| 456 |
+
|
| 457 |
+
audioContext = null;
|
| 458 |
+
analyser = null;
|
| 459 |
+
micSource = null;
|
| 460 |
+
mediaRecorder = null;
|
| 461 |
+
audioChunks = [];
|
| 462 |
+
isRecording = false;
|
| 463 |
+
isProcessingAudio = false;
|
| 464 |
+
speechDetected = false;
|
| 465 |
+
textProcessingTriggered = false;
|
| 466 |
+
|
| 467 |
+
// Clear audio buffers
|
| 468 |
+
if (animationId) {
|
| 469 |
+
cancelAnimationFrame(animationId);
|
| 470 |
+
animationId = null;
|
| 471 |
+
}
|
| 472 |
+
|
| 473 |
+
// ⚡ Clear backend conversation cache for fresh start
|
| 474 |
+
fetch('/clear_cache', { method: 'POST' })
|
| 475 |
+
.then(res => res.json())
|
| 476 |
+
.then(data => console.log(`✅ Backend cache cleared: ${data.cleared} entries`))
|
| 477 |
+
.catch(e => console.warn('Cache clear failed:', e));
|
| 478 |
+
|
| 479 |
+
if (statusText) {
|
| 480 |
+
statusText.innerText = 'Arrêté';
|
| 481 |
+
statusText.style.color = '#888';
|
| 482 |
+
}
|
| 483 |
+
console.log('✅ Stopped');
|
| 484 |
+
|
| 485 |
+
// Kill global stream on full stop
|
| 486 |
+
if (globalStream) {
|
| 487 |
+
try {
|
| 488 |
+
globalStream.getTracks().forEach(track => track.stop());
|
| 489 |
+
} catch (e) { }
|
| 490 |
+
globalStream = null;
|
| 491 |
+
}
|
| 492 |
+
}
|
| 493 |
+
};
|
| 494 |
+
|
| 495 |
+
// 📱 MOBILE TOUCH SUPPORT (CRITICAL FIX)
|
| 496 |
+
// Desktop: onclick works
|
| 497 |
+
// Mobile: Need touchstart/touchend
|
| 498 |
+
let touchHandled = false;
|
| 499 |
+
|
| 500 |
+
recordBtn.addEventListener('touchstart', (e) => {
|
| 501 |
+
e.preventDefault(); // Prevent mouse event simulation
|
| 502 |
+
touchHandled = true;
|
| 503 |
+
recordBtn.onclick(e); // Trigger the same logic
|
| 504 |
+
}, { passive: false });
|
| 505 |
+
|
| 506 |
+
recordBtn.addEventListener('touchend', (e) => {
|
| 507 |
+
e.preventDefault();
|
| 508 |
+
}, { passive: false });
|
| 509 |
+
|
| 510 |
+
// Fallback for desktop
|
| 511 |
+
recordBtn.addEventListener('click', (e) => {
|
| 512 |
+
if (touchHandled) {
|
| 513 |
+
touchHandled = false;
|
| 514 |
+
return; // Already handled by touch
|
| 515 |
+
}
|
| 516 |
+
// Desktop logic continues normally
|
| 517 |
+
});
|
| 518 |
+
|
| 519 |
+
// Disable context menu
|
| 520 |
+
recordBtn.oncontextmenu = (e) => e.preventDefault();
|
| 521 |
+
|
| 522 |
+
// ============================================================
|
| 523 |
+
// 🌍 LANGUAGE QUICK SELECTORS - Event Handlers
|
| 524 |
+
// ============================================================
|
| 525 |
+
|
| 526 |
+
const sourceLangQuick = document.getElementById('source-lang-quick');
|
| 527 |
+
const targetLangQuick = document.getElementById('target-lang-quick');
|
| 528 |
+
const swapLangsBtn = document.getElementById('swap-langs');
|
| 529 |
+
|
| 530 |
+
// 🎯 SOURCE LANGUAGE CHANGE
|
| 531 |
+
if (sourceLangQuick) {
|
| 532 |
+
sourceLangQuick.addEventListener('change', function () {
|
| 533 |
+
const newLang = this.value;
|
| 534 |
+
console.log(`🔄 Source language changed to: ${newLang}`);
|
| 535 |
+
|
| 536 |
+
// Save to localStorage for persistence
|
| 537 |
+
localStorage.setItem('sourceLangQuick', newLang);
|
| 538 |
+
|
| 539 |
+
// Update status to show change
|
| 540 |
+
if (statusText) {
|
| 541 |
+
statusText.innerText = `📍 Source: ${this.options[this.selectedIndex].text}`;
|
| 542 |
+
statusText.style.color = '#4a9b87';
|
| 543 |
+
setTimeout(() => {
|
| 544 |
+
statusText.innerText = 'Prêt';
|
| 545 |
+
statusText.style.color = '#888';
|
| 546 |
+
}, 2000);
|
| 547 |
+
}
|
| 548 |
+
|
| 549 |
+
// Restart recognition with new language if currently recording
|
| 550 |
+
if (window.continuousMode && recognition) {
|
| 551 |
+
console.log('🔄 Restarting recognition with new source language...');
|
| 552 |
+
try { recognition.stop(); } catch (e) { }
|
| 553 |
+
// It will auto-restart with new language via onend handler
|
| 554 |
+
}
|
| 555 |
+
});
|
| 556 |
+
|
| 557 |
+
// Restore saved value
|
| 558 |
+
const savedSource = localStorage.getItem('sourceLangQuick');
|
| 559 |
+
if (savedSource) {
|
| 560 |
+
sourceLangQuick.value = savedSource;
|
| 561 |
+
}
|
| 562 |
+
}
|
| 563 |
+
|
| 564 |
+
// 🎯 TARGET LANGUAGE CHANGE
|
| 565 |
+
if (targetLangQuick) {
|
| 566 |
+
targetLangQuick.addEventListener('change', function () {
|
| 567 |
+
const newLang = this.value;
|
| 568 |
+
console.log(`🎯 Target language changed to: ${newLang}`);
|
| 569 |
+
|
| 570 |
+
// Save to localStorage for persistence
|
| 571 |
+
localStorage.setItem('targetLangQuick', newLang);
|
| 572 |
+
|
| 573 |
+
// Also update the main selector if it exists
|
| 574 |
+
if (quickLangSelector) {
|
| 575 |
+
quickLangSelector.value = newLang;
|
| 576 |
+
}
|
| 577 |
+
|
| 578 |
+
// Update status to show change
|
| 579 |
+
if (statusText) {
|
| 580 |
+
statusText.innerText = `🎯 Cible: ${this.options[this.selectedIndex].text}`;
|
| 581 |
+
statusText.style.color = '#4a9b87';
|
| 582 |
+
setTimeout(() => {
|
| 583 |
+
statusText.innerText = 'Prêt';
|
| 584 |
+
statusText.style.color = '#888';
|
| 585 |
+
}, 2000);
|
| 586 |
+
}
|
| 587 |
+
});
|
| 588 |
+
|
| 589 |
+
// Restore saved value
|
| 590 |
+
const savedTarget = localStorage.getItem('targetLangQuick');
|
| 591 |
+
if (savedTarget) {
|
| 592 |
+
targetLangQuick.value = savedTarget;
|
| 593 |
+
}
|
| 594 |
+
}
|
| 595 |
+
|
| 596 |
+
// 🔄 SWAP LANGUAGES BUTTON
|
| 597 |
+
if (swapLangsBtn) {
|
| 598 |
+
swapLangsBtn.addEventListener('click', function () {
|
| 599 |
+
console.log('🔄 Swapping languages...');
|
| 600 |
+
|
| 601 |
+
const sourceSelect = document.getElementById('source-lang-quick');
|
| 602 |
+
const targetSelect = document.getElementById('target-lang-quick');
|
| 603 |
+
|
| 604 |
+
if (!sourceSelect || !targetSelect) {
|
| 605 |
+
console.warn('Language selectors not found');
|
| 606 |
+
return;
|
| 607 |
+
}
|
| 608 |
+
|
| 609 |
+
// Get current values
|
| 610 |
+
const currentSource = sourceSelect.value;
|
| 611 |
+
const currentTarget = targetSelect.value;
|
| 612 |
+
|
| 613 |
+
// Map target names to source codes
|
| 614 |
+
const targetToSourceMap = {
|
| 615 |
+
'French': 'fr-FR',
|
| 616 |
+
'English': 'en-US',
|
| 617 |
+
'Arabic': 'ar-SA',
|
| 618 |
+
'Moroccan Darija': 'ar-SA',
|
| 619 |
+
'Spanish': 'es-ES',
|
| 620 |
+
'German': 'de-DE'
|
| 621 |
+
};
|
| 622 |
+
|
| 623 |
+
// Map source codes to target names
|
| 624 |
+
const sourceToTargetMap = {
|
| 625 |
+
'fr-FR': 'French',
|
| 626 |
+
'en-US': 'English',
|
| 627 |
+
'ar-SA': 'Arabic',
|
| 628 |
+
'es-ES': 'Spanish',
|
| 629 |
+
'de-DE': 'German',
|
| 630 |
+
'auto': 'French' // Default when swapping from auto
|
| 631 |
+
};
|
| 632 |
+
|
| 633 |
+
// Calculate new values
|
| 634 |
+
const newSourceCode = targetToSourceMap[currentTarget] || 'auto';
|
| 635 |
+
const newTargetName = sourceToTargetMap[currentSource] || 'French';
|
| 636 |
+
|
| 637 |
+
// Apply swap
|
| 638 |
+
sourceSelect.value = newSourceCode;
|
| 639 |
+
targetSelect.value = newTargetName;
|
| 640 |
+
|
| 641 |
+
// Save to localStorage
|
| 642 |
+
localStorage.setItem('sourceLangQuick', newSourceCode);
|
| 643 |
+
localStorage.setItem('targetLangQuick', newTargetName);
|
| 644 |
+
|
| 645 |
+
// Visual feedback
|
| 646 |
+
this.style.transform = 'rotate(180deg)';
|
| 647 |
+
setTimeout(() => {
|
| 648 |
+
this.style.transform = 'rotate(0deg)';
|
| 649 |
+
}, 300);
|
| 650 |
+
|
| 651 |
+
// Update status
|
| 652 |
+
if (statusText) {
|
| 653 |
+
statusText.innerText = `🔄 ${sourceSelect.options[sourceSelect.selectedIndex].text} ↔ ${targetSelect.options[targetSelect.selectedIndex].text}`;
|
| 654 |
+
statusText.style.color = '#60a5fa';
|
| 655 |
+
setTimeout(() => {
|
| 656 |
+
statusText.innerText = 'Prêt';
|
| 657 |
+
statusText.style.color = '#888';
|
| 658 |
+
}, 2500);
|
| 659 |
+
}
|
| 660 |
+
|
| 661 |
+
console.log(`✅ Swapped: ${currentSource} → ${newTargetName}, ${currentTarget} → ${newSourceCode}`);
|
| 662 |
+
|
| 663 |
+
// Restart recognition if active
|
| 664 |
+
if (window.continuousMode && recognition) {
|
| 665 |
+
try { recognition.stop(); } catch (e) { }
|
| 666 |
+
}
|
| 667 |
+
});
|
| 668 |
+
}
|
| 669 |
+
|
| 670 |
+
console.log('�� Language quick selectors initialized');
|
| 671 |
+
console.log('✅✅✅ BUTTON HANDLER ATTACHED! ✅✅✅');
|
| 672 |
+
}
|
| 673 |
+
|
| 674 |
+
// Run initialization when DOM is ready
|
| 675 |
+
if (document.readyState === 'loading') {
|
| 676 |
+
console.log('📄 DOM not ready, waiting for DOMContentLoaded...');
|
| 677 |
+
document.addEventListener('DOMContentLoaded', initializeApp);
|
| 678 |
+
} else {
|
| 679 |
+
console.log('📄 DOM already ready, initializing now...');
|
| 680 |
+
initializeApp();
|
| 681 |
+
}
|
| 682 |
+
|
| 683 |
+
// --- CONTINUOUS CONVERSATION MODE ---
|
| 684 |
+
|
| 685 |
+
async function listenContinuously() {
|
| 686 |
+
if (!window.continuousMode) {
|
| 687 |
+
console.log("❌ listenContinuously called but window.continuousMode is false");
|
| 688 |
+
return;
|
| 689 |
+
}
|
| 690 |
+
|
| 691 |
+
// 🛡️ RECURSION GUARD: If we are already recording, don't start another loop!
|
| 692 |
+
if (isRecording) {
|
| 693 |
+
console.log("⚠️ Already recording, skipping duplicate start request");
|
| 694 |
+
return;
|
| 695 |
+
}
|
| 696 |
+
|
| 697 |
+
console.log("🎙️ Starting NEW listening cycle...");
|
| 698 |
+
|
| 699 |
+
try {
|
| 700 |
+
// 🔥 CRITICAL: Increment cycle ID to invalidate old handlers
|
| 701 |
+
currentCycleId++;
|
| 702 |
+
const thisCycleId = currentCycleId;
|
| 703 |
+
console.log(`🆔 Cycle ID: ${thisCycleId}`);
|
| 704 |
+
|
| 705 |
+
// 🔥 CRITICAL: Clean up old MediaRecorder to prevent ghost handlers
|
| 706 |
+
if (mediaRecorder && mediaRecorder.state !== 'inactive') {
|
| 707 |
+
try {
|
| 708 |
+
console.log("🧹 Cleaning up old mediaRecorder");
|
| 709 |
+
// Don't call stop() - it will trigger the old onstop handler!
|
| 710 |
+
// Just abandon it and create a new one
|
| 711 |
+
mediaRecorder.ondataavailable = null;
|
| 712 |
+
mediaRecorder.onstop = null;
|
| 713 |
+
mediaRecorder = null;
|
| 714 |
+
} catch (e) { console.warn("Cleanup warning:", e); }
|
| 715 |
+
}
|
| 716 |
+
|
| 717 |
+
isRecording = true;
|
| 718 |
+
audioChunks = [];
|
| 719 |
+
let speechDetected = false; // Reset speech detection
|
| 720 |
+
textProcessingTriggered = false; // Reset flag
|
| 721 |
+
silenceDetectionActive = true; // Enable silence detection
|
| 722 |
+
|
| 723 |
+
let stream;
|
| 724 |
+
|
| 725 |
+
// 🚀 REUSE STREAM IF AVAILABLE
|
| 726 |
+
if (globalStream && globalStream.active) {
|
| 727 |
+
console.log("♻️ Reusing existing microphone stream");
|
| 728 |
+
stream = globalStream;
|
| 729 |
+
} else {
|
| 730 |
+
console.log("🎤 Requesting NEW microphone access...");
|
| 731 |
+
stream = await navigator.mediaDevices.getUserMedia({ audio: true });
|
| 732 |
+
globalStream = stream;
|
| 733 |
+
console.log("✅ Microphone access granted");
|
| 734 |
+
}
|
| 735 |
+
|
| 736 |
+
// 🎯 REAL-TIME TRANSCRIPTION: Start immediately
|
| 737 |
+
startRealTimeTranscription();
|
| 738 |
+
|
| 739 |
+
// Setup audio analysis for silence detection
|
| 740 |
+
// Reuse context if active to reduce click/pop
|
| 741 |
+
if (!audioContext || audioContext.state === 'closed') {
|
| 742 |
+
audioContext = new (window.AudioContext || window.webkitAudioContext)();
|
| 743 |
+
}
|
| 744 |
+
|
| 745 |
+
analyser = audioContext.createAnalyser();
|
| 746 |
+
micSource = audioContext.createMediaStreamSource(stream);
|
| 747 |
+
micSource.connect(analyser);
|
| 748 |
+
analyser.fftSize = 256;
|
| 749 |
+
const bufferLength = analyser.frequencyBinCount;
|
| 750 |
+
const dataArray = new Uint8Array(bufferLength);
|
| 751 |
+
|
| 752 |
+
let silenceStart = Date.now();
|
| 753 |
+
// speechDetected already declared above
|
| 754 |
+
|
| 755 |
+
mediaRecorder = new MediaRecorder(stream);
|
| 756 |
+
|
| 757 |
+
mediaRecorder.ondataavailable = e => {
|
| 758 |
+
// 🛡️ Only process if this is still the current cycle
|
| 759 |
+
if (thisCycleId === currentCycleId) {
|
| 760 |
+
audioChunks.push(e.data);
|
| 761 |
+
} else {
|
| 762 |
+
console.warn(`⚠️ Ignoring data from old cycle ${thisCycleId} (current: ${currentCycleId})`);
|
| 763 |
+
}
|
| 764 |
+
};
|
| 765 |
+
|
| 766 |
+
mediaRecorder.onstop = async () => {
|
| 767 |
+
// 🛡️ CRITICAL: Ignore events from old cycles
|
| 768 |
+
if (thisCycleId !== currentCycleId) {
|
| 769 |
+
console.warn(`⚠️ Ignoring onstop from old cycle ${thisCycleId} (current: ${currentCycleId})`);
|
| 770 |
+
return;
|
| 771 |
+
}
|
| 772 |
+
|
| 773 |
+
console.log(`🛑 Chunk finalized (Cycle ${thisCycleId}).`);
|
| 774 |
+
|
| 775 |
+
// DON'T stop transcription here if we want continuous, but we do need to reset it
|
| 776 |
+
// for the new chunk context. Actually, let's keep it simply "Running".
|
| 777 |
+
|
| 778 |
+
// Process audio if we have valid speech
|
| 779 |
+
if (audioChunks.length > 0 && speechDetected) {
|
| 780 |
+
const blob = new Blob(audioChunks, { type: 'audio/wav' });
|
| 781 |
+
|
| 782 |
+
if (blob.size > 2000) {
|
| 783 |
+
// 🚀 INSTANT RESTART: Don't wait for processing!
|
| 784 |
+
// Trigger processing in background
|
| 785 |
+
statusText.innerText = 'Traitement...';
|
| 786 |
+
statusText.style.color = '#4a9b87';
|
| 787 |
+
|
| 788 |
+
// We DO NOT await here. We fire and forget (mostly),
|
| 789 |
+
// or let it handle the UI updates asynchronously.
|
| 790 |
+
// ⚡ OPTIMIZATION: Pass 'true' to bypass redundant silence check (we already checked it!)
|
| 791 |
+
processAudio(blob, true).catch(e => console.error("Processing error:", e));
|
| 792 |
+
|
| 793 |
+
}
|
| 794 |
+
}
|
| 795 |
+
|
| 796 |
+
// 🔄 INSTANT RESTART (Synchronized)
|
| 797 |
+
// Immediately restart listening *unless* completely stopped
|
| 798 |
+
if (window.continuousMode) {
|
| 799 |
+
// Reset flags for next turn - NOW it is safe to reset
|
| 800 |
+
speechDetected = false;
|
| 801 |
+
|
| 802 |
+
// 🚀 AGGRESSIVE IMMEDIATE RESTART
|
| 803 |
+
// Use 0ms delay to unblock the event loop but start ASAP
|
| 804 |
+
setTimeout(() => {
|
| 805 |
+
if (window.continuousMode) {
|
| 806 |
+
console.log("🔄 Instant Restart Triggered (Parallel)");
|
| 807 |
+
listenContinuously();
|
| 808 |
+
}
|
| 809 |
+
}, 0);
|
| 810 |
+
}
|
| 811 |
+
|
| 812 |
+
// NOTE: We do NOT close audioContext here anymore, to keep it 'warm'.
|
| 813 |
+
// Only disconnect analyser to save CPU if needed, but keeping it open is faster.
|
| 814 |
+
try {
|
| 815 |
+
if (micSource) micSource.disconnect();
|
| 816 |
+
if (analyser) analyser.disconnect();
|
| 817 |
+
if (animationId) cancelAnimationFrame(animationId);
|
| 818 |
+
} catch (e) { }
|
| 819 |
+
};
|
| 820 |
+
|
| 821 |
+
mediaRecorder.start();
|
| 822 |
+
|
| 823 |
+
if (animationId) cancelAnimationFrame(animationId);
|
| 824 |
+
|
| 825 |
+
// 🎯 Continuous monitoring with better noise filtering
|
| 826 |
+
let consecutiveSpeechFrames = 0;
|
| 827 |
+
let consecutiveSilenceFrames = 0;
|
| 828 |
+
const SPEECH_FRAMES_THRESHOLD = 3; // React faster to speech (3 frames = ~50ms)
|
| 829 |
+
const SILENCE_FRAMES_THRESHOLD = 1200; // ~20.0s silence (User request: "Keep button working / Don't cut")
|
| 830 |
+
|
| 831 |
+
function monitorAudio() {
|
| 832 |
+
if (!window.continuousMode || !isRecording) {
|
| 833 |
+
console.log("🛑 Audio monitoring stopped");
|
| 834 |
+
return;
|
| 835 |
+
}
|
| 836 |
+
|
| 837 |
+
// 🔇 Skip monitoring while TTS is playing
|
| 838 |
+
if (isTTSPlaying) {
|
| 839 |
+
requestAnimationFrame(monitorAudio);
|
| 840 |
+
return;
|
| 841 |
+
}
|
| 842 |
+
|
| 843 |
+
analyser.getByteFrequencyData(dataArray);
|
| 844 |
+
let sum = 0;
|
| 845 |
+
for (let i = 0; i < bufferLength; i++) sum += dataArray[i];
|
| 846 |
+
const average = sum / bufferLength;
|
| 847 |
+
|
| 848 |
+
// 🎯 Better noise filtering
|
| 849 |
+
// Use local VOLUME_THRESHOLD if defined, else generic 10
|
| 850 |
+
if (average > 4) { // Hardcoded decent threshold for consistency
|
| 851 |
+
consecutiveSpeechFrames++;
|
| 852 |
+
consecutiveSilenceFrames = 0;
|
| 853 |
+
|
| 854 |
+
// Only mark as speech after consecutive loud frames (avoid false starts)
|
| 855 |
+
if (consecutiveSpeechFrames >= SPEECH_FRAMES_THRESHOLD && !speechDetected) {
|
| 856 |
+
speechDetected = true;
|
| 857 |
+
silenceStart = Date.now();
|
| 858 |
+
console.log("🗣️ Speech confirmed (filtered noise)");
|
| 859 |
+
statusText.innerText = '🎤 Enregistrement...';
|
| 860 |
+
statusText.style.color = '#ff4444';
|
| 861 |
+
recordBtn.classList.add('active-speech'); // ✅ FIXED: Match CSS class
|
| 862 |
+
}
|
| 863 |
+
} else {
|
| 864 |
+
consecutiveSpeechFrames = 0;
|
| 865 |
+
consecutiveSilenceFrames++;
|
| 866 |
+
|
| 867 |
+
if (!speechDetected) {
|
| 868 |
+
// Still waiting for speech
|
| 869 |
+
if (!statusText.innerText.includes('Traitement')) {
|
| 870 |
+
statusText.innerText = '💤 En attente de parole...';
|
| 871 |
+
statusText.style.color = '#888';
|
| 872 |
+
}
|
| 873 |
+
} else {
|
| 874 |
+
// Speech detected before, now checking for end
|
| 875 |
+
if (consecutiveSilenceFrames >= SILENCE_FRAMES_THRESHOLD) {
|
| 876 |
+
console.log('🤫 Silence confirmed - ending speech');
|
| 877 |
+
consecutiveSpeechFrames = 0;
|
| 878 |
+
consecutiveSilenceFrames = 0;
|
| 879 |
+
// speechDetected = false; // ❌ DON'T RESET HERE! Needed for onstop check.
|
| 880 |
+
|
| 881 |
+
isRecording = false;
|
| 882 |
+
recordBtn.classList.remove('active-speech'); // ✅ FIXED: Match CSS class
|
| 883 |
+
|
| 884 |
+
// Stop recorder to process
|
| 885 |
+
if (mediaRecorder && mediaRecorder.state === 'recording') {
|
| 886 |
+
mediaRecorder.stop();
|
| 887 |
+
}
|
| 888 |
+
return;
|
| 889 |
+
}
|
| 890 |
+
}
|
| 891 |
+
}
|
| 892 |
+
|
| 893 |
+
animationId = requestAnimationFrame(monitorAudio);
|
| 894 |
+
}
|
| 895 |
+
|
| 896 |
+
monitorAudio();
|
| 897 |
+
|
| 898 |
+
} catch (err) {
|
| 899 |
+
console.error('Erreur listenContinuously:', err);
|
| 900 |
+
window.continuousMode = false;
|
| 901 |
+
recordBtn.classList.remove('active');
|
| 902 |
+
}
|
| 903 |
+
}
|
| 904 |
+
|
| 905 |
+
// --- REAL-TIME TRANSCRIPTION (Consolidated & Cleaned) ---
|
| 906 |
+
// This function handles browser-based speech recognition for instant feedback
|
| 907 |
+
// ⚠️ NOTE: Browser SpeechRecognition does NOT support Darija well - we use it only for visual feedback
|
| 908 |
+
|
| 909 |
+
let arabicModeActive = false; // Track if we're in Arabic/Darija mode
|
| 910 |
+
|
| 911 |
+
function startRealTimeTranscription() {
|
| 912 |
+
const SpeechRecognition = window.SpeechRecognition || window.webkitSpeechRecognition;
|
| 913 |
+
|
| 914 |
+
// 1. Check Browser Support
|
| 915 |
+
if (!SpeechRecognition) {
|
| 916 |
+
console.warn("⚠️ Browser Speech Recognition not supported.");
|
| 917 |
+
return;
|
| 918 |
+
}
|
| 919 |
+
|
| 920 |
+
// 2. Prevent Multiple Instances
|
| 921 |
+
if (recognition) {
|
| 922 |
+
try { recognition.stop(); } catch (e) { }
|
| 923 |
+
recognition = null;
|
| 924 |
+
}
|
| 925 |
+
|
| 926 |
+
// 🧹 Reset global capture
|
| 927 |
+
window.currentTranscript = "";
|
| 928 |
+
|
| 929 |
+
try {
|
| 930 |
+
// 3. Get language from QUICK SELECTORS (visible in UI)
|
| 931 |
+
const sourceLangQuick = document.getElementById('source-lang-quick');
|
| 932 |
+
const targetLangQuick = document.getElementById('target-lang-quick');
|
| 933 |
+
|
| 934 |
+
const targetLang = targetLangQuick?.value || quickLangSelector?.value || 'French';
|
| 935 |
+
let sourceLang = sourceLangQuick?.value || localStorage.getItem('sourceLangQuick') || 'auto';
|
| 936 |
+
|
| 937 |
+
console.log(`🎯 Quick Selectors: Source=${sourceLang}, Target=${targetLang}`);
|
| 938 |
+
|
| 939 |
+
// 🇲🇦 SMART MODE: Only activate if source is 'auto'
|
| 940 |
+
if (sourceLang === 'auto') {
|
| 941 |
+
// Smart detection based on target language
|
| 942 |
+
if (targetLang === 'French') {
|
| 943 |
+
sourceLang = 'ar-SA'; // Likely speaking Arabic/Darija
|
| 944 |
+
arabicModeActive = true;
|
| 945 |
+
console.log('🇲🇦 AUTO MODE: Target=French → Assuming Arabic/Darija');
|
| 946 |
+
} else if (targetLang === 'Moroccan Darija' || targetLang === 'Arabic') {
|
| 947 |
+
sourceLang = 'fr-FR'; // Likely speaking French
|
| 948 |
+
arabicModeActive = false;
|
| 949 |
+
console.log('🇫🇷 AUTO MODE: Target=Arabic → Assuming French');
|
| 950 |
+
} else if (targetLang === 'English') {
|
| 951 |
+
// Default to Arabic for Moroccan users, but check cache
|
| 952 |
+
sourceLang = detectedLanguage === 'French' ? 'fr-FR' : 'ar-SA';
|
| 953 |
+
arabicModeActive = sourceLang === 'ar-SA';
|
| 954 |
+
console.log(`🌍 AUTO MODE: Target=English → Assuming ${sourceLang}`);
|
| 955 |
+
} else {
|
| 956 |
+
sourceLang = 'fr-FR'; // Default fallback
|
| 957 |
+
arabicModeActive = false;
|
| 958 |
+
}
|
| 959 |
+
} else {
|
| 960 |
+
// MANUAL MODE: User selected specific source language
|
| 961 |
+
arabicModeActive = sourceLang === 'ar-SA';
|
| 962 |
+
console.log(`📌 MANUAL MODE: Source=${sourceLang} (Arabic mode: ${arabicModeActive})`);
|
| 963 |
+
}
|
| 964 |
+
|
| 965 |
+
// 4. Configure Recognition with selected language
|
| 966 |
+
recognition = new SpeechRecognition();
|
| 967 |
+
recognition.continuous = true;
|
| 968 |
+
recognition.interimResults = true;
|
| 969 |
+
recognition.lang = sourceLang;
|
| 970 |
+
currentRecognitionLang = sourceLang;
|
| 971 |
+
|
| 972 |
+
console.log(`🎤 Browser Recognition: ${sourceLang} (Arabic mode: ${arabicModeActive})`);
|
| 973 |
+
|
| 974 |
+
// 5. Event Handlers
|
| 975 |
+
recognition.onstart = () => {
|
| 976 |
+
console.log("✅ Real-time transcription active");
|
| 977 |
+
if (navigator.vibrate) navigator.vibrate(50); // Haptic feedback
|
| 978 |
+
};
|
| 979 |
+
|
| 980 |
+
recognition.onerror = (event) => {
|
| 981 |
+
console.warn("❌ Recognition error:", event.error);
|
| 982 |
+
if (event.error === 'not-allowed') {
|
| 983 |
+
statusText.innerText = "⚠️ Accès micro refusé";
|
| 984 |
+
statusText.style.color = "yellow";
|
| 985 |
+
} else if (event.error !== 'aborted') {
|
| 986 |
+
// Restart recognition on non-critical errors (in continuous mode)
|
| 987 |
+
if (window.continuousMode && isRecording) {
|
| 988 |
+
console.log("🔄 Restarting recognition after error...");
|
| 989 |
+
setTimeout(() => {
|
| 990 |
+
if (window.continuousMode && isRecording) {
|
| 991 |
+
try { recognition.start(); } catch (e) { }
|
| 992 |
+
}
|
| 993 |
+
}, 500);
|
| 994 |
+
}
|
| 995 |
+
}
|
| 996 |
+
};
|
| 997 |
+
|
| 998 |
+
recognition.onend = () => {
|
| 999 |
+
console.log("🔄 Recognition ended");
|
| 1000 |
+
|
| 1001 |
+
// Auto-restart in continuous mode (unless TTS is playing)
|
| 1002 |
+
if (window.continuousMode && isRecording) {
|
| 1003 |
+
if (isTTSPlaying) {
|
| 1004 |
+
console.log("⏸️ TTS is playing - recognition will restart when TTS ends");
|
| 1005 |
+
// Don't restart now - the audio.onended handler will do it
|
| 1006 |
+
} else {
|
| 1007 |
+
console.log("🔄 Auto-restarting recognition for continuous mode...");
|
| 1008 |
+
setTimeout(() => {
|
| 1009 |
+
if (window.continuousMode && isRecording && !isTTSPlaying) {
|
| 1010 |
+
try {
|
| 1011 |
+
recognition.start();
|
| 1012 |
+
console.log("✅ Recognition restarted successfully");
|
| 1013 |
+
} catch (e) {
|
| 1014 |
+
console.warn("Could not restart recognition:", e);
|
| 1015 |
+
}
|
| 1016 |
+
}
|
| 1017 |
+
}, 300);
|
| 1018 |
+
}
|
| 1019 |
+
}
|
| 1020 |
+
};
|
| 1021 |
+
|
| 1022 |
+
recognition.onresult = (event) => {
|
| 1023 |
+
let interimTranscript = '';
|
| 1024 |
+
let finalTranscript = '';
|
| 1025 |
+
|
| 1026 |
+
for (let i = event.resultIndex; i < event.results.length; ++i) {
|
| 1027 |
+
const transcript = event.results[i][0].transcript;
|
| 1028 |
+
if (event.results[i].isFinal) {
|
| 1029 |
+
finalTranscript += transcript;
|
| 1030 |
+
} else {
|
| 1031 |
+
interimTranscript += transcript;
|
| 1032 |
+
}
|
| 1033 |
+
}
|
| 1034 |
+
|
| 1035 |
+
// ✨ AFFICHAGE INSTANTANÉ - LYRICS STYLE
|
| 1036 |
+
const fullText = finalTranscript || interimTranscript;
|
| 1037 |
+
|
| 1038 |
+
// 🚀 STEAL THE MICROPHONE: Store global transcript for processAudio to pick up
|
| 1039 |
+
if (finalTranscript.trim().length > 0) {
|
| 1040 |
+
window.currentTranscript = finalTranscript;
|
| 1041 |
+
} else if (interimTranscript.trim().length > 0) {
|
| 1042 |
+
window.currentTranscript = interimTranscript;
|
| 1043 |
+
}
|
| 1044 |
+
|
| 1045 |
+
if (fullText.trim().length > 0) {
|
| 1046 |
+
// 🇲🇦 ARABIC MODE: Browser recognition is unreliable for Arabic/Darija
|
| 1047 |
+
// Show the text but with a note that final transcription will be better
|
| 1048 |
+
if (arabicModeActive) {
|
| 1049 |
+
// For Arabic, only show if it looks like actual Arabic text
|
| 1050 |
+
const hasArabicChars = /[\u0600-\u06FF]/.test(fullText);
|
| 1051 |
+
if (hasArabicChars && originalTextField) {
|
| 1052 |
+
originalTextField.innerText = fullText;
|
| 1053 |
+
originalTextField.hidden = false;
|
| 1054 |
+
originalTextField.style.opacity = '1';
|
| 1055 |
+
originalTextField.style.direction = 'rtl';
|
| 1056 |
+
originalTextField.style.textAlign = 'right';
|
| 1057 |
+
originalTextField.style.fontSize = '1.3rem';
|
| 1058 |
+
originalTextField.style.fontWeight = '500';
|
| 1059 |
+
} else {
|
| 1060 |
+
// Browser gave garbage (Latin chars for Arabic speech) - show waiting status
|
| 1061 |
+
if (originalTextField) {
|
| 1062 |
+
originalTextField.innerText = '🎤 جاري الاستماع...'; // "Listening..." in Arabic
|
| 1063 |
+
originalTextField.style.direction = 'rtl';
|
| 1064 |
+
originalTextField.style.textAlign = 'right';
|
| 1065 |
+
originalTextField.style.opacity = '0.7';
|
| 1066 |
+
}
|
| 1067 |
+
}
|
| 1068 |
+
} else {
|
| 1069 |
+
// French/English mode - show normally
|
| 1070 |
+
if (originalTextField) {
|
| 1071 |
+
originalTextField.innerText = fullText;
|
| 1072 |
+
originalTextField.hidden = false;
|
| 1073 |
+
originalTextField.style.opacity = '1';
|
| 1074 |
+
originalTextField.style.direction = 'ltr';
|
| 1075 |
+
originalTextField.style.textAlign = 'left';
|
| 1076 |
+
originalTextField.style.fontSize = '1.2rem';
|
| 1077 |
+
originalTextField.style.fontWeight = '500';
|
| 1078 |
+
originalTextField.style.lineHeight = '1.6';
|
| 1079 |
+
originalTextField.style.fontStyle = 'normal';
|
| 1080 |
+
originalTextField.style.animation = 'fadeIn 0.3s ease';
|
| 1081 |
+
}
|
| 1082 |
+
}
|
| 1083 |
+
|
| 1084 |
+
// Scroll to bottom
|
| 1085 |
+
const chatHistory = document.getElementById('chat-history');
|
| 1086 |
+
if (chatHistory) chatHistory.scrollTop = chatHistory.scrollHeight;
|
| 1087 |
+
|
| 1088 |
+
// ✨ Recognition is ONLY for visual display
|
| 1089 |
+
if (finalTranscript.trim().length > 2) {
|
| 1090 |
+
console.log("✅ Sentence transcribed (visual feedback only)");
|
| 1091 |
+
}
|
| 1092 |
+
}
|
| 1093 |
+
};
|
| 1094 |
+
|
| 1095 |
+
// 6. Start
|
| 1096 |
+
recognition.start();
|
| 1097 |
+
|
| 1098 |
+
} catch (e) {
|
| 1099 |
+
console.error("❌ Fatal Error starting recognition:", e);
|
| 1100 |
+
}
|
| 1101 |
+
}
|
| 1102 |
+
|
| 1103 |
+
// 🚀 INTELLIGENT MODE: Send Text directly (Primary trigger via isFinal)
|
| 1104 |
+
async function sendTextForProcessing(text) {
|
| 1105 |
+
if (isProcessingAudio) {
|
| 1106 |
+
console.log("⚠️ Already processing, skipping duplicate...");
|
| 1107 |
+
return;
|
| 1108 |
+
}
|
| 1109 |
+
isProcessingAudio = true;
|
| 1110 |
+
|
| 1111 |
+
const targetLang = quickLangSelector?.value || 'French';
|
| 1112 |
+
|
| 1113 |
+
// 🌍 Language detection is now handled by the backend with Gemini
|
| 1114 |
+
console.log(`📤 Sending text for processing: "${text}"`);
|
| 1115 |
+
statusText.innerText = 'Traduction en cours...';
|
| 1116 |
+
statusText.style.color = '#4a9b87';
|
| 1117 |
+
|
| 1118 |
+
const payload = {
|
| 1119 |
+
text_input: text, // Sending TEXT, not AUDIO
|
| 1120 |
+
source_language: 'auto', // Let backend (Gemini) detect language
|
| 1121 |
+
target_language: targetLang, // Use quick-lang-selector
|
| 1122 |
+
model: localStorage.getItem('selectedModel') || 'Gemini', // Updated default
|
| 1123 |
+
tts_engine: localStorage.getItem('ttsEngine') || 'openai', // Updated default
|
| 1124 |
+
stt_engine: localStorage.getItem('sttEngine') || 'seamless-m4t', // NEW: SeamlessM4T default
|
| 1125 |
+
ai_correction: localStorage.getItem('aiCorrectionEnabled') !== 'false', // NEW: AI Correction enabled by default
|
| 1126 |
+
voice_cloning: false,
|
| 1127 |
+
use_grammar_correction: localStorage.getItem('grammarCorrectionEnabled') !== 'false',
|
| 1128 |
+
voice_gender_preference: localStorage.getItem('voiceGenderPreference') || 'auto'
|
| 1129 |
+
};
|
| 1130 |
+
|
| 1131 |
+
try {
|
| 1132 |
+
const response = await fetch('/process_audio', {
|
| 1133 |
+
method: 'POST',
|
| 1134 |
+
headers: { 'Content-Type': 'application/json' },
|
| 1135 |
+
body: JSON.stringify(payload)
|
| 1136 |
+
});
|
| 1137 |
+
|
| 1138 |
+
const data = await response.json();
|
| 1139 |
+
|
| 1140 |
+
if (data.error) {
|
| 1141 |
+
console.error("❌ Processing error:", data.error);
|
| 1142 |
+
statusText.innerText = 'Erreur';
|
| 1143 |
+
} else {
|
| 1144 |
+
// Handle Success - Update translated text display only
|
| 1145 |
+
// Chat messages are created by the main axios handler to avoid duplicates
|
| 1146 |
+
if (translatedTextField) {
|
| 1147 |
+
translatedTextField.innerText = data.translated_text;
|
| 1148 |
+
translatedTextField.style.opacity = '1';
|
| 1149 |
+
}
|
| 1150 |
+
|
| 1151 |
+
// 🧠 SMART MODE LATCHING: Update recognition language for the NEXT turn
|
| 1152 |
+
if (data.source_language_full) {
|
| 1153 |
+
const newLang = data.source_language_full;
|
| 1154 |
+
console.log(`🧠 SMART MODE: Latching onto detected language: ${newLang}`);
|
| 1155 |
+
|
| 1156 |
+
// Update the global state so startRealTimeTranscription uses it
|
| 1157 |
+
// We map standard names to BCP-47 codes for SpeechRecognition
|
| 1158 |
+
const langToCode = {
|
| 1159 |
+
'French': 'fr-FR',
|
| 1160 |
+
'English': 'en-US',
|
| 1161 |
+
'Arabic': 'ar-SA', // Default to SA for generic, or MA if available
|
| 1162 |
+
'Moroccan Darija': 'ar-MA', // Chrome might treat this as ar-SA or similar
|
| 1163 |
+
'Spanish': 'es-ES',
|
| 1164 |
+
'German': 'de-DE',
|
| 1165 |
+
'Italian': 'it-IT',
|
| 1166 |
+
'Portuguese': 'pt-PT',
|
| 1167 |
+
'Russian': 'ru-RU',
|
| 1168 |
+
'Japanese': 'ja-JP',
|
| 1169 |
+
'Korean': 'ko-KR',
|
| 1170 |
+
'Chinese': 'zh-CN',
|
| 1171 |
+
'Hindi': 'hi-IN'
|
| 1172 |
+
};
|
| 1173 |
+
|
| 1174 |
+
const code = langToCode[newLang];
|
| 1175 |
+
if (code) {
|
| 1176 |
+
currentRecognitionLang = code;
|
| 1177 |
+
detectedLanguage = newLang; // Update global detected
|
| 1178 |
+
|
| 1179 |
+
// If we are in AUTO mode, this is critical
|
| 1180 |
+
if (document.getElementById('source-lang-selector').value === 'auto') {
|
| 1181 |
+
console.log(`🔄 UPDATING RECOGNITION to ${code} for next turn`);
|
| 1182 |
+
|
| 1183 |
+
// Restart recognition if it's running, to apply new language
|
| 1184 |
+
if (recognition) {
|
| 1185 |
+
try { recognition.stop(); } catch (e) { }
|
| 1186 |
+
// It will auto-restart via the 'end' event or our continuous loop
|
| 1187 |
+
}
|
| 1188 |
+
}
|
| 1189 |
+
}
|
| 1190 |
+
}
|
| 1191 |
+
|
| 1192 |
+
console.log("✅ Text processing complete - TTS will play automatically");
|
| 1193 |
+
|
| 1194 |
+
// Update status for continuous mode
|
| 1195 |
+
if (window.continuousMode) {
|
| 1196 |
+
statusText.innerText = '🔊 Lecture TTS...';
|
| 1197 |
+
statusText.style.color = '#4a9b87';
|
| 1198 |
+
console.log('🎙️ Continuous mode active - will resume listening after TTS');
|
| 1199 |
+
} else {
|
| 1200 |
+
statusText.innerText = 'Prêt';
|
| 1201 |
+
}
|
| 1202 |
+
}
|
| 1203 |
+
} catch (e) {
|
| 1204 |
+
console.error("❌ Text processing error:", e);
|
| 1205 |
+
statusText.innerText = 'Erreur réseau';
|
| 1206 |
+
} finally {
|
| 1207 |
+
isProcessingAudio = false;
|
| 1208 |
+
}
|
| 1209 |
+
}
|
| 1210 |
+
|
| 1211 |
+
// --- RECORDER LOGIC ---
|
| 1212 |
+
|
| 1213 |
+
// Silence Detection Config - Moved to top of file
|
| 1214 |
+
// CONSTANTS ARE GLOBAL NOW
|
| 1215 |
+
async function startSmartRecording() {
|
| 1216 |
+
try {
|
| 1217 |
+
console.log('🎤 STARTING RECORDING...');
|
| 1218 |
+
isRecording = true;
|
| 1219 |
+
recordBtn.classList.add('active');
|
| 1220 |
+
statusText.innerText = 'Écoute...';
|
| 1221 |
+
statusText.style.color = 'white';
|
| 1222 |
+
|
| 1223 |
+
document.dispatchEvent(new Event('reset-ui'));
|
| 1224 |
+
originalTextField.innerText = '...';
|
| 1225 |
+
translatedTextField.innerText = '...';
|
| 1226 |
+
|
| 1227 |
+
// 🎤 EXPERT MICROPHONE CONFIGURATION
|
| 1228 |
+
const stream = await navigator.mediaDevices.getUserMedia({
|
| 1229 |
+
audio: {
|
| 1230 |
+
echoCancellation: true, // 🛡️ Prevent Speaker Feedack
|
| 1231 |
+
noiseSuppression: true, // 🔇 Remove Background Noise
|
| 1232 |
+
autoGainControl: true, // 🎚️ Normalize Volume
|
| 1233 |
+
channelCount: 1,
|
| 1234 |
+
sampleRate: 48000
|
| 1235 |
+
}
|
| 1236 |
+
});
|
| 1237 |
+
|
| 1238 |
+
// 1. Setup Audio Analysis (Silence Detection)
|
| 1239 |
+
audioContext = new (window.AudioContext || window.webkitAudioContext)();
|
| 1240 |
+
|
| 1241 |
+
// ⚡ EXPERT: Force Active Context (Wake up the Audio Engine)
|
| 1242 |
+
if (audioContext.state === 'suspended') {
|
| 1243 |
+
await audioContext.resume();
|
| 1244 |
+
console.log('⚡ AudioContext Force-Resumed');
|
| 1245 |
+
}
|
| 1246 |
+
analyser = audioContext.createAnalyser();
|
| 1247 |
+
micSource = audioContext.createMediaStreamSource(stream);
|
| 1248 |
+
micSource.connect(analyser);
|
| 1249 |
+
analyser.fftSize = 256;
|
| 1250 |
+
const bufferLength = analyser.frequencyBinCount;
|
| 1251 |
+
const dataArray = new Uint8Array(bufferLength);
|
| 1252 |
+
|
| 1253 |
+
let silenceStart = Date.now();
|
| 1254 |
+
|
| 1255 |
+
// Flag to track if human speech was actually detected
|
| 1256 |
+
let smartSpeechDetected = false;
|
| 1257 |
+
|
| 1258 |
+
function detectSilence() {
|
| 1259 |
+
if (!isRecording) return;
|
| 1260 |
+
|
| 1261 |
+
analyser.getByteFrequencyData(dataArray);
|
| 1262 |
+
|
| 1263 |
+
// Calculate average volume
|
| 1264 |
+
let sum = 0;
|
| 1265 |
+
for (let i = 0; i < bufferLength; i++) sum += dataArray[i];
|
| 1266 |
+
const average = sum / bufferLength;
|
| 1267 |
+
|
| 1268 |
+
// Visual feedback
|
| 1269 |
+
const scale = 1 + (average / 100);
|
| 1270 |
+
recordBtn.style.transform = `scale(${Math.min(scale, 1.2)})`;
|
| 1271 |
+
|
| 1272 |
+
// UI Feedback for waiting status
|
| 1273 |
+
if (average < VOLUME_THRESHOLD && !smartSpeechDetected) {
|
| 1274 |
+
statusText.innerText = '💤 En attente de parole...';
|
| 1275 |
+
statusText.style.color = 'rgba(255,255,255,0.7)';
|
| 1276 |
+
}
|
| 1277 |
+
|
| 1278 |
+
if (average < VOLUME_THRESHOLD) {
|
| 1279 |
+
// It is silent
|
| 1280 |
+
if (Date.now() - silenceStart > SILENCE_LIMIT_MS) {
|
| 1281 |
+
// Silence limit reached! Stop!
|
| 1282 |
+
console.log("🤫 Silence limit reached.");
|
| 1283 |
+
stopSmartRecording();
|
| 1284 |
+
return;
|
| 1285 |
+
}
|
| 1286 |
+
} else {
|
| 1287 |
+
// Sound detected!
|
| 1288 |
+
silenceStart = Date.now();
|
| 1289 |
+
if (!smartSpeechDetected) {
|
| 1290 |
+
smartSpeechDetected = true; // ✅ Valid speech detected
|
| 1291 |
+
console.log("🗣️ Speech detected!");
|
| 1292 |
+
statusText.innerText = '🎤 Je vous écoute...';
|
| 1293 |
+
statusText.style.color = '#fff';
|
| 1294 |
+
recordBtn.classList.add('active-speech');
|
| 1295 |
+
}
|
| 1296 |
+
}
|
| 1297 |
+
|
| 1298 |
+
animationId = requestAnimationFrame(detectSilence);
|
| 1299 |
+
}
|
| 1300 |
+
|
| 1301 |
+
detectSilence(); // Start monitoring
|
| 1302 |
+
|
| 1303 |
+
// 2. Start Speech Recognition (Instant Mode)
|
| 1304 |
+
try { startRealTimeTranscription(); } catch (e) { }
|
| 1305 |
+
|
| 1306 |
+
// 3. Start MediaRecorder
|
| 1307 |
+
mediaRecorder = new MediaRecorder(stream);
|
| 1308 |
+
audioChunks = [];
|
| 1309 |
+
mediaRecorder.ondataavailable = e => audioChunks.push(e.data);
|
| 1310 |
+
mediaRecorder.onstop = async () => {
|
| 1311 |
+
console.log("🛑 Recorder stopped. Processing audio...");
|
| 1312 |
+
|
| 1313 |
+
// Clean up
|
| 1314 |
+
if (recognition) { try { recognition.stop(); } catch (e) { } }
|
| 1315 |
+
if (animationId) cancelAnimationFrame(animationId);
|
| 1316 |
+
if (micSource) micSource.disconnect();
|
| 1317 |
+
if (audioContext) audioContext.close();
|
| 1318 |
+
|
| 1319 |
+
if (audioChunks.length > 0) {
|
| 1320 |
+
const blob = new Blob(audioChunks, { type: 'audio/wav' });
|
| 1321 |
+
console.log(`📦 Audio Data: ${blob.size} bytes`);
|
| 1322 |
+
|
| 1323 |
+
// FORCE PROCESSING - UI Feedback
|
| 1324 |
+
statusText.innerText = 'Traitement...';
|
| 1325 |
+
statusText.style.color = '#4a9b87';
|
| 1326 |
+
|
| 1327 |
+
try {
|
| 1328 |
+
await processAudio(blob);
|
| 1329 |
+
} catch (e) {
|
| 1330 |
+
console.error("Error in processAudio", e);
|
| 1331 |
+
statusText.innerText = 'Erreur';
|
| 1332 |
+
}
|
| 1333 |
+
} else {
|
| 1334 |
+
console.error("❌ Audio was empty!");
|
| 1335 |
+
statusText.innerText = 'Audio Vide';
|
| 1336 |
+
}
|
| 1337 |
+
|
| 1338 |
+
// 🔄 AUTO-RESTART LOOP (Crucial for Continuous Conversation)
|
| 1339 |
+
if (window.continuousMode) {
|
| 1340 |
+
// 🛑 CRITICAL FIX: DO NOT RESTART IF TTS IS PLAYING!
|
| 1341 |
+
if (isTTSPlaying && window.lastBotAudio) {
|
| 1342 |
+
console.log("⏸️ TTS Playing - Waiting for audio to finish before restarting...");
|
| 1343 |
+
|
| 1344 |
+
// Chain the restart to the onended event
|
| 1345 |
+
const originalEnded = window.lastBotAudio.onended;
|
| 1346 |
+
window.lastBotAudio.onended = () => {
|
| 1347 |
+
if (originalEnded) originalEnded();
|
| 1348 |
+
console.log("✅ TTS Finished - Restarting conversation loop");
|
| 1349 |
+
// Small delay to ensure clean state
|
| 1350 |
+
setTimeout(() => {
|
| 1351 |
+
if (window.continuousMode) listenContinuously();
|
| 1352 |
+
}, 100);
|
| 1353 |
+
};
|
| 1354 |
+
return;
|
| 1355 |
+
} else {
|
| 1356 |
+
// No audio playing, restart immediately
|
| 1357 |
+
console.log("🔄 Auto-restarting conversation loop (No TTS active)...");
|
| 1358 |
+
setTimeout(() => {
|
| 1359 |
+
if (window.continuousMode) listenContinuously();
|
| 1360 |
+
}, 100);
|
| 1361 |
+
}
|
| 1362 |
+
} else {
|
| 1363 |
+
statusText.innerText = 'Prêt';
|
| 1364 |
+
}
|
| 1365 |
+
};
|
| 1366 |
+
mediaRecorder.start();
|
| 1367 |
+
console.log("🎤 Recording started (with Auto-Stop)...");
|
| 1368 |
+
|
| 1369 |
+
} catch (err) {
|
| 1370 |
+
console.error(err);
|
| 1371 |
+
statusText.innerText = "Erreur Micro";
|
| 1372 |
+
isRecording = false;
|
| 1373 |
+
recordBtn.classList.remove('active');
|
| 1374 |
+
}
|
| 1375 |
+
}
|
| 1376 |
+
|
| 1377 |
+
function stopSmartRecording() {
|
| 1378 |
+
if (mediaRecorder && mediaRecorder.state !== 'inactive') mediaRecorder.stop();
|
| 1379 |
+
if (recognition) { try { recognition.stop(); } catch (e) { } }
|
| 1380 |
+
isRecording = false;
|
| 1381 |
+
recordBtn.classList.remove('active');
|
| 1382 |
+
statusText.innerText = 'Réflexion...';
|
| 1383 |
+
}
|
| 1384 |
+
|
| 1385 |
+
// [Function setupRealTimeTranscription removed - Consolidated into startRealTimeTranscription]
|
| 1386 |
+
|
| 1387 |
+
function debouncedStreamTranslation(text) {
|
| 1388 |
+
if (streamTimeout) clearTimeout(streamTimeout);
|
| 1389 |
+
streamTimeout = setTimeout(() => performStreamTranslation(text), 200);
|
| 1390 |
+
}
|
| 1391 |
+
|
| 1392 |
+
async function performStreamTranslation(text) {
|
| 1393 |
+
try {
|
| 1394 |
+
const res = await axios.post('/stream_text', {
|
| 1395 |
+
text: text,
|
| 1396 |
+
target_lang: quickLangSelector?.value || 'English'
|
| 1397 |
+
});
|
| 1398 |
+
if (res.data.translation) {
|
| 1399 |
+
translatedTextField.innerText = res.data.translation;
|
| 1400 |
+
|
| 1401 |
+
// ✨ SHOW TRANSLATION CARD - Make it visible!
|
| 1402 |
+
if (res.data.translation.trim().length > 0) {
|
| 1403 |
+
translatedTextField.style.opacity = '1';
|
| 1404 |
+
console.log('🌍 Real-time translation:', res.data.translation);
|
| 1405 |
+
}
|
| 1406 |
+
}
|
| 1407 |
+
} catch (e) { console.error("Stream Error", e); }
|
| 1408 |
+
}
|
| 1409 |
+
|
| 1410 |
+
// Helper function to analyze audio energy and detect silence
|
| 1411 |
+
function analyzeAudioEnergy(blob) {
|
| 1412 |
+
return new Promise((resolve) => {
|
| 1413 |
+
const reader = new FileReader();
|
| 1414 |
+
reader.readAsArrayBuffer(blob);
|
| 1415 |
+
reader.onloadend = async () => {
|
| 1416 |
+
try {
|
| 1417 |
+
const audioContext = new (window.AudioContext || window.webkitAudioContext)();
|
| 1418 |
+
const audioBuffer = await audioContext.decodeAudioData(reader.result);
|
| 1419 |
+
|
| 1420 |
+
// Get audio samples
|
| 1421 |
+
const channelData = audioBuffer.getChannelData(0);
|
| 1422 |
+
|
| 1423 |
+
// Calculate RMS (Root Mean Square) energy
|
| 1424 |
+
let sum = 0;
|
| 1425 |
+
for (let i = 0; i < channelData.length; i++) {
|
| 1426 |
+
sum += channelData[i] * channelData[i];
|
| 1427 |
+
}
|
| 1428 |
+
const rms = Math.sqrt(sum / channelData.length);
|
| 1429 |
+
|
| 1430 |
+
// Calculate peak amplitude
|
| 1431 |
+
let peak = 0;
|
| 1432 |
+
for (let i = 0; i < channelData.length; i++) {
|
| 1433 |
+
const abs = Math.abs(channelData[i]);
|
| 1434 |
+
if (abs > peak) peak = abs;
|
| 1435 |
+
}
|
| 1436 |
+
|
| 1437 |
+
// Duration in seconds
|
| 1438 |
+
const duration = audioBuffer.duration;
|
| 1439 |
+
|
| 1440 |
+
console.log(`🔊 Audio Analysis: RMS=${rms.toFixed(4)}, Peak=${peak.toFixed(4)}, Duration=${duration.toFixed(2)}s`);
|
| 1441 |
+
|
| 1442 |
+
// 🛡️ WAR MODE SENSITIVITY: Pick up even whispers (0.002)
|
| 1443 |
+
// Was 0.01 - Lowered to prevent cutting out during "Chaos"
|
| 1444 |
+
resolve({ rms, peak, duration, isSilent: rms < 0.002 && peak < 0.01 });
|
| 1445 |
+
} catch (e) {
|
| 1446 |
+
console.error('⚠️ Audio analysis failed:', e);
|
| 1447 |
+
resolve({ rms: 0, peak: 0, duration: 0, isSilent: true });
|
| 1448 |
+
}
|
| 1449 |
+
};
|
| 1450 |
+
});
|
| 1451 |
+
}
|
| 1452 |
+
|
| 1453 |
+
async function processAudio(blob, bypassSilenceCheck = false) {
|
| 1454 |
+
// 🚀 PREVENT DUPLICATE PROCESSING
|
| 1455 |
+
if (isProcessingAudio) {
|
| 1456 |
+
console.log('⚠️ Audio already being processed, skipping...');
|
| 1457 |
+
return;
|
| 1458 |
+
}
|
| 1459 |
+
|
| 1460 |
+
isProcessingAudio = true;
|
| 1461 |
+
recordBtn.classList.add('processing'); // 🔵 Visual Feedback: Blue Spinner
|
| 1462 |
+
recordBtn.classList.remove('active'); // Stop Red pulse
|
| 1463 |
+
recordBtn.classList.remove('active-speech'); // Stop Green pulse
|
| 1464 |
+
|
| 1465 |
+
// 🔊 CRITICAL: SILENCE DETECTION - Prevent hallucination
|
| 1466 |
+
// ⚡ OPTIMIZATION: If we already confirmed speech in monitorAudio, skip this heavy decoding!
|
| 1467 |
+
if (!bypassSilenceCheck) {
|
| 1468 |
+
const audioAnalysis = await analyzeAudioEnergy(blob);
|
| 1469 |
+
|
| 1470 |
+
// Reject if audio is too quiet (silence/background noise)
|
| 1471 |
+
if (audioAnalysis.isSilent) {
|
| 1472 |
+
console.warn('🔇 SILENCE DETECTED (Threshold check failed) - Skipping processing');
|
| 1473 |
+
console.log(`📊 Analysis: RMS=${audioAnalysis.rms}, Peak=${audioAnalysis.peak}`);
|
| 1474 |
+
statusText.innerText = 'Trop silencieux';
|
| 1475 |
+
isProcessingAudio = false;
|
| 1476 |
+
|
| 1477 |
+
// Reset UI
|
| 1478 |
+
// ⚡ WAR MODE: Restart INSTANTLY (100ms) insead of 1s
|
| 1479 |
+
setTimeout(() => {
|
| 1480 |
+
statusText.innerText = 'Prêt';
|
| 1481 |
+
// Force restart if in continuous mode!
|
| 1482 |
+
if (window.continuousMode) listenContinuously();
|
| 1483 |
+
}, 100);
|
| 1484 |
+
return;
|
| 1485 |
+
}
|
| 1486 |
+
|
| 1487 |
+
// Reject if audio is too short (likely just a click)
|
| 1488 |
+
if (audioAnalysis.duration < 0.5) {
|
| 1489 |
+
console.log(`⏱️ Audio too short (${audioAnalysis.duration.toFixed(2)}s) - Skipping`);
|
| 1490 |
+
statusText.innerText = 'Audio trop court';
|
| 1491 |
+
isProcessingAudio = false;
|
| 1492 |
+
|
| 1493 |
+
setTimeout(() => {
|
| 1494 |
+
statusText.innerText = 'Prêt';
|
| 1495 |
+
if (window.continuousMode) listenContinuously();
|
| 1496 |
+
}, 800);
|
| 1497 |
+
return;
|
| 1498 |
+
}
|
| 1499 |
+
} else {
|
| 1500 |
+
console.log("⚡ SPEED: Bypassing secondary silence check (Speech already confirmed)");
|
| 1501 |
+
}
|
| 1502 |
+
|
| 1503 |
+
console.log('✅ Audio validation passed - Processing...');
|
| 1504 |
+
|
| 1505 |
+
const startTime = Date.now();
|
| 1506 |
+
const reader = new FileReader();
|
| 1507 |
+
reader.readAsDataURL(blob);
|
| 1508 |
+
reader.onloadend = async () => {
|
| 1509 |
+
const base64 = reader.result.split(',')[1];
|
| 1510 |
+
try {
|
| 1511 |
+
// 🚀 STEAL THE MICROPHONE (Client-Side STT Injection)
|
| 1512 |
+
// Use the global variable captured by Web Speech API directly!
|
| 1513 |
+
// This is cleaner than reading DOM.
|
| 1514 |
+
let textInput = (window.currentTranscript || originalTextField.innerText || "").trim();
|
| 1515 |
+
|
| 1516 |
+
// Clean up placeholders
|
| 1517 |
+
textInput = textInput.replace('...', '').replace('🎤', '').trim();
|
| 1518 |
+
|
| 1519 |
+
// Filter out placeholder indicators
|
| 1520 |
+
if (textInput.includes('Écoute') || textInput.length < 2) {
|
| 1521 |
+
textInput = ''; // Empty = backend will use Whisper/Gemini transcription
|
| 1522 |
+
console.log('🎯 Using backend STT only (no client text available)');
|
| 1523 |
+
} else {
|
| 1524 |
+
console.log(`🎤 Client-Side STT Injected: "${textInput}" (Skipping Server STT)`);
|
| 1525 |
+
}
|
| 1526 |
+
|
| 1527 |
+
// Get languages from quick selectors
|
| 1528 |
+
const targetLangQuick = document.getElementById('target-lang-quick');
|
| 1529 |
+
const sourceLangQuick = document.getElementById('source-lang-quick');
|
| 1530 |
+
const selectedTarget = targetLangQuick?.value || quickLangSelector?.value || 'French';
|
| 1531 |
+
const selectedSource = sourceLangQuick?.value || 'auto';
|
| 1532 |
+
|
| 1533 |
+
const settings = {
|
| 1534 |
+
audio: base64,
|
| 1535 |
+
text_input: textInput, // Only send real transcribed text, not placeholders
|
| 1536 |
+
target_language: selectedTarget,
|
| 1537 |
+
source_language: selectedSource === 'auto' ? 'auto' : selectedSource, // Pass manual selection to backend
|
| 1538 |
+
stt_engine: localStorage.getItem('sttEngine') || 'openai-whisper', // ⚡ WHISPER (Requested by User)
|
| 1539 |
+
model: localStorage.getItem('aiModel') || 'gpt-4o-mini', // ✅ CHATGPT (Requested by User)
|
| 1540 |
+
tts_engine: localStorage.getItem('ttsEngine') || 'seamless', // 🔊 SEAMLESS TTS (Kaggle GPU - FREE!)
|
| 1541 |
+
openai_api_key: localStorage.getItem('openaiKey'),
|
| 1542 |
+
google_api_key: localStorage.getItem('googleKey'), // ✅ For Gemini STT
|
| 1543 |
+
openai_voice: localStorage.getItem('openaiVoice') || 'nova',
|
| 1544 |
+
elevenlabs_key: localStorage.getItem('elevenlabsKey'), // Fixed: elevenlabs_key not elevenlabs_api_key
|
| 1545 |
+
use_grammar_correction: localStorage.getItem('grammarCorrectionEnabled') !== 'false', // Default: enabled
|
| 1546 |
+
voice_gender_preference: localStorage.getItem('voiceGenderPreference') || 'auto' // 🎙️ Voice gender: auto/male/female
|
| 1547 |
+
};
|
| 1548 |
+
|
| 1549 |
+
console.log(`📝 Grammar Correction: ${settings.use_grammar_correction ? 'ENABLED (GPT)' : 'DISABLED (Direct Translation)'}`);
|
| 1550 |
+
console.log(`🎙️ Voice Gender Preference: ${settings.voice_gender_preference.toUpperCase()}`);
|
| 1551 |
+
|
| 1552 |
+
|
| 1553 |
+
// VOICE CLONING LOGIC - Check if enabled via toggle
|
| 1554 |
+
// 🚀 SPEED FIRST: Default is DISABLED for instant translations
|
| 1555 |
+
const voiceCloneEnabled = localStorage.getItem('voiceCloneEnabled') === 'true'; // Default: DISABLED
|
| 1556 |
+
const ttsEngine = settings.tts_engine;
|
| 1557 |
+
|
| 1558 |
+
console.log(`🎭 Voice Cloning Status: ${voiceCloneEnabled ? 'ENABLED' : 'DISABLED'}`);
|
| 1559 |
+
|
| 1560 |
+
// Send voice cloning data if enabled
|
| 1561 |
+
if (voiceCloneEnabled) {
|
| 1562 |
+
console.log('🎤 Voice Cloning ENABLED → Sending audio sample to server');
|
| 1563 |
+
settings.voice_audio = `data:audio/wav;base64,${base64}`;
|
| 1564 |
+
settings.voice_cloning = true;
|
| 1565 |
+
} else {
|
| 1566 |
+
console.log('🔇 Voice Cloning DISABLED → Using gender-matched fallback voices');
|
| 1567 |
+
settings.voice_cloning = false;
|
| 1568 |
+
}
|
| 1569 |
+
|
| 1570 |
+
const res = await axios.post('/process_audio', settings);
|
| 1571 |
+
|
| 1572 |
+
if (res.data.translated_text) {
|
| 1573 |
+
const translation = res.data.translated_text;
|
| 1574 |
+
const userText = settings.text_input;
|
| 1575 |
+
|
| 1576 |
+
console.log('✅ Response received:', {
|
| 1577 |
+
original: userText?.substring(0, 50),
|
| 1578 |
+
translation: translation?.substring(0, 50),
|
| 1579 |
+
hasAudio: !!res.data.tts_audio
|
| 1580 |
+
});
|
| 1581 |
+
|
| 1582 |
+
// 🔊 FORCE DISPLAY RESULT (Fallback)
|
| 1583 |
+
const resultDisplay = document.getElementById('result-display');
|
| 1584 |
+
const originalDisplay = document.getElementById('original-display');
|
| 1585 |
+
const translationDisplay = document.getElementById('translation-display');
|
| 1586 |
+
const pronunciationDisplay = document.getElementById('pronunciation-display');
|
| 1587 |
+
const greeting = document.getElementById('greeting');
|
| 1588 |
+
|
| 1589 |
+
if (resultDisplay && translationDisplay) {
|
| 1590 |
+
if (greeting) greeting.style.display = 'none';
|
| 1591 |
+
resultDisplay.style.display = 'block';
|
| 1592 |
+
if (originalDisplay) originalDisplay.innerText = userText || 'Audio input';
|
| 1593 |
+
|
| 1594 |
+
// Pronunciation
|
| 1595 |
+
if (pronunciationDisplay) {
|
| 1596 |
+
const pronunciation = res.data.pronunciation;
|
| 1597 |
+
if (pronunciation && pronunciation !== translation) {
|
| 1598 |
+
pronunciationDisplay.innerText = pronunciation;
|
| 1599 |
+
pronunciationDisplay.style.display = 'block';
|
| 1600 |
+
} else {
|
| 1601 |
+
pronunciationDisplay.style.display = 'none';
|
| 1602 |
+
}
|
| 1603 |
+
}
|
| 1604 |
+
|
| 1605 |
+
translationDisplay.innerText = translation;
|
| 1606 |
+
console.log('📺 Result displayed on screen');
|
| 1607 |
+
}
|
| 1608 |
+
|
| 1609 |
+
// 🔊 FORCE PLAY AUDIO
|
| 1610 |
+
if (res.data.tts_audio) {
|
| 1611 |
+
const audioSrc = `data:audio/mp3;base64,${res.data.tts_audio}`;
|
| 1612 |
+
const audio = new Audio(audioSrc);
|
| 1613 |
+
audio.play().then(() => {
|
| 1614 |
+
console.log('🔊 Audio playing!');
|
| 1615 |
+
}).catch(err => {
|
| 1616 |
+
console.log('❌ Auto-play blocked:', err);
|
| 1617 |
+
// Show play button
|
| 1618 |
+
if (translationDisplay) {
|
| 1619 |
+
translationDisplay.innerHTML += ' <button onclick="this.previousSibling.click()" style="background:#4CAF50;color:white;border:none;padding:5px 10px;border-radius:5px;cursor:pointer;">▶️ Play</button>';
|
| 1620 |
+
}
|
| 1621 |
+
});
|
| 1622 |
+
window.lastAudio = audio;
|
| 1623 |
+
}
|
| 1624 |
+
|
| 1625 |
+
// 🛡️ HALLUCINATION CHECK - Block fake Whisper outputs
|
| 1626 |
+
// userText already defined above
|
| 1627 |
+
if (isHallucination(userText) || isHallucination(translation)) {
|
| 1628 |
+
console.log(`🚫 HALLUCINATION DETECTED - Skipping message creation`);
|
| 1629 |
+
console.log(` User: "${userText}" | Translation: "${translation}"`);
|
| 1630 |
+
statusText.innerText = 'Prêt';
|
| 1631 |
+
isProcessingAudio = false;
|
| 1632 |
+
recordBtn.classList.remove('processing');
|
| 1633 |
+
return;
|
| 1634 |
+
}
|
| 1635 |
+
|
| 1636 |
+
// AUTOMATIC LANGUAGE DETECTION - Update UI
|
| 1637 |
+
if (res.data.source_language_full && sourceLangSelector) {
|
| 1638 |
+
const detectedLang = res.data.source_language_full;
|
| 1639 |
+
|
| 1640 |
+
// Store detected language for potential future auto-selection
|
| 1641 |
+
detectedLanguage = detectedLang;
|
| 1642 |
+
console.log(`🌍 Language auto-detected: ${detectedLang}`);
|
| 1643 |
+
|
| 1644 |
+
// Update real-time transcription language for next recording
|
| 1645 |
+
if (recognition) {
|
| 1646 |
+
const langMap = {
|
| 1647 |
+
'English': 'en-US', 'French': 'fr-FR', 'Spanish': 'es-ES',
|
| 1648 |
+
'German': 'de-DE', 'Italian': 'it-IT', 'Portuguese': 'pt-PT',
|
| 1649 |
+
'Russian': 'ru-RU', 'Japanese': 'ja-JP', 'Korean': 'ko-KR',
|
| 1650 |
+
'Chinese': 'zh-CN', 'Arabic': 'ar-SA', 'Hindi': 'hi-IN',
|
| 1651 |
+
'Dutch': 'nl-NL', 'Polish': 'pl-PL', 'Turkish': 'tr-TR',
|
| 1652 |
+
'Indonesian': 'id-ID', 'Malay': 'ms-MY', 'Thai': 'th-TH',
|
| 1653 |
+
'Vietnamese': 'vi-VN', 'Bengali': 'bn-IN', 'Urdu': 'ur-PK',
|
| 1654 |
+
'Swahili': 'sw-KE', 'Hebrew': 'he-IL', 'Persian': 'fa-IR',
|
| 1655 |
+
'Ukrainian': 'uk-UA', 'Swedish': 'sv-SE', 'Greek': 'el-GR',
|
| 1656 |
+
'Czech': 'cs-CZ', 'Romanian': 'ro-RO', 'Hungarian': 'hu-HU',
|
| 1657 |
+
'Danish': 'da-DK', 'Finnish': 'fi-FI', 'Norwegian': 'no-NO',
|
| 1658 |
+
'Slovak': 'sk-SK', 'Filipino': 'fil-PH', 'Amharic': 'am-ET'
|
| 1659 |
+
};
|
| 1660 |
+
|
| 1661 |
+
const speechLang = langMap[detectedLang] || navigator.language || 'en-US';
|
| 1662 |
+
console.log(`🎤 Speech recognition updated to: ${speechLang}`);
|
| 1663 |
+
}
|
| 1664 |
+
}
|
| 1665 |
+
|
| 1666 |
+
// Hide greeting
|
| 1667 |
+
const greetingEl = document.getElementById('greeting');
|
| 1668 |
+
if (greetingEl) greetingEl.style.display = 'none';
|
| 1669 |
+
|
| 1670 |
+
// 2. Add User Message to Chat (with source language)
|
| 1671 |
+
// userText already defined above for hallucination check
|
| 1672 |
+
const sourceLang = res.data.source_language_full || 'Auto';
|
| 1673 |
+
const targetLang = res.data.target_language || 'Translation';
|
| 1674 |
+
createChatMessage('user', userText, null, null, sourceLang);
|
| 1675 |
+
|
| 1676 |
+
// 3. Create NEW audio for this message
|
| 1677 |
+
let messageAudioSrc = null;
|
| 1678 |
+
if (res.data.tts_audio) {
|
| 1679 |
+
messageAudioSrc = `data:audio/mp3;base64,${res.data.tts_audio}`;
|
| 1680 |
+
// Play on global player
|
| 1681 |
+
audioPlayer.src = messageAudioSrc;
|
| 1682 |
+
audioPlayer.play().catch(err => {
|
| 1683 |
+
console.log('Auto-play blocked:', err);
|
| 1684 |
+
});
|
| 1685 |
+
}
|
| 1686 |
+
|
| 1687 |
+
// 4. Add Bot Message with its OWN Audio Player (with target language)
|
| 1688 |
+
const info = {
|
| 1689 |
+
latency: ((Date.now() - startTime) / 1000).toFixed(2),
|
| 1690 |
+
stt: res.data.stt_engine,
|
| 1691 |
+
translation: res.data.translation_engine,
|
| 1692 |
+
tts: res.data.tts_engine
|
| 1693 |
+
};
|
| 1694 |
+
createChatMessage('bot', translation, messageAudioSrc, info, targetLang);
|
| 1695 |
+
|
| 1696 |
+
// 🎙️ IMPORTANT: Update status based on mode
|
| 1697 |
+
if (window.continuousMode) {
|
| 1698 |
+
// Keep button active in continuous mode
|
| 1699 |
+
statusText.innerText = 'Écoute en continu...';
|
| 1700 |
+
console.log('✅ TTS généré - En attente de la prochaine phrase');
|
| 1701 |
+
} else {
|
| 1702 |
+
// Normal mode: reset button
|
| 1703 |
+
isRecording = false;
|
| 1704 |
+
recordBtn.classList.remove('active');
|
| 1705 |
+
recordBtn.disabled = false;
|
| 1706 |
+
statusText.innerText = 'Prêt';
|
| 1707 |
+
console.log('✅ TTS généré - Bouton prêt');
|
| 1708 |
+
}
|
| 1709 |
+
}
|
| 1710 |
+
} catch (e) {
|
| 1711 |
+
console.error("Erreur de traitement:", e);
|
| 1712 |
+
statusText.innerText = "Erreur de connexion";
|
| 1713 |
+
|
| 1714 |
+
// Re-enable button even on error
|
| 1715 |
+
isRecording = false;
|
| 1716 |
+
recordBtn.classList.remove('active');
|
| 1717 |
+
recordBtn.disabled = false;
|
| 1718 |
+
}
|
| 1719 |
+
finally {
|
| 1720 |
+
// Ensure button is always ready
|
| 1721 |
+
recordBtn.disabled = false;
|
| 1722 |
+
recordBtn.classList.remove('processing'); // 🔵 Stop Blue Spinner
|
| 1723 |
+
isProcessingAudio = false; // 🚀 Reset processing flag
|
| 1724 |
+
|
| 1725 |
+
// If NOT continuous mode, ensure text says Ready
|
| 1726 |
+
if (!window.continuousMode) {
|
| 1727 |
+
statusText.innerText = 'Prêt';
|
| 1728 |
+
}
|
| 1729 |
+
}
|
| 1730 |
+
};
|
| 1731 |
+
}
|
| 1732 |
+
|
| 1733 |
+
// Logic de sauvegarde/chargement des paramètres
|
| 1734 |
+
window.loadModalSettings = () => {
|
| 1735 |
+
document.getElementById('stt-engine').value = localStorage.getItem('sttEngine') || 'openai-whisper';
|
| 1736 |
+
document.getElementById('openai-key').value = localStorage.getItem('openaiKey') || '';
|
| 1737 |
+
if (localStorage.getItem('sourceLang')) document.getElementById('source-lang-selector').value = localStorage.getItem('sourceLang');
|
| 1738 |
+
|
| 1739 |
+
// 🎯 Set default target language for bidirectional translation
|
| 1740 |
+
const savedTargetLang = localStorage.getItem('targetLang');
|
| 1741 |
+
if (savedTargetLang && quickLangSelector) {
|
| 1742 |
+
quickLangSelector.value = savedTargetLang;
|
| 1743 |
+
} else if (quickLangSelector) {
|
| 1744 |
+
// Default to French for Arabic ↔ French bidirectional translation
|
| 1745 |
+
quickLangSelector.value = 'French';
|
| 1746 |
+
console.log('🌍 Default target language set to French for bidirectional translation');
|
| 1747 |
+
}
|
| 1748 |
+
};
|
| 1749 |
+
|
| 1750 |
+
window.saveModalSettings = () => {
|
| 1751 |
+
localStorage.setItem('sttEngine', document.getElementById('stt-engine').value);
|
| 1752 |
+
localStorage.setItem('openaiKey', document.getElementById('openai-key').value);
|
| 1753 |
+
localStorage.setItem('targetLang', quickLangSelector.value);
|
| 1754 |
+
localStorage.setItem('sourceLang', document.getElementById('source-lang-selector').value);
|
| 1755 |
+
};
|
| 1756 |
+
|
| 1757 |
+
// Removed: replay-trigger button (deleted from HTML)
|
| 1758 |
+
// Functionality removed as button no longer exists
|
| 1759 |
+
|
| 1760 |
+
// ===================================
|
| 1761 |
+
// 🛠️ BUTTON TOGGLE LOGIC (FIXED)
|
| 1762 |
+
// ===================================
|
| 1763 |
+
|
| 1764 |
+
function setupToggle(id, storageKey, defaultValue, onToggle) {
|
| 1765 |
+
const btn = document.getElementById(id);
|
| 1766 |
+
if (!btn) return;
|
| 1767 |
+
|
| 1768 |
+
// Load initial state
|
| 1769 |
+
const saved = localStorage.getItem(storageKey);
|
| 1770 |
+
const isActive = saved === null ? defaultValue : saved === 'true';
|
| 1771 |
+
|
| 1772 |
+
if (isActive) btn.classList.add('active');
|
| 1773 |
+
else btn.classList.remove('active');
|
| 1774 |
+
|
| 1775 |
+
btn.addEventListener('click', (e) => {
|
| 1776 |
+
e.stopPropagation(); // Prevent bubbling
|
| 1777 |
+
const currentlyActive = btn.classList.contains('active');
|
| 1778 |
+
const newState = !currentlyActive;
|
| 1779 |
+
|
| 1780 |
+
// Toggle visual
|
| 1781 |
+
if (newState) btn.classList.add('active');
|
| 1782 |
+
else btn.classList.remove('active');
|
| 1783 |
+
|
| 1784 |
+
// Save state
|
| 1785 |
+
localStorage.setItem(storageKey, newState);
|
| 1786 |
+
|
| 1787 |
+
// Optional callback
|
| 1788 |
+
if (onToggle) onToggle(newState);
|
| 1789 |
+
|
| 1790 |
+
console.log(`🔘 Toggle ${id}: ${newState ? 'ON' : 'OFF'}`);
|
| 1791 |
+
});
|
| 1792 |
+
}
|
| 1793 |
+
|
| 1794 |
+
function setupCycle(id, storageKey, values, onCycle) {
|
| 1795 |
+
const btn = document.getElementById(id);
|
| 1796 |
+
if (!btn) return;
|
| 1797 |
+
|
| 1798 |
+
// Load initial state
|
| 1799 |
+
let currentVal = localStorage.getItem(storageKey) || values[0];
|
| 1800 |
+
if (!values.includes(currentVal)) currentVal = values[0]; // Fallback
|
| 1801 |
+
|
| 1802 |
+
const updateVisual = (val) => {
|
| 1803 |
+
// Remove all active classes first if needed, or just set generic active
|
| 1804 |
+
// For gender, we might want different icons?
|
| 1805 |
+
// For now, simple active state if not default
|
| 1806 |
+
if (val !== values[0]) btn.classList.add('active');
|
| 1807 |
+
else btn.classList.remove('active');
|
| 1808 |
+
|
| 1809 |
+
// Tooltip feedback
|
| 1810 |
+
btn.title = `Mode: ${val.toUpperCase()}`;
|
| 1811 |
+
};
|
| 1812 |
+
|
| 1813 |
+
updateVisual(currentVal);
|
| 1814 |
+
|
| 1815 |
+
btn.addEventListener('click', (e) => {
|
| 1816 |
+
e.stopPropagation();
|
| 1817 |
+
const currentIndex = values.indexOf(currentVal);
|
| 1818 |
+
const nextIndex = (currentIndex + 1) % values.length;
|
| 1819 |
+
currentVal = values[nextIndex];
|
| 1820 |
+
|
| 1821 |
+
localStorage.setItem(storageKey, currentVal);
|
| 1822 |
+
updateVisual(currentVal);
|
| 1823 |
+
|
| 1824 |
+
if (onCycle) onCycle(currentVal);
|
| 1825 |
+
console.log(`🔄 Cycle ${id}: ${currentVal}`);
|
| 1826 |
+
|
| 1827 |
+
// Visual text feedback (Toast)
|
| 1828 |
+
statusText.innerText = `Mode: ${currentVal.toUpperCase()}`;
|
| 1829 |
+
setTimeout(() => statusText.innerText = 'Prêt', 1500);
|
| 1830 |
+
});
|
| 1831 |
+
}
|
| 1832 |
+
|
| 1833 |
+
// 🎯 Initialize Toggles when DOM is ready
|
| 1834 |
+
document.addEventListener('DOMContentLoaded', () => {
|
| 1835 |
+
// 1. Magic/Grammar Toggle
|
| 1836 |
+
setupToggle('grammar-toggle', 'grammarCorrectionEnabled', true, (state) => {
|
| 1837 |
+
statusText.innerText = state ? '✨ Correction: ON' : '📝 Correction: OFF';
|
| 1838 |
+
setTimeout(() => statusText.innerText = 'Prêt', 1500);
|
| 1839 |
+
});
|
| 1840 |
+
|
| 1841 |
+
// 2. Voice Gender Toggle (Auto -> Male -> Female)
|
| 1842 |
+
setupCycle('voice-gender-toggle', 'voiceGenderPreference', ['auto', 'male', 'female']);
|
| 1843 |
+
|
| 1844 |
+
// 3. Smart Mode Toggle (Brain)
|
| 1845 |
+
setupToggle('smart-mode-toggle', 'smartModeEnabled', true, (state) => {
|
| 1846 |
+
statusText.innerText = state ? '🧠 Mode Smart: ON' : '🧠 Mode Smart: OFF';
|
| 1847 |
+
setTimeout(() => statusText.innerText = 'Prêt', 1500);
|
| 1848 |
+
});
|
| 1849 |
+
});
|
| 1850 |
+
|
| 1851 |
+
// ⚙️ SETTINGS MODAL LOGIC (Fixing the "Broken Button")
|
| 1852 |
+
document.addEventListener('DOMContentLoaded', () => {
|
| 1853 |
+
const settingsBtn = document.getElementById('settings-trigger'); // Found ID
|
| 1854 |
+
const closeSettingsBtn = document.getElementById('close-settings');
|
| 1855 |
+
const settingsModal = document.getElementById('settings-modal');
|
| 1856 |
+
|
| 1857 |
+
// Open
|
| 1858 |
+
if (settingsBtn && settingsModal) {
|
| 1859 |
+
settingsBtn.addEventListener('click', () => {
|
| 1860 |
+
settingsModal.style.display = 'flex'; // Or remove hidden class
|
| 1861 |
+
// settingsModal.classList.remove('hidden'); // If using classes
|
| 1862 |
+
console.log('⚙️ Settings Opened');
|
| 1863 |
+
});
|
| 1864 |
+
} else {
|
| 1865 |
+
console.error('❌ Settings Trigger or Modal NOT FOUND');
|
| 1866 |
+
}
|
| 1867 |
+
|
| 1868 |
+
// Close Button
|
| 1869 |
+
if (closeSettingsBtn && settingsModal) {
|
| 1870 |
+
closeSettingsBtn.addEventListener('click', () => {
|
| 1871 |
+
settingsModal.style.display = 'none';
|
| 1872 |
+
});
|
| 1873 |
+
}
|
| 1874 |
+
|
| 1875 |
+
// Close on Outside Click
|
| 1876 |
+
window.addEventListener('click', (e) => {
|
| 1877 |
+
if (e.target === settingsModal) {
|
| 1878 |
+
settingsModal.style.display = 'none';
|
| 1879 |
+
}
|
| 1880 |
+
});
|
| 1881 |
+
|
| 1882 |
+
// 💾 SAVE & LOAD LOGIC
|
| 1883 |
+
const saveBtn = document.getElementById('save-settings');
|
| 1884 |
+
const aiSelector = document.getElementById('ai-model-selector');
|
| 1885 |
+
const ttsSelector = document.getElementById('tts-selector');
|
| 1886 |
+
|
| 1887 |
+
// Load Initial Values
|
| 1888 |
+
if (aiSelector) aiSelector.value = localStorage.getItem('aiModel') || 'gpt-4o-mini';
|
| 1889 |
+
if (ttsSelector) ttsSelector.value = localStorage.getItem('ttsEngine') || 'openai';
|
| 1890 |
+
|
| 1891 |
+
// Save Handler
|
| 1892 |
+
if (saveBtn) {
|
| 1893 |
+
saveBtn.addEventListener('click', () => {
|
| 1894 |
+
if (aiSelector) {
|
| 1895 |
+
localStorage.setItem('aiModel', aiSelector.value);
|
| 1896 |
+
console.log(`🧠 AI Model set to: ${aiSelector.value}`);
|
| 1897 |
+
}
|
| 1898 |
+
if (ttsSelector) {
|
| 1899 |
+
localStorage.setItem('ttsEngine', ttsSelector.value);
|
| 1900 |
+
console.log(`🗣️ TTS Engine set to: ${ttsSelector.value}`);
|
| 1901 |
+
}
|
| 1902 |
+
|
| 1903 |
+
// Close Modal
|
| 1904 |
+
if (settingsModal) settingsModal.style.display = 'none';
|
| 1905 |
+
|
| 1906 |
+
// Feedback
|
| 1907 |
+
if (statusText) {
|
| 1908 |
+
statusText.innerText = '✅ Sauvegardé!';
|
| 1909 |
+
setTimeout(() => statusText.innerText = 'Prêt', 2000);
|
| 1910 |
+
}
|
| 1911 |
+
});
|
| 1912 |
+
}
|
| 1913 |
+
});
|
| 1914 |
+
|
| 1915 |
+
|
| 1916 |
+
// 🎯 Initialize language settings on page load
|
| 1917 |
+
document.addEventListener('DOMContentLoaded', () => {
|
| 1918 |
+
// Set default target language for bidirectional translation
|
| 1919 |
+
if (quickLangSelector) {
|
| 1920 |
+
const savedTargetLang = localStorage.getItem('targetLang');
|
| 1921 |
+
if (savedTargetLang) {
|
| 1922 |
+
quickLangSelector.value = savedTargetLang;
|
| 1923 |
+
console.log(`🌍 Loaded saved target language: ${savedTargetLang}`);
|
| 1924 |
+
} else {
|
| 1925 |
+
quickLangSelector.value = 'French';
|
| 1926 |
+
console.log('🌍 Default target language set to French for Arabic ↔ French bidirectional translation');
|
| 1927 |
+
}
|
| 1928 |
+
}
|
| 1929 |
+
|
| 1930 |
+
// Set source to auto for automatic detection
|
| 1931 |
+
const sourceLangSelector = document.getElementById('source-lang-selector');
|
| 1932 |
+
if (sourceLangSelector) {
|
| 1933 |
+
sourceLangSelector.value = 'auto';
|
| 1934 |
+
console.log('🎯 Source language set to AUTO for automatic detection');
|
| 1935 |
+
|
| 1936 |
+
// 🔄 HARD SYNC: If user changes Source, clear "Smart History" to prevent confusion
|
| 1937 |
+
sourceLangSelector.addEventListener('change', function () {
|
| 1938 |
+
console.log('🔄 Source Language Changed -> Clearing Smart History...');
|
| 1939 |
+
localStorage.setItem('sourceLang', this.value);
|
| 1940 |
+
fetch('/clear_cache', { method: 'POST' });
|
| 1941 |
+
});
|
| 1942 |
+
}
|
| 1943 |
+
|
| 1944 |
+
// 🔄 HARD SYNC: If user changes Target, clear "Smart History" to prevent confusion
|
| 1945 |
+
if (quickLangSelector) {
|
| 1946 |
+
quickLangSelector.addEventListener('change', function () {
|
| 1947 |
+
console.log('🔄 Target Language Changed -> Clearing Smart History...');
|
| 1948 |
+
localStorage.setItem('targetLang', this.value);
|
| 1949 |
+
fetch('/clear_cache', { method: 'POST' });
|
| 1950 |
+
});
|
| 1951 |
+
}
|
| 1952 |
+
|
| 1953 |
+
// ===================================
|
| 1954 |
+
// 🎭 VOICE CLONING TOGGLE
|
| 1955 |
+
// ===================================
|
| 1956 |
+
let voiceCloneEnabled = localStorage.getItem('voiceCloneEnabled') !== 'false'; // Default: ON
|
| 1957 |
+
|
| 1958 |
+
const voiceCloneToggle = document.getElementById('voice-clone-toggle');
|
| 1959 |
+
if (voiceCloneToggle) {
|
| 1960 |
+
// Set initial state
|
| 1961 |
+
if (voiceCloneEnabled) {
|
| 1962 |
+
voiceCloneToggle.classList.add('active');
|
| 1963 |
+
} else {
|
| 1964 |
+
voiceCloneToggle.classList.remove('active');
|
| 1965 |
+
}
|
| 1966 |
+
|
| 1967 |
+
// Toggle on click
|
| 1968 |
+
voiceCloneToggle.addEventListener('click', function () {
|
| 1969 |
+
voiceCloneEnabled = !voiceCloneEnabled;
|
| 1970 |
+
localStorage.setItem('voiceCloneEnabled', voiceCloneEnabled);
|
| 1971 |
+
|
| 1972 |
+
if (voiceCloneEnabled) {
|
| 1973 |
+
this.classList.add('active');
|
| 1974 |
+
console.log('🎭 Voice Cloning: ON');
|
| 1975 |
+
} else {
|
| 1976 |
+
this.classList.remove('active');
|
| 1977 |
+
console.log('🎭 Voice Cloning: OFF');
|
| 1978 |
+
}
|
| 1979 |
+
});
|
| 1980 |
+
}
|
| 1981 |
+
|
| 1982 |
+
});
|
templates/admin.html
ADDED
|
@@ -0,0 +1,357 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
<!DOCTYPE html>
|
| 2 |
+
<html lang="en">
|
| 3 |
+
|
| 4 |
+
<head>
|
| 5 |
+
<meta charset="UTF-8">
|
| 6 |
+
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no">
|
| 7 |
+
<title>Admin Control Panel | Instant Translator</title>
|
| 8 |
+
<link href="https://fonts.googleapis.com/css2?family=Outfit:wght@300;400;500;600;700&display=swap" rel="stylesheet">
|
| 9 |
+
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css">
|
| 10 |
+
<style>
|
| 11 |
+
:root {
|
| 12 |
+
--primary: #6366f1;
|
| 13 |
+
--surface: #09090b;
|
| 14 |
+
--surface-light: #18181b;
|
| 15 |
+
--text: #e4e4e7;
|
| 16 |
+
--danger: #ef4444;
|
| 17 |
+
--success: #22c55e;
|
| 18 |
+
}
|
| 19 |
+
|
| 20 |
+
body {
|
| 21 |
+
font-family: 'Outfit', sans-serif;
|
| 22 |
+
background-color: #000;
|
| 23 |
+
color: var(--text);
|
| 24 |
+
margin: 0;
|
| 25 |
+
padding: 20px;
|
| 26 |
+
-webkit-font-smoothing: antialiased;
|
| 27 |
+
}
|
| 28 |
+
|
| 29 |
+
.admin-container {
|
| 30 |
+
max-width: 800px;
|
| 31 |
+
margin: 0 auto;
|
| 32 |
+
}
|
| 33 |
+
|
| 34 |
+
.header {
|
| 35 |
+
display: flex;
|
| 36 |
+
justify-content: space-between;
|
| 37 |
+
align-items: center;
|
| 38 |
+
margin-bottom: 30px;
|
| 39 |
+
padding-bottom: 20px;
|
| 40 |
+
border-bottom: 1px solid #27272a;
|
| 41 |
+
}
|
| 42 |
+
|
| 43 |
+
h1 {
|
| 44 |
+
margin: 0;
|
| 45 |
+
font-size: 24px;
|
| 46 |
+
background: linear-gradient(135deg, #fff 0%, #a5b4fc 100%);
|
| 47 |
+
-webkit-background-clip: text;
|
| 48 |
+
-webkit-text-fill-color: transparent;
|
| 49 |
+
}
|
| 50 |
+
|
| 51 |
+
.card {
|
| 52 |
+
background: var(--surface-light);
|
| 53 |
+
border-radius: 16px;
|
| 54 |
+
padding: 24px;
|
| 55 |
+
margin-bottom: 20px;
|
| 56 |
+
border: 1px solid #27272a;
|
| 57 |
+
}
|
| 58 |
+
|
| 59 |
+
.card h2 {
|
| 60 |
+
margin-top: 0;
|
| 61 |
+
font-size: 18px;
|
| 62 |
+
color: #a1a1aa;
|
| 63 |
+
margin-bottom: 20px;
|
| 64 |
+
}
|
| 65 |
+
|
| 66 |
+
.stat-grid {
|
| 67 |
+
display: grid;
|
| 68 |
+
grid-template-columns: repeat(auto-fit, minmax(150px, 1fr));
|
| 69 |
+
gap: 15px;
|
| 70 |
+
}
|
| 71 |
+
|
| 72 |
+
.stat-box {
|
| 73 |
+
background: #27272a;
|
| 74 |
+
padding: 15px;
|
| 75 |
+
border-radius: 12px;
|
| 76 |
+
text-align: center;
|
| 77 |
+
}
|
| 78 |
+
|
| 79 |
+
.stat-value {
|
| 80 |
+
font-size: 24px;
|
| 81 |
+
font-weight: 700;
|
| 82 |
+
color: #fff;
|
| 83 |
+
}
|
| 84 |
+
|
| 85 |
+
.stat-label {
|
| 86 |
+
font-size: 12px;
|
| 87 |
+
color: #a1a1aa;
|
| 88 |
+
margin-top: 5px;
|
| 89 |
+
}
|
| 90 |
+
|
| 91 |
+
.form-group {
|
| 92 |
+
margin-bottom: 15px;
|
| 93 |
+
}
|
| 94 |
+
|
| 95 |
+
.form-group label {
|
| 96 |
+
display: block;
|
| 97 |
+
margin-bottom: 8px;
|
| 98 |
+
font-size: 14px;
|
| 99 |
+
color: #a1a1aa;
|
| 100 |
+
}
|
| 101 |
+
|
| 102 |
+
.form-group input {
|
| 103 |
+
width: 100%;
|
| 104 |
+
padding: 12px;
|
| 105 |
+
background: #000;
|
| 106 |
+
border: 1px solid #27272a;
|
| 107 |
+
border-radius: 8px;
|
| 108 |
+
color: #fff;
|
| 109 |
+
font-family: inherit;
|
| 110 |
+
box-sizing: border-box;
|
| 111 |
+
}
|
| 112 |
+
|
| 113 |
+
.form-group input:focus {
|
| 114 |
+
outline: none;
|
| 115 |
+
border-color: var(--primary);
|
| 116 |
+
}
|
| 117 |
+
|
| 118 |
+
.btn {
|
| 119 |
+
background: var(--primary);
|
| 120 |
+
color: white;
|
| 121 |
+
border: none;
|
| 122 |
+
padding: 12px 24px;
|
| 123 |
+
border-radius: 8px;
|
| 124 |
+
font-weight: 600;
|
| 125 |
+
cursor: pointer;
|
| 126 |
+
width: 100%;
|
| 127 |
+
font-size: 16px;
|
| 128 |
+
transition: opacity 0.2s;
|
| 129 |
+
}
|
| 130 |
+
|
| 131 |
+
.btn:hover {
|
| 132 |
+
opacity: 0.9;
|
| 133 |
+
}
|
| 134 |
+
|
| 135 |
+
.btn-danger {
|
| 136 |
+
background: var(--danger);
|
| 137 |
+
}
|
| 138 |
+
|
| 139 |
+
.login-screen {
|
| 140 |
+
position: fixed;
|
| 141 |
+
inset: 0;
|
| 142 |
+
background: #000;
|
| 143 |
+
z-index: 100;
|
| 144 |
+
display: flex;
|
| 145 |
+
align-items: center;
|
| 146 |
+
justify-content: center;
|
| 147 |
+
}
|
| 148 |
+
|
| 149 |
+
.login-box {
|
| 150 |
+
width: 100%;
|
| 151 |
+
max-width: 400px;
|
| 152 |
+
padding: 30px;
|
| 153 |
+
}
|
| 154 |
+
|
| 155 |
+
.status-badge {
|
| 156 |
+
display: inline-block;
|
| 157 |
+
padding: 4px 12px;
|
| 158 |
+
border-radius: 20px;
|
| 159 |
+
font-size: 12px;
|
| 160 |
+
font-weight: 600;
|
| 161 |
+
background: #27272a;
|
| 162 |
+
}
|
| 163 |
+
|
| 164 |
+
.status-badge.online {
|
| 165 |
+
background: rgba(34, 197, 94, 0.2);
|
| 166 |
+
color: var(--success);
|
| 167 |
+
}
|
| 168 |
+
</style>
|
| 169 |
+
</head>
|
| 170 |
+
|
| 171 |
+
<body>
|
| 172 |
+
|
| 173 |
+
<!-- LOGIN SCREEN -->
|
| 174 |
+
<div id="login-screen" class="login-screen">
|
| 175 |
+
<div class="card login-box">
|
| 176 |
+
<h2 style="text-align: center; color: white;">Admin Access</h2>
|
| 177 |
+
<div class="form-group">
|
| 178 |
+
<label>Admin Password</label>
|
| 179 |
+
<input type="password" id="admin-pass" placeholder="Enter PIN">
|
| 180 |
+
</div>
|
| 181 |
+
<button class="btn" onclick="login()">Unlock Panel</button>
|
| 182 |
+
</div>
|
| 183 |
+
</div>
|
| 184 |
+
|
| 185 |
+
<!-- DASHBOARD -->
|
| 186 |
+
<div class="admin-container">
|
| 187 |
+
<div class="header">
|
| 188 |
+
<div>
|
| 189 |
+
<h1>Admin Control Panel</h1>
|
| 190 |
+
<div style="margin-top: 5px; color: #a1a1aa; font-size: 13px;">
|
| 191 |
+
<span class="status-badge online"><i class="fa-solid fa-circle" style="font-size: 8px;"></i> System
|
| 192 |
+
Online</span>
|
| 193 |
+
v2.5 Mobile Ready
|
| 194 |
+
</div>
|
| 195 |
+
</div>
|
| 196 |
+
<button class="btn" style="width: auto; background: #27272a;" onclick="logout()"><i
|
| 197 |
+
class="fa-solid fa-power-off"></i></button>
|
| 198 |
+
</div>
|
| 199 |
+
|
| 200 |
+
<!-- STATS -->
|
| 201 |
+
<div class="card">
|
| 202 |
+
<h2>📊 Live Statistics</h2>
|
| 203 |
+
<div class="stat-grid">
|
| 204 |
+
<div class="stat-box">
|
| 205 |
+
<div class="stat-value" id="stat-requests">0</div>
|
| 206 |
+
<div class="stat-label">Total Translations</div>
|
| 207 |
+
</div>
|
| 208 |
+
<div class="stat-box">
|
| 209 |
+
<div class="stat-value" id="stat-errors">0</div>
|
| 210 |
+
<div class="stat-label">API Errors</div>
|
| 211 |
+
</div>
|
| 212 |
+
<div class="stat-box">
|
| 213 |
+
<div class="stat-value" id="stat-cost">$0.00</div>
|
| 214 |
+
<div class="stat-label">Est. Cost</div>
|
| 215 |
+
</div>
|
| 216 |
+
</div>
|
| 217 |
+
</div>
|
| 218 |
+
|
| 219 |
+
<!-- MASTER KEYS -->
|
| 220 |
+
<div class="card">
|
| 221 |
+
<h2>🔑 Master API Configuration</h2>
|
| 222 |
+
<p style="font-size: 13px; color: #71717a; margin-bottom: 20px;">
|
| 223 |
+
These keys are stored securely on the server. App users will use these keys via the API without ever
|
| 224 |
+
seeing them.
|
| 225 |
+
<b>Required for App Store compliance.</b>
|
| 226 |
+
</p>
|
| 227 |
+
|
| 228 |
+
<div class="form-group">
|
| 229 |
+
<label>OpenAI API Key (Universal)</label>
|
| 230 |
+
<input type="password" id="key-openai" placeholder="sk-...">
|
| 231 |
+
</div>
|
| 232 |
+
|
| 233 |
+
<div class="form-group">
|
| 234 |
+
<label>ElevenLabs API Key</label>
|
| 235 |
+
<input type="password" id="key-eleven" placeholder="xi-...">
|
| 236 |
+
</div>
|
| 237 |
+
|
| 238 |
+
<div class="form-group">
|
| 239 |
+
<label>Google Cloud API Key</label>
|
| 240 |
+
<input type="password" id="key-google" placeholder="AIza...">
|
| 241 |
+
</div>
|
| 242 |
+
|
| 243 |
+
<!-- TTS ENGINE SELECTOR -->
|
| 244 |
+
<div class="form-group" style="border-top: 1px solid #27272a; padding-top: 20px; margin-top: 20px;">
|
| 245 |
+
<label style="margin-bottom: 12px;">🎤 TTS Engine Selection</label>
|
| 246 |
+
<div
|
| 247 |
+
style="display: flex; gap: 10px; align-items: center; background: #27272a; padding: 12px; border-radius: 8px;">
|
| 248 |
+
<div style="flex: 1; text-align: center; padding: 10px; border-radius: 6px; cursor: pointer; transition: 0.3s; background: #6366f1; color: white;"
|
| 249 |
+
id="tts-openai" onclick="selectTTS('openai')">
|
| 250 |
+
<div style="font-weight: 600;">OpenAI TTS</div>
|
| 251 |
+
<div style="font-size: 11px; opacity: 0.8; margin-top: 4px;">$0.015/1K · HD Quality</div>
|
| 252 |
+
</div>
|
| 253 |
+
<div style="flex: 1; text-align: center; padding: 10px; border-radius: 6px; cursor: pointer; transition: 0.3s; background: transparent; border: 1px solid #3f3f46; color: #a1a1aa;"
|
| 254 |
+
id="tts-elevenlabs" onclick="selectTTS('elevenlabs')">
|
| 255 |
+
<div style="font-weight: 600;">ElevenLabs</div>
|
| 256 |
+
<div style="font-size: 11px; opacity: 0.8; margin-top: 4px">$0.30/1K · Ultra Premium</div>
|
| 257 |
+
</div>
|
| 258 |
+
</div>
|
| 259 |
+
<div style="margin-top: 10px; font-size: 12px; color: #71717a; text-align: center;" id="tts-status">
|
| 260 |
+
Current: <span style="color: #6366f1; font-weight: 600;">OpenAI TTS</span>
|
| 261 |
+
</div>
|
| 262 |
+
</div>
|
| 263 |
+
|
| 264 |
+
<button class="btn" onclick="saveKeys()">Save Master Configuration</button>
|
| 265 |
+
</div>
|
| 266 |
+
|
| 267 |
+
<!-- SYSTEM CONTROL -->
|
| 268 |
+
<div class="card">
|
| 269 |
+
<h2>🛡️ System Controls</h2>
|
| 270 |
+
<div class="form-group">
|
| 271 |
+
<label style="display: flex; align-items: center; justify-content: space-between;">
|
| 272 |
+
Example Mode (Free for users)
|
| 273 |
+
<input type="checkbox" style="width: auto;" checked>
|
| 274 |
+
</label>
|
| 275 |
+
</div>
|
| 276 |
+
<button class="btn btn-danger" onclick="clearLogs()">Clear Server Logs</button>
|
| 277 |
+
</div>
|
| 278 |
+
</div>
|
| 279 |
+
|
| 280 |
+
<script>
|
| 281 |
+
// Simple client-side gate (Real auth is server-side)
|
| 282 |
+
function login() {
|
| 283 |
+
const pass = document.getElementById('admin-pass').value;
|
| 284 |
+
if (pass === 'admin123') { // Placeholder logic
|
| 285 |
+
document.getElementById('login-screen').style.display = 'none';
|
| 286 |
+
loadStats();
|
| 287 |
+
} else {
|
| 288 |
+
alert('Access Denied');
|
| 289 |
+
}
|
| 290 |
+
}
|
| 291 |
+
|
| 292 |
+
function logout() {
|
| 293 |
+
location.reload();
|
| 294 |
+
}
|
| 295 |
+
|
| 296 |
+
function loadStats() {
|
| 297 |
+
// Simulator
|
| 298 |
+
document.getElementById('stat-requests').innerText = Math.floor(Math.random() * 500) + 120;
|
| 299 |
+
document.getElementById('stat-errors').innerText = '0';
|
| 300 |
+
|
| 301 |
+
// Load saved TTS engine
|
| 302 |
+
const savedEngine = localStorage.getItem('ttsEngine') || 'openai';
|
| 303 |
+
selectTTS(savedEngine, false);
|
| 304 |
+
}
|
| 305 |
+
|
| 306 |
+
function selectTTS(engine, save = true) {
|
| 307 |
+
const openaiBtn = document.getElementById('tts-openai');
|
| 308 |
+
const elevenlabsBtn = document.getElementById('tts-elevenlabs');
|
| 309 |
+
const status = document.getElementById('tts-status');
|
| 310 |
+
|
| 311 |
+
if (engine === 'openai') {
|
| 312 |
+
// OpenAI selected
|
| 313 |
+
openaiBtn.style.background = '#6366f1';
|
| 314 |
+
openaiBtn.style.color = 'white';
|
| 315 |
+
openaiBtn.style.border = 'none';
|
| 316 |
+
|
| 317 |
+
elevenlabsBtn.style.background = 'transparent';
|
| 318 |
+
elevenlabsBtn.style.color = '#a1a1aa';
|
| 319 |
+
elevenlabsBtn.style.border = '1px solid #3f3f46';
|
| 320 |
+
|
| 321 |
+
status.innerHTML = 'Current: <span style="color: #6366f1; font-weight: 600;">OpenAI TTS</span>';
|
| 322 |
+
} else {
|
| 323 |
+
// ElevenLabs selected
|
| 324 |
+
elevenlabsBtn.style.background = '#6366f1';
|
| 325 |
+
elevenlabsBtn.style.color = 'white';
|
| 326 |
+
elevenlabsBtn.style.border = 'none';
|
| 327 |
+
|
| 328 |
+
openaiBtn.style.background = 'transparent';
|
| 329 |
+
openaiBtn.style.color = '#a1a1aa';
|
| 330 |
+
openaiBtn.style.border = '1px solid #3f3f46';
|
| 331 |
+
|
| 332 |
+
status.innerHTML = 'Current: <span style="color: #6366f1; font-weight: 600;">ElevenLabs</span>';
|
| 333 |
+
}
|
| 334 |
+
|
| 335 |
+
// Save to localStorage
|
| 336 |
+
if (save) {
|
| 337 |
+
localStorage.setItem('ttsEngine', engine);
|
| 338 |
+
console.log('TTS Engine switched to:', engine);
|
| 339 |
+
}
|
| 340 |
+
}
|
| 341 |
+
|
| 342 |
+
function saveKeys() {
|
| 343 |
+
const btn = document.querySelector('button[onclick="saveKeys()"]');
|
| 344 |
+
btn.innerText = 'Saving...';
|
| 345 |
+
setTimeout(() => {
|
| 346 |
+
btn.innerText = '✅ Saved Securely';
|
| 347 |
+
btn.style.background = '#22c55e';
|
| 348 |
+
setTimeout(() => {
|
| 349 |
+
btn.innerText = 'Save Master Configuration';
|
| 350 |
+
btn.style.background = '#6366f1';
|
| 351 |
+
}, 2000);
|
| 352 |
+
}, 800);
|
| 353 |
+
}
|
| 354 |
+
</script>
|
| 355 |
+
</body>
|
| 356 |
+
|
| 357 |
+
</html>
|
templates/index.html
ADDED
|
@@ -0,0 +1,914 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
<!DOCTYPE html>
|
| 2 |
+
<html lang="fr">
|
| 3 |
+
|
| 4 |
+
<head>
|
| 5 |
+
<meta charset="UTF-8">
|
| 6 |
+
<meta name="viewport"
|
| 7 |
+
content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no, viewport-fit=cover">
|
| 8 |
+
|
| 9 |
+
<!-- ✨ WORLD-CHANGING MOBILE EXPERIENCE (PWA) -->
|
| 10 |
+
<meta name="theme-color" content="#0a0a0b">
|
| 11 |
+
<meta name="apple-mobile-web-app-capable" content="yes">
|
| 12 |
+
<meta name="apple-mobile-web-app-status-bar-style" content="black-translucent">
|
| 13 |
+
<meta name="application-name" content="Babel">
|
| 14 |
+
<meta name="apple-mobile-web-app-title" content="Babel">
|
| 15 |
+
|
| 16 |
+
<!-- DISABLE BROWSER CACHING for instant loading -->
|
| 17 |
+
<meta http-equiv="Cache-Control" content="no-cache, no-store, must-revalidate">
|
| 18 |
+
<meta http-equiv="Pragma" content="no-cache">
|
| 19 |
+
<meta http-equiv="Expires" content="0">
|
| 20 |
+
|
| 21 |
+
<title>Babel</title>
|
| 22 |
+
<link
|
| 23 |
+
href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&family=Noto+Sans+Arabic:wght@400;500;600&family=JetBrains+Mono:wght@400;500&display=swap"
|
| 24 |
+
rel="stylesheet">
|
| 25 |
+
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css">
|
| 26 |
+
<link rel="stylesheet" href="/static/enhanced.css">
|
| 27 |
+
<link rel="manifest" href="/static/manifest.json">
|
| 28 |
+
<style>
|
| 29 |
+
/* CSS moved to static/enhanced.css */
|
| 30 |
+
</style>
|
| 31 |
+
</head>
|
| 32 |
+
|
| 33 |
+
<body>
|
| 34 |
+
<div class="app-layout">
|
| 35 |
+
<!-- –✨ GROK SIDEBAR -->
|
| 36 |
+
<aside class="grok-sidebar">
|
| 37 |
+
<div class="sidebar-top">
|
| 38 |
+
<div class="sidebar-logo">
|
| 39 |
+
<i class="fa-solid fa-cube"></i>
|
| 40 |
+
</div>
|
| 41 |
+
<button class="new-chat" onclick="location.reload()" title="New Session">
|
| 42 |
+
<i class="fa-solid fa-plus"></i>
|
| 43 |
+
</button>
|
| 44 |
+
</div>
|
| 45 |
+
|
| 46 |
+
<nav class="sidebar-menu">
|
| 47 |
+
<div class="menu-item active">
|
| 48 |
+
<i class="fa-solid fa-comment-dots"></i>
|
| 49 |
+
<span>Chat</span>
|
| 50 |
+
</div>
|
| 51 |
+
<div class="menu-item" id="sidebar-voice-mode">
|
| 52 |
+
<i class="fa-solid fa-microphone-lines"></i>
|
| 53 |
+
<span>Voice</span>
|
| 54 |
+
</div>
|
| 55 |
+
<!-- Settings Trigger Moved Here -->
|
| 56 |
+
<div class="menu-item" id="settings-trigger">
|
| 57 |
+
<i class="fa-solid fa-sliders"></i>
|
| 58 |
+
<span>Settings</span>
|
| 59 |
+
</div>
|
| 60 |
+
</nav>
|
| 61 |
+
|
| 62 |
+
<div class="sidebar-footer">
|
| 63 |
+
<div class="user-profile">
|
| 64 |
+
<div class="avatar">U</div>
|
| 65 |
+
<div class="info">
|
| 66 |
+
<span class="name">User</span>
|
| 67 |
+
<span class="status">Pro</span>
|
| 68 |
+
</div>
|
| 69 |
+
</div>
|
| 70 |
+
</div>
|
| 71 |
+
</aside>
|
| 72 |
+
|
| 73 |
+
<!-- –✨ MAIN CANVAS -->
|
| 74 |
+
<main class="grok-main">
|
| 75 |
+
<!-- Header for Mobile/Context -->
|
| 76 |
+
<header class="main-header">
|
| 77 |
+
<span class="model-badge">Babel v2.0</span>
|
| 78 |
+
</header>
|
| 79 |
+
|
| 80 |
+
<!-- CHAT STAGE -->
|
| 81 |
+
<div id="chat-stage" class="chat-stage">
|
| 82 |
+
<!-- Empty State (Centered Logo) -->
|
| 83 |
+
<div class="empty-state" id="greeting">
|
| 84 |
+
<div class="grok-logo-hero">Babel</div>
|
| 85 |
+
<p class="grok-hero-text">Intelligence Vocale Instantanee</p>
|
| 86 |
+
</div>
|
| 87 |
+
|
| 88 |
+
<!-- RESULT DISPLAY (Fallback if chat doesn't show) -->
|
| 89 |
+
<div id="result-display" style="display: none; padding: 20px; text-align: center;">
|
| 90 |
+
<div id="original-display" style="font-size: 1.1em; color: #aaa; margin-bottom: 10px;"></div>
|
| 91 |
+
<div id="translation-display" style="font-size: 1.5em; color: #fff; font-weight: bold;"></div>
|
| 92 |
+
</div>
|
| 93 |
+
|
| 94 |
+
<div id="chat-history" class="chat-history"></div>
|
| 95 |
+
</div>
|
| 96 |
+
|
| 97 |
+
<!-- GROK INPUT BAR AREA -->
|
| 98 |
+
<div class="grok-input-wrapper">
|
| 99 |
+
|
| 100 |
+
<!-- The Main Input Pill -->
|
| 101 |
+
<div class="grok-input-bar">
|
| 102 |
+
<div class="input-icon left">
|
| 103 |
+
<i class="fa-solid fa-paperclip"></i>
|
| 104 |
+
</div>
|
| 105 |
+
|
| 106 |
+
<!-- Status Text acting as Placeholder -->
|
| 107 |
+
<div class="input-status-area">
|
| 108 |
+
<span id="status-placeholder" class="status-text">Prêt à traduire...</span>
|
| 109 |
+
<span id="latency-badge"
|
| 110 |
+
style="display:none; font-size: 0.7em; color: #666; margin-left: 10px;">0ms</span>
|
| 111 |
+
</div>
|
| 112 |
+
|
| 113 |
+
<!-- THE ORB (Record Button) -->
|
| 114 |
+
<div class="input-icon right action" id="record-btn">
|
| 115 |
+
<i class="fa-solid fa-microphone"></i>
|
| 116 |
+
</div>
|
| 117 |
+
</div>
|
| 118 |
+
|
| 119 |
+
<!-- SUGGESTION CHIPS (Controls) -->
|
| 120 |
+
<div class="grok-chips">
|
| 121 |
+
|
| 122 |
+
<!-- Source Language -->
|
| 123 |
+
<div class="chip-select-wrapper">
|
| 124 |
+
<select id="source-lang-quick" class="chip-select">
|
| 125 |
+
<option value="auto">🎯 Auto</option>
|
| 126 |
+
<option value="ar-SA">🇲🇦 Darija</option>
|
| 127 |
+
<option value="fr-FR">🇫🇷 Français</option>
|
| 128 |
+
<option value="en-US">🇬🇧 English</option>
|
| 129 |
+
<option value="es-ES">🇪🇸 Español</option>
|
| 130 |
+
</select>
|
| 131 |
+
<i class="fa-solid fa-chevron-down"></i>
|
| 132 |
+
</div>
|
| 133 |
+
|
| 134 |
+
<!-- Swap -->
|
| 135 |
+
<button id="swap-langs" class="chip-icon" title="Swap">
|
| 136 |
+
<i class="fa-solid fa-arrow-right-arrow-left"></i>
|
| 137 |
+
</button>
|
| 138 |
+
|
| 139 |
+
<!-- Target Language (SeamlessM4T TTS supported) -->
|
| 140 |
+
<div class="chip-select-wrapper">
|
| 141 |
+
<select id="target-lang-quick" class="chip-select">
|
| 142 |
+
<option value="en-US" selected>🇬🇧 English</option>
|
| 143 |
+
<option value="fr-FR">🇫🇷 Français</option>
|
| 144 |
+
<option value="ar-SA">🇲🇦 Darija</option>
|
| 145 |
+
<option value="es-ES">🇪🇸 Español</option>
|
| 146 |
+
<option value="de-DE">🇩🇪 Deutsch</option>
|
| 147 |
+
<option value="it-IT">🇮🇹 Italiano</option>
|
| 148 |
+
<option value="pt-PT">🇵🇹 Português</option>
|
| 149 |
+
<option value="zh-CN">🇨🇳 中文</option>
|
| 150 |
+
<option value="ja-JP">🇯🇵 日本語</option>
|
| 151 |
+
<option value="ko-KR">🇰🇷 한국어</option>
|
| 152 |
+
<option value="ru-RU">🇷🇺 Русский</option>
|
| 153 |
+
<option value="tr-TR">🇹🇷 Türkçe</option>
|
| 154 |
+
</select>
|
| 155 |
+
<i class="fa-solid fa-chevron-down"></i>
|
| 156 |
+
</div>
|
| 157 |
+
|
| 158 |
+
<div class="chip-divider"></div>
|
| 159 |
+
|
| 160 |
+
<!-- Features -->
|
| 161 |
+
<button id="smart-mode-toggle" class="chip-pill active"
|
| 162 |
+
title="Auto-detect language and smart target">
|
| 163 |
+
<i class="fa-solid fa-brain"></i> Smart
|
| 164 |
+
</button>
|
| 165 |
+
|
| 166 |
+
<!-- Voice Cloning Toggle -->
|
| 167 |
+
<button id="voice-clone-toggle" class="chip-pill active" title="Clone voice from input audio">
|
| 168 |
+
<i class="fa-solid fa-user-secret"></i> Clone Voice
|
| 169 |
+
</button>
|
| 170 |
+
</div>
|
| 171 |
+
</div>
|
| 172 |
+
</main>
|
| 173 |
+
|
| 174 |
+
<!-- AUDIO PLAYER (Hidden - for TTS playback) -->
|
| 175 |
+
<audio id="audio-player" style="display: none;"></audio>
|
| 176 |
+
</div>
|
| 177 |
+
|
| 178 |
+
<!-- SETTINGS MODAL (Preserved ID) -->
|
| 179 |
+
<div class="modal" id="settings-modal">
|
| 180 |
+
<button id="close-settings" class="close-modal-btn">
|
| 181 |
+
<i class="fa-solid fa-xmark"></i>
|
| 182 |
+
</button>
|
| 183 |
+
<div class="modal-content">
|
| 184 |
+
<h2>Parametres</h2>
|
| 185 |
+
<div class="form-group">
|
| 186 |
+
<label>Votre Langue</label>
|
| 187 |
+
<select id="source-lang-selector" class="form-control" style="height: auto; max-height: 200px;">
|
| 188 |
+
<option value="auto">Detection Automatique</option>
|
| 189 |
+
<optgroup label="Langues Principales">
|
| 190 |
+
<option value="fr">Français</option>
|
| 191 |
+
<option value="ar">Darija / Arabe</option>
|
| 192 |
+
<option value="en">Anglais</option>
|
| 193 |
+
<option value="es">Espagnol</option>
|
| 194 |
+
<option value="de">Allemand</option>
|
| 195 |
+
<option value="it">Italien</option>
|
| 196 |
+
<option value="zh">Chinois</option>
|
| 197 |
+
<option value="ja">Japonais</option>
|
| 198 |
+
<option value="ru">Russe</option>
|
| 199 |
+
</optgroup>
|
| 200 |
+
<optgroup label="✨ Toutes les Langues">
|
| 201 |
+
<option value="af">Afrikaans</option>
|
| 202 |
+
<option value="sq">Albanian (Albanais)</option>
|
| 203 |
+
<option value="am">Amharic (Amharique)</option>
|
| 204 |
+
<option value="hy">Armenian (Arménien)</option>
|
| 205 |
+
<option value="az">Azerbaijani (Azéri)</option>
|
| 206 |
+
<option value="eu">Basque</option>
|
| 207 |
+
<option value="be">Belarusian (Biélorusse)</option>
|
| 208 |
+
<option value="bn">Bengali</option>
|
| 209 |
+
<option value="bs">Bosnian (Bosnien)</option>
|
| 210 |
+
<option value="bg">Bulgarian (Bulgare)</option>
|
| 211 |
+
<option value="ca">Catalan</option>
|
| 212 |
+
<option value="ceb">Cebuano</option>
|
| 213 |
+
<option value="co">Corsican (Corse)</option>
|
| 214 |
+
<option value="hr">Croatian (Croate)</option>
|
| 215 |
+
<option value="cs">Czech (Tchèque)</option>
|
| 216 |
+
<option value="da">Danish (Danois)</option>
|
| 217 |
+
<option value="nl">Dutch (Néerlandais)</option>
|
| 218 |
+
<option value="et">Estonian (Estonien)</option>
|
| 219 |
+
<option value="fi">Finnish (Finnois)</option>
|
| 220 |
+
<option value="gl">Galician (Galicien)</option>
|
| 221 |
+
<option value="ka">Georgian (Géorgien)</option>
|
| 222 |
+
<option value="el">Greek (Grec)</option>
|
| 223 |
+
<option value="gu">Gujarati</option>
|
| 224 |
+
<option value="ht">Haitian Creole (Créole Haïtien)</option>
|
| 225 |
+
<option value="ha">Hausa</option>
|
| 226 |
+
<option value="haw">Hawaiian (Hawaïen)</option>
|
| 227 |
+
<option value="he">Hebrew (Hébreu)</option>
|
| 228 |
+
<option value="hi">Hindi</option>
|
| 229 |
+
<option value="hmn">Hmong</option>
|
| 230 |
+
<option value="hu">Hungarian (Hongrois)</option>
|
| 231 |
+
<option value="is">Icelandic (Islandais)</option>
|
| 232 |
+
<option value="ig">Igbo</option>
|
| 233 |
+
<option value="id">Indonesian (Indonésien)</option>
|
| 234 |
+
<option value="ga">Irish (Irlandais)</option>
|
| 235 |
+
<option value="jw">Javanese (Javanais)</option>
|
| 236 |
+
<option value="kn">Kannada</option>
|
| 237 |
+
<option value="kk">Kazakh</option>
|
| 238 |
+
<option value="km">Khmer</option>
|
| 239 |
+
<option value="ko">Korean (Coréen)</option>
|
| 240 |
+
<option value="ku">Kurdish (Kurde)</option>
|
| 241 |
+
<option value="ky">Kyrgyz (Kirghize)</option>
|
| 242 |
+
<option value="lo">Lao</option>
|
| 243 |
+
<option value="la">Latin</option>
|
| 244 |
+
<option value="lv">Latvian (Letton)</option>
|
| 245 |
+
<option value="lt">Lithuanian (Lituanien)</option>
|
| 246 |
+
<option value="lb">Luxembourgish (Luxembourgeois)</option>
|
| 247 |
+
<option value="mk">Macedonian (Macédonien)</option>
|
| 248 |
+
<option value="mg">Malagasy (Malgache)</option>
|
| 249 |
+
<option value="ms">Malay (Malais)</option>
|
| 250 |
+
<option value="ml">Malayalam</option>
|
| 251 |
+
<option value="mt">Maltese (Maltais)</option>
|
| 252 |
+
<option value="mi">Maori</option>
|
| 253 |
+
<option value="mr">Marathi</option>
|
| 254 |
+
<option value="mn">Mongolian (Mongol)</option>
|
| 255 |
+
<option value="my">Myanmar (Birman)</option>
|
| 256 |
+
<option value="ne">Nepali (Népalais)</option>
|
| 257 |
+
<option value="no">Norwegian (Norvégien)</option>
|
| 258 |
+
<option value="ny">Nyanja (Chichewa)</option>
|
| 259 |
+
<option value="ps">Pashto (Pachto)</option>
|
| 260 |
+
<option value="fa">Persian (Persan/Farsi)</option>
|
| 261 |
+
<option value="pl">Polish (Polonais)</option>
|
| 262 |
+
<option value="pa">Punjabi</option>
|
| 263 |
+
<option value="ro">Romanian (Roumain)</option>
|
| 264 |
+
<option value="sm">Samoan</option>
|
| 265 |
+
<option value="gd">Scottish Gaelic</option>
|
| 266 |
+
<option value="sr">Serbian (Serbe)</option>
|
| 267 |
+
<option value="sn">Shona</option>
|
| 268 |
+
<option value="sd">Sindhi</option>
|
| 269 |
+
<option value="si">Sinhala (Cingalais)</option>
|
| 270 |
+
<option value="sk">Slovak (Slovaque)</option>
|
| 271 |
+
<option value="sl">Slovenian (Slovêne)</option>
|
| 272 |
+
<option value="so">Somali</option>
|
| 273 |
+
<option value="su">Sundanese (Soundanais)</option>
|
| 274 |
+
<option value="sw">Swahili</option>
|
| 275 |
+
<option value="sv">Swedish (Suédois)</option>
|
| 276 |
+
<option value="tl">Tagalog (Tagalog/Filipino)</option>
|
| 277 |
+
<option value="tg">Tajik (Tadjik)</option>
|
| 278 |
+
<option value="ta">Tamil</option>
|
| 279 |
+
<option value="te">Telugu</option>
|
| 280 |
+
<option value="th">Thai (Thaï)</option>
|
| 281 |
+
<option value="tr">Turkish (Turc)</option>
|
| 282 |
+
<option value="uk">Ukrainian (Ukrainien)</option>
|
| 283 |
+
<option value="ur">Urdu</option>
|
| 284 |
+
<option value="uz">Uzbek (Ouzbek)</option>
|
| 285 |
+
<option value="vi">Vietnamese (Vietnamien)</option>
|
| 286 |
+
<option value="cy">Welsh (Gallois)</option>
|
| 287 |
+
<option value="xh">Xhosa</option>
|
| 288 |
+
<option value="yi">Yiddish</option>
|
| 289 |
+
<option value="yo">Yoruba</option>
|
| 290 |
+
<option value="zu">Zulu</option>
|
| 291 |
+
<div class="form-group">
|
| 292 |
+
<label>Langue Cible</label>
|
| 293 |
+
<!-- ✨ UNIVERSAL LANGUAGE SELECTOR -->
|
| 294 |
+
<select id="target-lang-quick" class="lang-quick-select">
|
| 295 |
+
<option value="French">‡«‡· French</option>
|
| 296 |
+
<option value="Arabic">‡¸‡¦ Arabic</option>
|
| 297 |
+
<option value="English">‡¬‡§ English</option>
|
| 298 |
+
<option value="Darija">‡²‡¦ Darija</option>
|
| 299 |
+
<option value="Spanish">‡ª‡¸ Spanish</option>
|
| 300 |
+
|
| 301 |
+
<optgroup label="Ϭ African & Middle Eastern">
|
| 302 |
+
<option value="Amharic">‡ª‡¹ Amharic</option>
|
| 303 |
+
<option value="Arabic">‡¸‡¦ Arabic (Standard)</option>
|
| 304 |
+
<option value="Darija">‡²‡¦ Arabic (Moroccan Darija)</option>
|
| 305 |
+
<option value="Berber">™“ Amazigh (Berber)</option>
|
| 306 |
+
<option value="Egyptian">‡ª‡¬ Arabic (Egyptian)</option>
|
| 307 |
+
<option value="Hausa">‡³‡¬ Hausa</option>
|
| 308 |
+
<option value="Hebrew">‡®‡± Hebrew</option>
|
| 309 |
+
<option value="Igbo">‡³‡¬ Igbo</option>
|
| 310 |
+
<option value="Persian">‡®‡· Persian (Farsi)</option>
|
| 311 |
+
<option value="Somali">‡¸‡´ Somali</option>
|
| 312 |
+
<option value="Swahili">‡°‡ª Swahili</option>
|
| 313 |
+
<option value="Turkish">‡¹‡· Turkish</option>
|
| 314 |
+
<option value="Yoruba">‡³‡¬ Yoruba</option>
|
| 315 |
+
<option value="Zulu">‡¿‡¦ Zulu</option>
|
| 316 |
+
</optgroup>
|
| 317 |
+
|
| 318 |
+
<optgroup label="✨ Asian & Pacific">
|
| 319 |
+
<option value="Bengali">‡§‡© Bengali</option>
|
| 320 |
+
<option value="Chinese">‡¨‡³ Chinese (Mandarin)</option>
|
| 321 |
+
<option value="Cantonese">‡‡° Chinese (Cantonese)</option>
|
| 322 |
+
<option value="Filipino">‡µ‡ Filipino (Tagalog)</option>
|
| 323 |
+
<option value="Gujarati">‡®‡³ Gujarati</option>
|
| 324 |
+
<option value="Hindi">‡®‡³ Hindi</option>
|
| 325 |
+
<option value="Indonesian">‡®‡© Indonesian</option>
|
| 326 |
+
<option value="Japanese">‡¯‡µ Japanese</option>
|
| 327 |
+
<option value="Javanese">‡®‡© Javanese</option>
|
| 328 |
+
<option value="Kannada">‡®‡³ Kannada</option>
|
| 329 |
+
<option value="Khmer">‡°‡ Khmer</option>
|
| 330 |
+
<option value="Korean">‡°‡· Korean</option>
|
| 331 |
+
<option value="Lao">‡±‡¦ Lao</option>
|
| 332 |
+
<option value="Malay">‡²‡¾ Malay</option>
|
| 333 |
+
<option value="Malayalam">‡®‡³ Malayalam</option>
|
| 334 |
+
<option value="Marathi">‡®‡³ Marathi</option>
|
| 335 |
+
<option value="Myanmar">‡²‡² Myanmar (Burmese)</option>
|
| 336 |
+
<option value="Nepali">‡³‡µ Nepali</option>
|
| 337 |
+
<option value="Punjabi">‡®‡³ Punjabi</option>
|
| 338 |
+
<option value="Sinhala">‡±‡° Sinhala</option>
|
| 339 |
+
<option value="Tamil">‡®‡³ Tamil</option>
|
| 340 |
+
<option value="Telugu">‡®‡³ Telugu</option>
|
| 341 |
+
<option value="Thai">‡¹‡ Thai</option>
|
| 342 |
+
<option value="Urdu">‡µ‡° Urdu</option>
|
| 343 |
+
<option value="Vietnamese">‡»‡³ Vietnamese</option>
|
| 344 |
+
</optgroup>
|
| 345 |
+
|
| 346 |
+
<optgroup label="✨ European">
|
| 347 |
+
<option value="Albanian">‡¦‡± Albanian</option>
|
| 348 |
+
<option value="Armenian">‡¦‡² Armenian</option>
|
| 349 |
+
<option value="Azerbaijani">‡¦‡¿ Azerbaijani</option>
|
| 350 |
+
<option value="Bosnian">‡§‡¦ Bosnian</option>
|
| 351 |
+
<option value="Bulgarian">‡§‡¬ Bulgarian</option>
|
| 352 |
+
<option value="Catalan">‡ª‡¸ Catalan</option>
|
| 353 |
+
<option value="Croatian">‡‡· Croatian</option>
|
| 354 |
+
<option value="Czech">‡¨‡¿ Czech</option>
|
| 355 |
+
<option value="Danish">‡©‡° Danish</option>
|
| 356 |
+
<option value="Dutch">‡³‡± Dutch</option>
|
| 357 |
+
<option value="Estonian">‡ª‡ª Estonian</option>
|
| 358 |
+
<option value="Finnish">‡«‡® Finnish</option>
|
| 359 |
+
<option value="French">‡«‡· French</option>
|
| 360 |
+
<option value="Georgian">‡¬‡ª Georgian</option>
|
| 361 |
+
<option value="German">‡©‡ª German</option>
|
| 362 |
+
<option value="Greek">‡¬‡· Greek</option>
|
| 363 |
+
<option value="Hungarian">‡‡º Hungarian</option>
|
| 364 |
+
<option value="Icelandic">‡®‡¸ Icelandic</option>
|
| 365 |
+
<option value="Irish">‡®‡ª Irish</option>
|
| 366 |
+
<option value="Italian">‡®‡¹ Italian</option>
|
| 367 |
+
<option value="Latvian">‡±‡» Latvian</option>
|
| 368 |
+
<option value="Lithuanian">‡±‡¹ Lithuanian</option>
|
| 369 |
+
<option value="Macedonian">‡²‡° Macedonian</option>
|
| 370 |
+
<option value="Maltese">‡²‡¹ Maltese</option>
|
| 371 |
+
<option value="Norwegian">‡³‡´ Norwegian</option>
|
| 372 |
+
<option value="Polish">‡µ‡± Polish</option>
|
| 373 |
+
<option value="Portuguese">‡µ‡¹ Portuguese</option>
|
| 374 |
+
<option value="Romanian">‡·‡´ Romanian</option>
|
| 375 |
+
<option value="Russian">‡·‡º Russian</option>
|
| 376 |
+
<option value="Serbian">‡·‡¸ Serbian</option>
|
| 377 |
+
<option value="Slovak">‡¸‡° Slovak</option>
|
| 378 |
+
<option value="Slovenian">‡¸‡® Slovenian</option>
|
| 379 |
+
<option value="Spanish">‡ª‡¸ Spanish</option>
|
| 380 |
+
<option value="Swedish">‡¸‡ª Swedish</option>
|
| 381 |
+
<option value="Ukrainian">‡º‡¦ Ukrainian</option>
|
| 382 |
+
<option value="Welsh">´ó §ó ¢ó ·ó ¬ó ³ó ¿ Welsh</option>
|
| 383 |
+
</optgroup>
|
| 384 |
+
</select>
|
| 385 |
+
</div>
|
| 386 |
+
|
| 387 |
+
<!-- § AI MODEL SELECTOR (New) -->
|
| 388 |
+
<div class="form-group" style="margin-top: 16px;">
|
| 389 |
+
<label>Mode d'Intelligence (Cerveau)</label>
|
| 390 |
+
<select id="ai-model-selector" class="form-control">
|
| 391 |
+
<option value="gpt-4o-mini">Ÿ¢ OpenAI GPT-4o (Stable & Illimité)</option>
|
| 392 |
+
<option value="gemini-flash-latest">š¡ Google Gemini Flash (Experimental)</option>
|
| 393 |
+
<option value="gemini-pro">’Ž Google Gemini Pro (Balanced)</option>
|
| 394 |
+
</select>
|
| 395 |
+
</div>
|
| 396 |
+
|
| 397 |
+
|
| 398 |
+
<audio id="audio-player"></audio>
|
| 399 |
+
|
| 400 |
+
<script src="https://cdn.jsdelivr.net/npm/axios/dist/axios.min.js"></script>
|
| 401 |
+
<script src="/static/enhanced.js"></script>
|
| 402 |
+
<script src="/static/script.js"></script>
|
| 403 |
+
|
| 404 |
+
|
| 405 |
+
<script>
|
| 406 |
+
// Console log helper
|
| 407 |
+
function log(m) { console.log(m); }
|
| 408 |
+
|
| 409 |
+
// Settings Modal
|
| 410 |
+
const modal = document.getElementById('settings-modal');
|
| 411 |
+
document.getElementById('settings-trigger').onclick = () => modal.classList.add('open');
|
| 412 |
+
document.getElementById('close-settings').onclick = () => modal.classList.remove('open');
|
| 413 |
+
document.getElementById('save-settings').onclick = () => {
|
| 414 |
+
if (window.saveModalSettings) window.saveModalSettings();
|
| 415 |
+
modal.classList.remove('open');
|
| 416 |
+
};
|
| 417 |
+
|
| 418 |
+
// UI SYNC - Chat messages are handled by script.js createChatMessage()
|
| 419 |
+
// This observer is only for real-time status updates
|
| 420 |
+
const greeting = document.getElementById('greeting');
|
| 421 |
+
const status = document.getElementById('status-placeholder');
|
| 422 |
+
|
| 423 |
+
const observer = new MutationObserver((mutations) => {
|
| 424 |
+
mutations.forEach(m => {
|
| 425 |
+
if (m.target.id === 'original-text') {
|
| 426 |
+
const txt = m.target.innerText;
|
| 427 |
+
if (txt && txt !== '...') {
|
| 428 |
+
status.innerText = txt.substring(0, 50) + (txt.length > 50 ? '...' : '');
|
| 429 |
+
greeting.style.display = 'none';
|
| 430 |
+
}
|
| 431 |
+
}
|
| 432 |
+
});
|
| 433 |
+
});
|
| 434 |
+
|
| 435 |
+
observer.observe(document.getElementById('original-text'), { childList: true, characterData: true, subtree: true });
|
| 436 |
+
|
| 437 |
+
// SMART CONVERSATION MODE TOGGLE
|
| 438 |
+
const smartModeToggle = document.getElementById('smart-mode-toggle');
|
| 439 |
+
const statusPlaceholder = document.getElementById('status-placeholder');
|
| 440 |
+
|
| 441 |
+
const savedSmartMode = localStorage.getItem('smartModeEnabled');
|
| 442 |
+
if (savedSmartMode === 'false') {
|
| 443 |
+
smartModeToggle.classList.remove('active');
|
| 444 |
+
} else {
|
| 445 |
+
smartModeToggle.classList.add('active');
|
| 446 |
+
localStorage.setItem('smartModeEnabled', 'true');
|
| 447 |
+
}
|
| 448 |
+
|
| 449 |
+
smartModeToggle.onclick = () => {
|
| 450 |
+
smartModeToggle.classList.toggle('active');
|
| 451 |
+
const isActive = smartModeToggle.classList.contains('active');
|
| 452 |
+
localStorage.setItem('smartModeEnabled', isActive);
|
| 453 |
+
|
| 454 |
+
// Update status
|
| 455 |
+
if (isActive) {
|
| 456 |
+
console.log('Smart Conversation Mode ENABLED');
|
| 457 |
+
statusPlaceholder.innerText = 'Mode intelligent';
|
| 458 |
+
setTimeout(() => {
|
| 459 |
+
if (statusPlaceholder.innerText === 'Mode intelligent') {
|
| 460 |
+
statusPlaceholder.innerText = 'Pret';
|
| 461 |
+
}
|
| 462 |
+
}, 2000);
|
| 463 |
+
} else {
|
| 464 |
+
console.log('Smart Mode DISABLED - Using fixed target');
|
| 465 |
+
statusPlaceholder.innerText = 'Cible fixe';
|
| 466 |
+
setTimeout(() => {
|
| 467 |
+
if (statusPlaceholder.innerText === 'Cible fixe') {
|
| 468 |
+
statusPlaceholder.innerText = 'Pret';
|
| 469 |
+
}
|
| 470 |
+
}, 2000);
|
| 471 |
+
}
|
| 472 |
+
};
|
| 473 |
+
|
| 474 |
+
document.addEventListener('reset-ui', () => {
|
| 475 |
+
document.getElementById('status-placeholder').innerText = 'Pret';
|
| 476 |
+
});
|
| 477 |
+
|
| 478 |
+
// VOICE CLONING TOGGLE
|
| 479 |
+
let voiceCloneEnabled = localStorage.getItem('voiceCloneEnabled') === 'true';
|
| 480 |
+
const cloneToggle = document.getElementById('clone-toggle');
|
| 481 |
+
|
| 482 |
+
if (cloneToggle) {
|
| 483 |
+
function updateCloneToggle() {
|
| 484 |
+
if (voiceCloneEnabled) {
|
| 485 |
+
cloneToggle.classList.add('active');
|
| 486 |
+
cloneToggle.title = 'Clonage de Voix: Active';
|
| 487 |
+
} else {
|
| 488 |
+
cloneToggle.classList.remove('active');
|
| 489 |
+
cloneToggle.title = 'Clonage de Voix: Desactive';
|
| 490 |
+
}
|
| 491 |
+
}
|
| 492 |
+
|
| 493 |
+
cloneToggle.addEventListener('click', () => {
|
| 494 |
+
voiceCloneEnabled = !voiceCloneEnabled;
|
| 495 |
+
localStorage.setItem('voiceCloneEnabled', voiceCloneEnabled);
|
| 496 |
+
updateCloneToggle();
|
| 497 |
+
});
|
| 498 |
+
|
| 499 |
+
updateCloneToggle();
|
| 500 |
+
}
|
| 501 |
+
|
| 502 |
+
// GRAMMAR CORRECTION TOGGLE
|
| 503 |
+
let grammarCorrectionEnabled = localStorage.getItem('grammarCorrectionEnabled') !== 'false';
|
| 504 |
+
const grammarToggle = document.getElementById('grammar-toggle');
|
| 505 |
+
|
| 506 |
+
if (grammarToggle) {
|
| 507 |
+
function updateGrammarToggle() {
|
| 508 |
+
if (grammarCorrectionEnabled) {
|
| 509 |
+
grammarToggle.classList.add('active');
|
| 510 |
+
grammarToggle.title = 'Correction Intelligente: Active';
|
| 511 |
+
} else {
|
| 512 |
+
grammarToggle.classList.remove('active');
|
| 513 |
+
grammarToggle.title = 'Correction Intelligente: Desactive';
|
| 514 |
+
}
|
| 515 |
+
}
|
| 516 |
+
|
| 517 |
+
grammarToggle.addEventListener('click', () => {
|
| 518 |
+
grammarCorrectionEnabled = !grammarCorrectionEnabled;
|
| 519 |
+
localStorage.setItem('grammarCorrectionEnabled', grammarCorrectionEnabled);
|
| 520 |
+
updateGrammarToggle();
|
| 521 |
+
});
|
| 522 |
+
|
| 523 |
+
updateGrammarToggle();
|
| 524 |
+
}
|
| 525 |
+
|
| 526 |
+
// AI CORRECTION TOGGLE (Claude/GPT Reasoning)
|
| 527 |
+
// Read from localStorage - default is FALSE (disabled by default)
|
| 528 |
+
const savedAiCorrection = localStorage.getItem('aiCorrectionEnabled');
|
| 529 |
+
let aiCorrectionEnabled = savedAiCorrection === 'true'; // Only true if explicitly set to 'true'
|
| 530 |
+
console.log('📦 AI Correction from localStorage:', savedAiCorrection, '→', aiCorrectionEnabled);
|
| 531 |
+
|
| 532 |
+
const aiCorrectionToggle = document.getElementById('ai-correction-toggle');
|
| 533 |
+
const aiCorrectionHidden = document.getElementById('ai-correction');
|
| 534 |
+
const aiCorrectionCheckbox = document.getElementById('ai-correction-checkbox');
|
| 535 |
+
|
| 536 |
+
function updateAiCorrectionState() {
|
| 537 |
+
console.log('🔄 AI Correction State Update:', aiCorrectionEnabled);
|
| 538 |
+
|
| 539 |
+
// Update hidden input
|
| 540 |
+
if (aiCorrectionHidden) aiCorrectionHidden.value = aiCorrectionEnabled ? 'true' : 'false';
|
| 541 |
+
|
| 542 |
+
// Update checkbox in settings
|
| 543 |
+
if (aiCorrectionCheckbox) aiCorrectionCheckbox.checked = aiCorrectionEnabled;
|
| 544 |
+
|
| 545 |
+
// Update toggle button visual state
|
| 546 |
+
if (aiCorrectionToggle) {
|
| 547 |
+
if (aiCorrectionEnabled) {
|
| 548 |
+
aiCorrectionToggle.classList.add('active');
|
| 549 |
+
aiCorrectionToggle.style.background = 'linear-gradient(135deg, #667eea, #764ba2)';
|
| 550 |
+
aiCorrectionToggle.style.color = '#fff';
|
| 551 |
+
aiCorrectionToggle.title = '🧠 AI Correction: ACTIVÉ (Claude/GPT)';
|
| 552 |
+
aiCorrectionToggle.innerHTML = '<i class="fa-solid fa-sparkles"></i> AI ✓';
|
| 553 |
+
} else {
|
| 554 |
+
aiCorrectionToggle.classList.remove('active');
|
| 555 |
+
aiCorrectionToggle.style.background = 'rgba(255,255,255,0.1)';
|
| 556 |
+
aiCorrectionToggle.style.color = 'rgba(255,255,255,0.5)';
|
| 557 |
+
aiCorrectionToggle.title = '❌ AI Correction: DÉSACTIVÉ';
|
| 558 |
+
aiCorrectionToggle.innerHTML = '<i class="fa-solid fa-sparkles"></i> AI ✗';
|
| 559 |
+
}
|
| 560 |
+
}
|
| 561 |
+
}
|
| 562 |
+
|
| 563 |
+
if (aiCorrectionToggle) {
|
| 564 |
+
aiCorrectionToggle.addEventListener('click', (e) => {
|
| 565 |
+
e.preventDefault();
|
| 566 |
+
e.stopPropagation();
|
| 567 |
+
|
| 568 |
+
// Toggle the state
|
| 569 |
+
aiCorrectionEnabled = !aiCorrectionEnabled;
|
| 570 |
+
|
| 571 |
+
// Save to localStorage as string
|
| 572 |
+
localStorage.setItem('aiCorrectionEnabled', aiCorrectionEnabled.toString());
|
| 573 |
+
|
| 574 |
+
// Update UI
|
| 575 |
+
updateAiCorrectionState();
|
| 576 |
+
|
| 577 |
+
// Log for debugging
|
| 578 |
+
console.log('🧠 AI Correction TOGGLED to:', aiCorrectionEnabled ? 'ENABLED ✓' : 'DISABLED ✗');
|
| 579 |
+
|
| 580 |
+
// Visual feedback
|
| 581 |
+
aiCorrectionToggle.style.transform = 'scale(0.95)';
|
| 582 |
+
setTimeout(() => aiCorrectionToggle.style.transform = '', 150);
|
| 583 |
+
});
|
| 584 |
+
}
|
| 585 |
+
|
| 586 |
+
if (aiCorrectionCheckbox) {
|
| 587 |
+
aiCorrectionCheckbox.addEventListener('change', () => {
|
| 588 |
+
aiCorrectionEnabled = aiCorrectionCheckbox.checked;
|
| 589 |
+
localStorage.setItem('aiCorrectionEnabled', aiCorrectionEnabled.toString());
|
| 590 |
+
updateAiCorrectionState();
|
| 591 |
+
console.log('🧠 AI Correction (checkbox) set to:', aiCorrectionEnabled);
|
| 592 |
+
});
|
| 593 |
+
}
|
| 594 |
+
|
| 595 |
+
// Initialize state on page load
|
| 596 |
+
updateAiCorrectionState();
|
| 597 |
+
console.log('✅ AI Correction Toggle initialized:', aiCorrectionEnabled ? 'ON' : 'OFF');
|
| 598 |
+
|
| 599 |
+
// 🔄 SWAP LANGUAGES BUTTON
|
| 600 |
+
const swapBtn = document.getElementById('swap-langs');
|
| 601 |
+
// Using getElementById directly to avoid redeclaration
|
| 602 |
+
const srcLang = document.getElementById('source-lang-quick');
|
| 603 |
+
const tgtLang = document.getElementById('target-lang-quick');
|
| 604 |
+
|
| 605 |
+
if (swapBtn && srcLang && tgtLang) {
|
| 606 |
+
swapBtn.addEventListener('click', (e) => {
|
| 607 |
+
e.preventDefault();
|
| 608 |
+
|
| 609 |
+
// Get current values
|
| 610 |
+
const currentSource = srcLang.value;
|
| 611 |
+
const currentTarget = tgtLang.value;
|
| 612 |
+
|
| 613 |
+
// Swap them
|
| 614 |
+
if (currentSource !== 'auto') {
|
| 615 |
+
// Find matching option in target
|
| 616 |
+
const targetOptions = Array.from(tgtLang.options).map(o => o.value);
|
| 617 |
+
const sourceOptions = Array.from(srcLang.options).map(o => o.value);
|
| 618 |
+
|
| 619 |
+
if (targetOptions.includes(currentSource) && sourceOptions.includes(currentTarget)) {
|
| 620 |
+
srcLang.value = currentTarget;
|
| 621 |
+
tgtLang.value = currentSource;
|
| 622 |
+
} else {
|
| 623 |
+
// Fallback: just swap to auto and the target
|
| 624 |
+
srcLang.value = 'auto';
|
| 625 |
+
tgtLang.value = currentSource === 'auto' ? 'fr-FR' : currentSource;
|
| 626 |
+
}
|
| 627 |
+
} else {
|
| 628 |
+
// Source is auto - swap target with a default
|
| 629 |
+
const oldTarget = currentTarget;
|
| 630 |
+
tgtLang.value = oldTarget === 'fr-FR' ? 'ar-SA' : 'fr-FR';
|
| 631 |
+
}
|
| 632 |
+
|
| 633 |
+
// Save to localStorage
|
| 634 |
+
localStorage.setItem('sourceLangQuick', srcLang.value);
|
| 635 |
+
localStorage.setItem('targetLangQuick', tgtLang.value);
|
| 636 |
+
|
| 637 |
+
// Visual feedback
|
| 638 |
+
swapBtn.style.transform = 'rotate(180deg)';
|
| 639 |
+
setTimeout(() => swapBtn.style.transform = '', 300);
|
| 640 |
+
|
| 641 |
+
console.log(`🔄 Languages swapped: ${srcLang.value} → ${tgtLang.value}`);
|
| 642 |
+
});
|
| 643 |
+
}
|
| 644 |
+
|
| 645 |
+
// 🧠 SMART AUTO-SWAP: When language is detected, auto-swap target
|
| 646 |
+
// This is handled in processAudio response
|
| 647 |
+
|
| 648 |
+
// STT ENGINE SELECTOR
|
| 649 |
+
const sttSelector = document.getElementById('stt-selector-settings');
|
| 650 |
+
const sttEngineHidden = document.getElementById('stt-engine');
|
| 651 |
+
const savedSttEngine = localStorage.getItem('sttEngine') || 'seamless-m4t';
|
| 652 |
+
|
| 653 |
+
if (sttSelector) {
|
| 654 |
+
sttSelector.value = savedSttEngine;
|
| 655 |
+
if (sttEngineHidden) sttEngineHidden.value = savedSttEngine;
|
| 656 |
+
|
| 657 |
+
sttSelector.addEventListener('change', function () {
|
| 658 |
+
localStorage.setItem('sttEngine', this.value);
|
| 659 |
+
if (sttEngineHidden) sttEngineHidden.value = this.value;
|
| 660 |
+
console.log('🎤 STT Engine set to:', this.value);
|
| 661 |
+
});
|
| 662 |
+
}
|
| 663 |
+
|
| 664 |
+
// 🔊 TTS ENGINE SELECTOR
|
| 665 |
+
const ttsSelector = document.getElementById('tts-selector');
|
| 666 |
+
const ttsEngineHidden = document.getElementById('tts-engine');
|
| 667 |
+
const savedTtsEngine = localStorage.getItem('ttsEngine') || 'seamless';
|
| 668 |
+
|
| 669 |
+
if (ttsSelector) {
|
| 670 |
+
ttsSelector.value = savedTtsEngine;
|
| 671 |
+
if (ttsEngineHidden) ttsEngineHidden.value = savedTtsEngine;
|
| 672 |
+
|
| 673 |
+
ttsSelector.addEventListener('change', function () {
|
| 674 |
+
localStorage.setItem('ttsEngine', this.value);
|
| 675 |
+
if (ttsEngineHidden) ttsEngineHidden.value = this.value;
|
| 676 |
+
console.log('🔊 TTS Engine set to:', this.value);
|
| 677 |
+
|
| 678 |
+
// Visual feedback
|
| 679 |
+
this.style.backgroundColor = this.value === 'seamless' ? '#4CAF50' : '#2196F3';
|
| 680 |
+
setTimeout(() => this.style.backgroundColor = '', 500);
|
| 681 |
+
});
|
| 682 |
+
}
|
| 683 |
+
|
| 684 |
+
// 🌍 TRANSLATION ENGINE SELECTOR (NLLB vs MarianMT)
|
| 685 |
+
const translationEngineSelector = document.getElementById('translation-engine-selector');
|
| 686 |
+
const translationEngineHidden = document.getElementById('translation-engine');
|
| 687 |
+
const nllbModelSizeSelector = document.getElementById('nllb-model-size-selector');
|
| 688 |
+
const nllbModelSizeHidden = document.getElementById('nllb-model-size');
|
| 689 |
+
const nllbModelSizeGroup = document.getElementById('nllb-model-size-group');
|
| 690 |
+
|
| 691 |
+
const savedTranslationEngine = localStorage.getItem('translationEngine') || 'nllb';
|
| 692 |
+
const savedNllbModelSize = localStorage.getItem('nllbModelSize') || 'fast';
|
| 693 |
+
|
| 694 |
+
function updateNllbModelSizeVisibility() {
|
| 695 |
+
if (nllbModelSizeGroup) {
|
| 696 |
+
nllbModelSizeGroup.style.display =
|
| 697 |
+
translationEngineSelector && translationEngineSelector.value === 'nllb' ? 'block' : 'none';
|
| 698 |
+
}
|
| 699 |
+
}
|
| 700 |
+
|
| 701 |
+
if (translationEngineSelector) {
|
| 702 |
+
translationEngineSelector.value = savedTranslationEngine;
|
| 703 |
+
if (translationEngineHidden) translationEngineHidden.value = savedTranslationEngine;
|
| 704 |
+
updateNllbModelSizeVisibility();
|
| 705 |
+
|
| 706 |
+
translationEngineSelector.addEventListener('change', function () {
|
| 707 |
+
localStorage.setItem('translationEngine', this.value);
|
| 708 |
+
if (translationEngineHidden) translationEngineHidden.value = this.value;
|
| 709 |
+
console.log('🌍 Translation Engine set to:', this.value);
|
| 710 |
+
updateNllbModelSizeVisibility();
|
| 711 |
+
});
|
| 712 |
+
}
|
| 713 |
+
|
| 714 |
+
if (nllbModelSizeSelector) {
|
| 715 |
+
nllbModelSizeSelector.value = savedNllbModelSize;
|
| 716 |
+
if (nllbModelSizeHidden) nllbModelSizeHidden.value = savedNllbModelSize;
|
| 717 |
+
|
| 718 |
+
nllbModelSizeSelector.addEventListener('change', function () {
|
| 719 |
+
localStorage.setItem('nllbModelSize', this.value);
|
| 720 |
+
if (nllbModelSizeHidden) nllbModelSizeHidden.value = this.value;
|
| 721 |
+
console.log('🧠 NLLB Model Size set to:', this.value);
|
| 722 |
+
});
|
| 723 |
+
}
|
| 724 |
+
|
| 725 |
+
|
| 726 |
+
// SMART CONVERSATION MODE TOGGLE
|
| 727 |
+
const smartModeToggle = document.getElementById('smart-mode-toggle');
|
| 728 |
+
// ... (existing code for smart mode)
|
| 729 |
+
|
| 730 |
+
// REPLAY BUTTON LOGIC
|
| 731 |
+
const replayBtn = document.getElementById('replay-trigger');
|
| 732 |
+
if (replayBtn) {
|
| 733 |
+
replayBtn.addEventListener('click', () => {
|
| 734 |
+
if (window.lastBotAudio) {
|
| 735 |
+
console.log('”„ Replaying last audio...');
|
| 736 |
+
// Add visual effect
|
| 737 |
+
const icon = replayBtn.querySelector('i');
|
| 738 |
+
icon.style.transition = 'transform 0.5s ease';
|
| 739 |
+
icon.style.transform = 'rotate(-360deg)';
|
| 740 |
+
|
| 741 |
+
window.lastBotAudio.currentTime = 0;
|
| 742 |
+
window.lastBotAudio.play().catch(e => console.error("Replay failed:", e));
|
| 743 |
+
|
| 744 |
+
setTimeout(() => {
|
| 745 |
+
icon.style.transition = 'none';
|
| 746 |
+
icon.style.transform = 'rotate(0deg)';
|
| 747 |
+
}, 500);
|
| 748 |
+
} else {
|
| 749 |
+
// No audio to replay - shake animation
|
| 750 |
+
console.log('š ï¸ No audio to replay');
|
| 751 |
+
replayBtn.style.animation = 'shake 0.4s cubic-bezier(.36,.07,.19,.97) both';
|
| 752 |
+
setTimeout(() => replayBtn.style.animation = '', 400);
|
| 753 |
+
}
|
| 754 |
+
});
|
| 755 |
+
}
|
| 756 |
+
|
| 757 |
+
// Add Shake Animation Keyframes
|
| 758 |
+
const styleSheet = document.createElement("style");
|
| 759 |
+
styleSheet.innerText = `
|
| 760 |
+
@keyframes shake {
|
| 761 |
+
10%, 90% { transform: translate3d(-1px, 0, 0); }
|
| 762 |
+
20%, 80% { transform: translate3d(2px, 0, 0); }
|
| 763 |
+
30%, 50%, 70% { transform: translate3d(-4px, 0, 0); }
|
| 764 |
+
40%, 60% { transform: translate3d(4px, 0, 0); }
|
| 765 |
+
}
|
| 766 |
+
`;
|
| 767 |
+
document.head.appendChild(styleSheet);
|
| 768 |
+
|
| 769 |
+
// VOICE GENDER SELECTOR (3 states: auto, male, female)
|
| 770 |
+
let voiceGenderPreference = localStorage.getItem('voiceGenderPreference') || 'auto';
|
| 771 |
+
const voiceGenderToggle = document.getElementById('voice-gender-toggle');
|
| 772 |
+
|
| 773 |
+
if (voiceGenderToggle) {
|
| 774 |
+
const icons = {
|
| 775 |
+
'auto': 'fa-user-gear',
|
| 776 |
+
'male': 'fa-person',
|
| 777 |
+
'female': 'fa-person-dress'
|
| 778 |
+
};
|
| 779 |
+
|
| 780 |
+
const titles = {
|
| 781 |
+
'auto': 'Voix: Auto',
|
| 782 |
+
'male': 'Voix: Masculine',
|
| 783 |
+
'female': 'Voix: Feminine'
|
| 784 |
+
};
|
| 785 |
+
|
| 786 |
+
function updateVoiceGenderToggle() {
|
| 787 |
+
voiceGenderToggle.classList.remove('auto', 'male', 'female');
|
| 788 |
+
voiceGenderToggle.classList.add(voiceGenderPreference);
|
| 789 |
+
|
| 790 |
+
const iconElement = voiceGenderToggle.querySelector('i');
|
| 791 |
+
iconElement.className = `fa-solid ${icons[voiceGenderPreference]}`;
|
| 792 |
+
voiceGenderToggle.title = titles[voiceGenderPreference];
|
| 793 |
+
}
|
| 794 |
+
|
| 795 |
+
voiceGenderToggle.addEventListener('click', () => {
|
| 796 |
+
if (voiceGenderPreference === 'auto') {
|
| 797 |
+
voiceGenderPreference = 'male';
|
| 798 |
+
} else if (voiceGenderPreference === 'male') {
|
| 799 |
+
voiceGenderPreference = 'female';
|
| 800 |
+
} else {
|
| 801 |
+
voiceGenderPreference = 'auto';
|
| 802 |
+
}
|
| 803 |
+
localStorage.setItem('voiceGenderPreference', voiceGenderPreference);
|
| 804 |
+
updateVoiceGenderToggle();
|
| 805 |
+
});
|
| 806 |
+
|
| 807 |
+
updateVoiceGenderToggle();
|
| 808 |
+
}
|
| 809 |
+
|
| 810 |
+
// QUICK LANGUAGE SELECTORS
|
| 811 |
+
const sourceLangQuick = document.getElementById('source-lang-quick');
|
| 812 |
+
const targetLangQuick = document.getElementById('target-lang-quick');
|
| 813 |
+
const swapLangsBtn = document.getElementById('swap-langs');
|
| 814 |
+
const quickLangSelector = document.getElementById('quick-lang-selector');
|
| 815 |
+
|
| 816 |
+
// Load saved preferences
|
| 817 |
+
const savedSourceLang = localStorage.getItem('sourceLangQuick') || 'auto';
|
| 818 |
+
const savedTargetLang = localStorage.getItem('targetLangQuick') || 'French';
|
| 819 |
+
|
| 820 |
+
if (sourceLangQuick) sourceLangQuick.value = savedSourceLang;
|
| 821 |
+
if (targetLangQuick) targetLangQuick.value = savedTargetLang;
|
| 822 |
+
if (quickLangSelector) quickLangSelector.value = savedTargetLang;
|
| 823 |
+
|
| 824 |
+
// Sync source language with script.js
|
| 825 |
+
if (sourceLangQuick) {
|
| 826 |
+
sourceLangQuick.addEventListener('change', function () {
|
| 827 |
+
localStorage.setItem('sourceLangQuick', this.value);
|
| 828 |
+
console.log('ޤ Source language set to:', this.value);
|
| 829 |
+
|
| 830 |
+
// Clear cache for new language
|
| 831 |
+
fetch('/clear_cache', { method: 'POST' });
|
| 832 |
+
|
| 833 |
+
// Update status
|
| 834 |
+
const langName = this.options[this.selectedIndex].text;
|
| 835 |
+
statusPlaceholder.innerText = langName;
|
| 836 |
+
setTimeout(() => {
|
| 837 |
+
if (statusPlaceholder.innerText === langName) {
|
| 838 |
+
statusPlaceholder.innerText = 'Pret';
|
| 839 |
+
}
|
| 840 |
+
}, 1500);
|
| 841 |
+
});
|
| 842 |
+
}
|
| 843 |
+
|
| 844 |
+
// Sync target language with existing selector
|
| 845 |
+
if (targetLangQuick) {
|
| 846 |
+
targetLangQuick.addEventListener('change', function () {
|
| 847 |
+
localStorage.setItem('targetLangQuick', this.value);
|
| 848 |
+
localStorage.setItem('targetLang', this.value);
|
| 849 |
+
|
| 850 |
+
// Sync with modal selector
|
| 851 |
+
if (quickLangSelector) quickLangSelector.value = this.value;
|
| 852 |
+
|
| 853 |
+
console.log('✨ Target language set to:', this.value);
|
| 854 |
+
|
| 855 |
+
// Clear cache for new language
|
| 856 |
+
fetch('/clear_cache', { method: 'POST' });
|
| 857 |
+
});
|
| 858 |
+
}
|
| 859 |
+
|
| 860 |
+
// SWAP LANGUAGES BUTTON
|
| 861 |
+
if (swapLangsBtn && sourceLangQuick && targetLangQuick) {
|
| 862 |
+
swapLangsBtn.addEventListener('click', function () {
|
| 863 |
+
// Map between source codes and target names
|
| 864 |
+
const sourceToTarget = {
|
| 865 |
+
'ar-SA': 'Arabic',
|
| 866 |
+
'fr-FR': 'French',
|
| 867 |
+
'en-US': 'English',
|
| 868 |
+
'es-ES': 'Spanish',
|
| 869 |
+
'de-DE': 'German',
|
| 870 |
+
'auto': 'auto'
|
| 871 |
+
};
|
| 872 |
+
|
| 873 |
+
const targetToSource = {
|
| 874 |
+
'Arabic': 'ar-SA',
|
| 875 |
+
'Moroccan Darija': 'ar-SA',
|
| 876 |
+
'French': 'fr-FR',
|
| 877 |
+
'English': 'en-US',
|
| 878 |
+
'Spanish': 'es-ES',
|
| 879 |
+
'German': 'de-DE'
|
| 880 |
+
};
|
| 881 |
+
|
| 882 |
+
const currentSource = sourceLangQuick.value;
|
| 883 |
+
const currentTarget = targetLangQuick.value;
|
| 884 |
+
|
| 885 |
+
// Swap
|
| 886 |
+
const newSource = targetToSource[currentTarget] || 'auto';
|
| 887 |
+
const newTarget = sourceToTarget[currentSource] || 'French';
|
| 888 |
+
|
| 889 |
+
sourceLangQuick.value = newSource;
|
| 890 |
+
targetLangQuick.value = newTarget;
|
| 891 |
+
|
| 892 |
+
// Save
|
| 893 |
+
localStorage.setItem('sourceLangQuick', newSource);
|
| 894 |
+
localStorage.setItem('targetLangQuick', newTarget);
|
| 895 |
+
localStorage.setItem('targetLang', newTarget);
|
| 896 |
+
|
| 897 |
+
if (quickLangSelector) quickLangSelector.value = newTarget;
|
| 898 |
+
|
| 899 |
+
// Visual feedback
|
| 900 |
+
this.style.transform = 'rotate(180deg)';
|
| 901 |
+
setTimeout(() => {
|
| 902 |
+
this.style.transform = 'rotate(0deg)';
|
| 903 |
+
}, 300);
|
| 904 |
+
|
| 905 |
+
console.log('”„ Languages swapped:', newSource, '†”', newTarget);
|
| 906 |
+
|
| 907 |
+
// Clear cache
|
| 908 |
+
fetch('/clear_cache', { method: 'POST' });
|
| 909 |
+
});
|
| 910 |
+
}
|
| 911 |
+
</script>
|
| 912 |
+
</body>
|
| 913 |
+
|
| 914 |
+
</html>
|
templates/index_new.html
ADDED
|
@@ -0,0 +1,727 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
<!DOCTYPE html>
|
| 2 |
+
<html lang="fr">
|
| 3 |
+
|
| 4 |
+
<head>
|
| 5 |
+
<meta charset="UTF-8">
|
| 6 |
+
<meta name="viewport"
|
| 7 |
+
content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no, viewport-fit=cover">
|
| 8 |
+
|
| 9 |
+
<!-- 🌍 PWA -->
|
| 10 |
+
<meta name="theme-color" content="#22c55e">
|
| 11 |
+
<meta name="apple-mobile-web-app-capable" content="yes">
|
| 12 |
+
<meta name="apple-mobile-web-app-status-bar-style" content="default">
|
| 13 |
+
<meta name="application-name" content="Babel">
|
| 14 |
+
<meta name="apple-mobile-web-app-title" content="Babel">
|
| 15 |
+
<link rel="manifest" href="/static/manifest.json">
|
| 16 |
+
|
| 17 |
+
<title>Babel - Traducteur Vocal</title>
|
| 18 |
+
<link href="https://fonts.googleapis.com/css2?family=Nunito:wght@400;600;700;800;900&display=swap" rel="stylesheet">
|
| 19 |
+
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css">
|
| 20 |
+
|
| 21 |
+
<style>
|
| 22 |
+
:root {
|
| 23 |
+
/* 🌿 FRESH GREEN PALETTE (Cloned from reference) */
|
| 24 |
+
--bg-gradient: linear-gradient(180deg, #4ade80 0%, #22c55e 50%, #16a34a 100%);
|
| 25 |
+
--card-bg: #ffffff;
|
| 26 |
+
--card-shadow: 0 8px 32px rgba(0, 0, 0, 0.12);
|
| 27 |
+
--text-dark: #1a1a2e;
|
| 28 |
+
--text-medium: #4a5568;
|
| 29 |
+
--text-light: #718096;
|
| 30 |
+
|
| 31 |
+
/* Action Colors */
|
| 32 |
+
--accent-green: #22c55e;
|
| 33 |
+
--accent-yellow: #fbbf24;
|
| 34 |
+
--accent-purple: #a855f7;
|
| 35 |
+
--accent-teal: #14b8a6;
|
| 36 |
+
|
| 37 |
+
/* Bottom Bar */
|
| 38 |
+
--bar-bg: #1a1a2e;
|
| 39 |
+
--bar-active: #fbbf24;
|
| 40 |
+
}
|
| 41 |
+
|
| 42 |
+
* {
|
| 43 |
+
margin: 0;
|
| 44 |
+
padding: 0;
|
| 45 |
+
box-sizing: border-box;
|
| 46 |
+
-webkit-tap-highlight-color: transparent;
|
| 47 |
+
}
|
| 48 |
+
|
| 49 |
+
body {
|
| 50 |
+
font-family: 'Nunito', -apple-system, BlinkMacSystemFont, sans-serif;
|
| 51 |
+
background: var(--bg-gradient);
|
| 52 |
+
min-height: 100vh;
|
| 53 |
+
min-height: 100dvh;
|
| 54 |
+
color: var(--text-dark);
|
| 55 |
+
overflow-x: hidden;
|
| 56 |
+
padding-bottom: 100px;
|
| 57 |
+
}
|
| 58 |
+
|
| 59 |
+
/* 🔝 TOP HEADER */
|
| 60 |
+
.top-header {
|
| 61 |
+
padding: 16px 20px;
|
| 62 |
+
padding-top: max(16px, env(safe-area-inset-top));
|
| 63 |
+
display: flex;
|
| 64 |
+
justify-content: space-between;
|
| 65 |
+
align-items: center;
|
| 66 |
+
}
|
| 67 |
+
|
| 68 |
+
.greeting {
|
| 69 |
+
color: white;
|
| 70 |
+
font-size: 1.1rem;
|
| 71 |
+
font-weight: 700;
|
| 72 |
+
}
|
| 73 |
+
|
| 74 |
+
.settings-icon {
|
| 75 |
+
width: 44px;
|
| 76 |
+
height: 44px;
|
| 77 |
+
background: rgba(255, 255, 255, 0.2);
|
| 78 |
+
border-radius: 12px;
|
| 79 |
+
display: flex;
|
| 80 |
+
align-items: center;
|
| 81 |
+
justify-content: center;
|
| 82 |
+
color: white;
|
| 83 |
+
font-size: 1.2rem;
|
| 84 |
+
cursor: pointer;
|
| 85 |
+
transition: all 0.2s;
|
| 86 |
+
}
|
| 87 |
+
|
| 88 |
+
.settings-icon:hover {
|
| 89 |
+
background: rgba(255, 255, 255, 0.3);
|
| 90 |
+
transform: scale(1.05);
|
| 91 |
+
}
|
| 92 |
+
|
| 93 |
+
/* 📦 MAIN CARD */
|
| 94 |
+
.main-card {
|
| 95 |
+
background: var(--card-bg);
|
| 96 |
+
margin: 0 16px;
|
| 97 |
+
border-radius: 24px;
|
| 98 |
+
padding: 24px;
|
| 99 |
+
box-shadow: var(--card-shadow);
|
| 100 |
+
}
|
| 101 |
+
|
| 102 |
+
.card-label {
|
| 103 |
+
font-size: 0.85rem;
|
| 104 |
+
color: var(--text-light);
|
| 105 |
+
font-weight: 600;
|
| 106 |
+
margin-bottom: 8px;
|
| 107 |
+
}
|
| 108 |
+
|
| 109 |
+
.main-value {
|
| 110 |
+
font-size: 2.5rem;
|
| 111 |
+
font-weight: 900;
|
| 112 |
+
color: var(--text-dark);
|
| 113 |
+
margin-bottom: 16px;
|
| 114 |
+
}
|
| 115 |
+
|
| 116 |
+
.main-value span {
|
| 117 |
+
font-size: 1.5rem;
|
| 118 |
+
color: var(--text-medium);
|
| 119 |
+
}
|
| 120 |
+
|
| 121 |
+
/* Language Display */
|
| 122 |
+
.language-display {
|
| 123 |
+
background: #f1f5f9;
|
| 124 |
+
border-radius: 12px;
|
| 125 |
+
padding: 12px 16px;
|
| 126 |
+
font-family: 'JetBrains Mono', monospace;
|
| 127 |
+
font-size: 0.9rem;
|
| 128 |
+
color: var(--text-medium);
|
| 129 |
+
margin-bottom: 20px;
|
| 130 |
+
text-align: center;
|
| 131 |
+
overflow: hidden;
|
| 132 |
+
text-overflow: ellipsis;
|
| 133 |
+
white-space: nowrap;
|
| 134 |
+
}
|
| 135 |
+
|
| 136 |
+
/* 🔘 ACTION BUTTONS ROW */
|
| 137 |
+
.action-row {
|
| 138 |
+
display: flex;
|
| 139 |
+
justify-content: center;
|
| 140 |
+
gap: 16px;
|
| 141 |
+
margin-top: 8px;
|
| 142 |
+
}
|
| 143 |
+
|
| 144 |
+
.action-btn {
|
| 145 |
+
display: flex;
|
| 146 |
+
flex-direction: column;
|
| 147 |
+
align-items: center;
|
| 148 |
+
gap: 8px;
|
| 149 |
+
cursor: pointer;
|
| 150 |
+
transition: all 0.2s;
|
| 151 |
+
}
|
| 152 |
+
|
| 153 |
+
.action-btn:hover {
|
| 154 |
+
transform: translateY(-2px);
|
| 155 |
+
}
|
| 156 |
+
|
| 157 |
+
.action-icon {
|
| 158 |
+
width: 56px;
|
| 159 |
+
height: 56px;
|
| 160 |
+
border-radius: 16px;
|
| 161 |
+
display: flex;
|
| 162 |
+
align-items: center;
|
| 163 |
+
justify-content: center;
|
| 164 |
+
font-size: 1.4rem;
|
| 165 |
+
color: white;
|
| 166 |
+
}
|
| 167 |
+
|
| 168 |
+
.action-icon.green {
|
| 169 |
+
background: var(--accent-green);
|
| 170 |
+
}
|
| 171 |
+
|
| 172 |
+
.action-icon.yellow {
|
| 173 |
+
background: var(--accent-yellow);
|
| 174 |
+
color: #1a1a2e;
|
| 175 |
+
}
|
| 176 |
+
|
| 177 |
+
.action-icon.purple {
|
| 178 |
+
background: var(--accent-purple);
|
| 179 |
+
}
|
| 180 |
+
|
| 181 |
+
.action-icon.teal {
|
| 182 |
+
background: var(--accent-teal);
|
| 183 |
+
}
|
| 184 |
+
|
| 185 |
+
.action-label {
|
| 186 |
+
font-size: 0.8rem;
|
| 187 |
+
font-weight: 700;
|
| 188 |
+
color: var(--text-dark);
|
| 189 |
+
}
|
| 190 |
+
|
| 191 |
+
/* 📊 PROGRESS SECTION */
|
| 192 |
+
.progress-card {
|
| 193 |
+
background: var(--bar-bg);
|
| 194 |
+
margin: 16px;
|
| 195 |
+
border-radius: 20px;
|
| 196 |
+
padding: 16px 20px;
|
| 197 |
+
display: flex;
|
| 198 |
+
align-items: center;
|
| 199 |
+
gap: 16px;
|
| 200 |
+
}
|
| 201 |
+
|
| 202 |
+
.progress-icon {
|
| 203 |
+
width: 44px;
|
| 204 |
+
height: 44px;
|
| 205 |
+
background: var(--accent-yellow);
|
| 206 |
+
border-radius: 12px;
|
| 207 |
+
display: flex;
|
| 208 |
+
align-items: center;
|
| 209 |
+
justify-content: center;
|
| 210 |
+
font-size: 1.2rem;
|
| 211 |
+
}
|
| 212 |
+
|
| 213 |
+
.progress-info {
|
| 214 |
+
flex: 1;
|
| 215 |
+
}
|
| 216 |
+
|
| 217 |
+
.progress-title {
|
| 218 |
+
color: white;
|
| 219 |
+
font-weight: 800;
|
| 220 |
+
font-size: 1.1rem;
|
| 221 |
+
}
|
| 222 |
+
|
| 223 |
+
.progress-sub {
|
| 224 |
+
color: rgba(255, 255, 255, 0.6);
|
| 225 |
+
font-size: 0.85rem;
|
| 226 |
+
}
|
| 227 |
+
|
| 228 |
+
/* 📜 HISTORY CARD */
|
| 229 |
+
.history-card {
|
| 230 |
+
background: var(--card-bg);
|
| 231 |
+
margin: 16px;
|
| 232 |
+
border-radius: 24px;
|
| 233 |
+
padding: 20px;
|
| 234 |
+
box-shadow: var(--card-shadow);
|
| 235 |
+
}
|
| 236 |
+
|
| 237 |
+
.history-header {
|
| 238 |
+
display: flex;
|
| 239 |
+
justify-content: space-between;
|
| 240 |
+
align-items: center;
|
| 241 |
+
margin-bottom: 16px;
|
| 242 |
+
}
|
| 243 |
+
|
| 244 |
+
.history-title {
|
| 245 |
+
font-size: 1.1rem;
|
| 246 |
+
font-weight: 800;
|
| 247 |
+
color: var(--text-dark);
|
| 248 |
+
}
|
| 249 |
+
|
| 250 |
+
.show-all {
|
| 251 |
+
font-size: 0.85rem;
|
| 252 |
+
color: var(--text-medium);
|
| 253 |
+
cursor: pointer;
|
| 254 |
+
}
|
| 255 |
+
|
| 256 |
+
.history-item {
|
| 257 |
+
display: flex;
|
| 258 |
+
align-items: center;
|
| 259 |
+
gap: 12px;
|
| 260 |
+
padding: 12px 0;
|
| 261 |
+
border-bottom: 1px solid #f1f5f9;
|
| 262 |
+
}
|
| 263 |
+
|
| 264 |
+
.history-item:last-child {
|
| 265 |
+
border-bottom: none;
|
| 266 |
+
}
|
| 267 |
+
|
| 268 |
+
.history-icon {
|
| 269 |
+
width: 40px;
|
| 270 |
+
height: 40px;
|
| 271 |
+
border-radius: 12px;
|
| 272 |
+
display: flex;
|
| 273 |
+
align-items: center;
|
| 274 |
+
justify-content: center;
|
| 275 |
+
font-size: 1rem;
|
| 276 |
+
}
|
| 277 |
+
|
| 278 |
+
.history-icon.received {
|
| 279 |
+
background: #dcfce7;
|
| 280 |
+
color: var(--accent-green);
|
| 281 |
+
}
|
| 282 |
+
|
| 283 |
+
.history-icon.sent {
|
| 284 |
+
background: #fef3c7;
|
| 285 |
+
color: var(--accent-yellow);
|
| 286 |
+
}
|
| 287 |
+
|
| 288 |
+
.history-details {
|
| 289 |
+
flex: 1;
|
| 290 |
+
}
|
| 291 |
+
|
| 292 |
+
.history-type {
|
| 293 |
+
font-weight: 700;
|
| 294 |
+
font-size: 0.95rem;
|
| 295 |
+
color: var(--text-dark);
|
| 296 |
+
}
|
| 297 |
+
|
| 298 |
+
.history-amount {
|
| 299 |
+
font-size: 0.85rem;
|
| 300 |
+
color: var(--text-medium);
|
| 301 |
+
}
|
| 302 |
+
|
| 303 |
+
.history-time {
|
| 304 |
+
font-size: 0.8rem;
|
| 305 |
+
color: var(--text-light);
|
| 306 |
+
}
|
| 307 |
+
|
| 308 |
+
/* 📱 BOTTOM NAV BAR */
|
| 309 |
+
.bottom-bar {
|
| 310 |
+
position: fixed;
|
| 311 |
+
bottom: 0;
|
| 312 |
+
left: 0;
|
| 313 |
+
right: 0;
|
| 314 |
+
padding: 12px 24px;
|
| 315 |
+
padding-bottom: max(12px, env(safe-area-inset-bottom));
|
| 316 |
+
display: flex;
|
| 317 |
+
justify-content: center;
|
| 318 |
+
}
|
| 319 |
+
|
| 320 |
+
.nav-pill {
|
| 321 |
+
background: var(--bar-bg);
|
| 322 |
+
border-radius: 100px;
|
| 323 |
+
padding: 12px 24px;
|
| 324 |
+
display: flex;
|
| 325 |
+
align-items: center;
|
| 326 |
+
gap: 8px;
|
| 327 |
+
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.3);
|
| 328 |
+
}
|
| 329 |
+
|
| 330 |
+
.nav-item {
|
| 331 |
+
width: 48px;
|
| 332 |
+
height: 48px;
|
| 333 |
+
border-radius: 50%;
|
| 334 |
+
display: flex;
|
| 335 |
+
align-items: center;
|
| 336 |
+
justify-content: center;
|
| 337 |
+
color: rgba(255, 255, 255, 0.5);
|
| 338 |
+
font-size: 1.2rem;
|
| 339 |
+
cursor: pointer;
|
| 340 |
+
transition: all 0.2s;
|
| 341 |
+
}
|
| 342 |
+
|
| 343 |
+
.nav-item:hover {
|
| 344 |
+
color: white;
|
| 345 |
+
}
|
| 346 |
+
|
| 347 |
+
.nav-item.active {
|
| 348 |
+
background: var(--accent-yellow);
|
| 349 |
+
color: var(--bar-bg);
|
| 350 |
+
}
|
| 351 |
+
|
| 352 |
+
/* 🎤 RECORD BUTTON (Special) */
|
| 353 |
+
.record-orb {
|
| 354 |
+
width: 64px;
|
| 355 |
+
height: 64px;
|
| 356 |
+
background: linear-gradient(135deg, #22c55e 0%, #16a34a 100%);
|
| 357 |
+
border-radius: 50%;
|
| 358 |
+
display: flex;
|
| 359 |
+
align-items: center;
|
| 360 |
+
justify-content: center;
|
| 361 |
+
color: white;
|
| 362 |
+
font-size: 1.5rem;
|
| 363 |
+
cursor: pointer;
|
| 364 |
+
transition: all 0.3s cubic-bezier(0.34, 1.56, 0.64, 1);
|
| 365 |
+
margin: -20px 8px 0 8px;
|
| 366 |
+
box-shadow: 0 8px 24px rgba(34, 197, 94, 0.4);
|
| 367 |
+
border: 4px solid var(--bar-bg);
|
| 368 |
+
}
|
| 369 |
+
|
| 370 |
+
.record-orb:hover {
|
| 371 |
+
transform: scale(1.1);
|
| 372 |
+
}
|
| 373 |
+
|
| 374 |
+
.record-orb:active {
|
| 375 |
+
transform: scale(0.95);
|
| 376 |
+
}
|
| 377 |
+
|
| 378 |
+
.record-orb.active {
|
| 379 |
+
background: linear-gradient(135deg, #ef4444 0%, #dc2626 100%);
|
| 380 |
+
box-shadow: 0 8px 24px rgba(239, 68, 68, 0.4);
|
| 381 |
+
animation: pulse-record 1.5s infinite;
|
| 382 |
+
}
|
| 383 |
+
|
| 384 |
+
@keyframes pulse-record {
|
| 385 |
+
|
| 386 |
+
0%,
|
| 387 |
+
100% {
|
| 388 |
+
box-shadow: 0 8px 24px rgba(239, 68, 68, 0.4);
|
| 389 |
+
}
|
| 390 |
+
|
| 391 |
+
50% {
|
| 392 |
+
box-shadow: 0 8px 40px rgba(239, 68, 68, 0.6);
|
| 393 |
+
}
|
| 394 |
+
}
|
| 395 |
+
|
| 396 |
+
/* 📝 CHAT MESSAGES */
|
| 397 |
+
.chat-container {
|
| 398 |
+
padding: 0 16px;
|
| 399 |
+
display: flex;
|
| 400 |
+
flex-direction: column;
|
| 401 |
+
gap: 12px;
|
| 402 |
+
}
|
| 403 |
+
|
| 404 |
+
.chat-message {
|
| 405 |
+
max-width: 85%;
|
| 406 |
+
padding: 16px;
|
| 407 |
+
border-radius: 20px;
|
| 408 |
+
animation: slideIn 0.3s ease;
|
| 409 |
+
}
|
| 410 |
+
|
| 411 |
+
@keyframes slideIn {
|
| 412 |
+
from {
|
| 413 |
+
opacity: 0;
|
| 414 |
+
transform: translateY(10px);
|
| 415 |
+
}
|
| 416 |
+
|
| 417 |
+
to {
|
| 418 |
+
opacity: 1;
|
| 419 |
+
transform: translateY(0);
|
| 420 |
+
}
|
| 421 |
+
}
|
| 422 |
+
|
| 423 |
+
.chat-message.user {
|
| 424 |
+
background: var(--card-bg);
|
| 425 |
+
align-self: flex-start;
|
| 426 |
+
border-bottom-left-radius: 4px;
|
| 427 |
+
box-shadow: var(--card-shadow);
|
| 428 |
+
}
|
| 429 |
+
|
| 430 |
+
.chat-message.bot {
|
| 431 |
+
background: var(--bar-bg);
|
| 432 |
+
color: white;
|
| 433 |
+
align-self: flex-end;
|
| 434 |
+
border-bottom-right-radius: 4px;
|
| 435 |
+
}
|
| 436 |
+
|
| 437 |
+
.message-text {
|
| 438 |
+
font-size: 1rem;
|
| 439 |
+
line-height: 1.5;
|
| 440 |
+
}
|
| 441 |
+
|
| 442 |
+
.message-meta {
|
| 443 |
+
font-size: 0.75rem;
|
| 444 |
+
opacity: 0.6;
|
| 445 |
+
margin-top: 8px;
|
| 446 |
+
}
|
| 447 |
+
|
| 448 |
+
/* 🔘 LANGUAGE SELECTOR */
|
| 449 |
+
.lang-select {
|
| 450 |
+
background: #f1f5f9;
|
| 451 |
+
border: none;
|
| 452 |
+
border-radius: 12px;
|
| 453 |
+
padding: 12px 16px;
|
| 454 |
+
font-family: 'Nunito', sans-serif;
|
| 455 |
+
font-size: 0.9rem;
|
| 456 |
+
font-weight: 600;
|
| 457 |
+
color: var(--text-dark);
|
| 458 |
+
cursor: pointer;
|
| 459 |
+
width: 100%;
|
| 460 |
+
appearance: none;
|
| 461 |
+
}
|
| 462 |
+
|
| 463 |
+
/* Empty State */
|
| 464 |
+
.empty-state {
|
| 465 |
+
text-align: center;
|
| 466 |
+
padding: 40px 20px;
|
| 467 |
+
}
|
| 468 |
+
|
| 469 |
+
.empty-icon {
|
| 470 |
+
font-size: 4rem;
|
| 471 |
+
margin-bottom: 16px;
|
| 472 |
+
}
|
| 473 |
+
|
| 474 |
+
.empty-title {
|
| 475 |
+
font-size: 1.5rem;
|
| 476 |
+
font-weight: 900;
|
| 477 |
+
color: white;
|
| 478 |
+
margin-bottom: 8px;
|
| 479 |
+
}
|
| 480 |
+
|
| 481 |
+
.empty-sub {
|
| 482 |
+
color: rgba(255, 255, 255, 0.8);
|
| 483 |
+
font-size: 1rem;
|
| 484 |
+
}
|
| 485 |
+
|
| 486 |
+
/* MODAL */
|
| 487 |
+
.modal {
|
| 488 |
+
position: fixed;
|
| 489 |
+
inset: 0;
|
| 490 |
+
background: rgba(0, 0, 0, 0.5);
|
| 491 |
+
display: none;
|
| 492 |
+
align-items: flex-end;
|
| 493 |
+
justify-content: center;
|
| 494 |
+
z-index: 2000;
|
| 495 |
+
backdrop-filter: blur(4px);
|
| 496 |
+
}
|
| 497 |
+
|
| 498 |
+
.modal.visible {
|
| 499 |
+
display: flex;
|
| 500 |
+
}
|
| 501 |
+
|
| 502 |
+
.modal-content {
|
| 503 |
+
background: var(--card-bg);
|
| 504 |
+
width: 100%;
|
| 505 |
+
max-width: 500px;
|
| 506 |
+
border-radius: 24px 24px 0 0;
|
| 507 |
+
padding: 24px;
|
| 508 |
+
max-height: 80vh;
|
| 509 |
+
overflow-y: auto;
|
| 510 |
+
animation: slideUp 0.3s ease;
|
| 511 |
+
}
|
| 512 |
+
|
| 513 |
+
@keyframes slideUp {
|
| 514 |
+
from {
|
| 515 |
+
transform: translateY(100%);
|
| 516 |
+
}
|
| 517 |
+
|
| 518 |
+
to {
|
| 519 |
+
transform: translateY(0);
|
| 520 |
+
}
|
| 521 |
+
}
|
| 522 |
+
|
| 523 |
+
.modal-handle {
|
| 524 |
+
width: 40px;
|
| 525 |
+
height: 4px;
|
| 526 |
+
background: #e2e8f0;
|
| 527 |
+
border-radius: 2px;
|
| 528 |
+
margin: 0 auto 20px;
|
| 529 |
+
}
|
| 530 |
+
|
| 531 |
+
.modal-title {
|
| 532 |
+
font-size: 1.3rem;
|
| 533 |
+
font-weight: 800;
|
| 534 |
+
margin-bottom: 20px;
|
| 535 |
+
}
|
| 536 |
+
|
| 537 |
+
.form-group {
|
| 538 |
+
margin-bottom: 16px;
|
| 539 |
+
}
|
| 540 |
+
|
| 541 |
+
.form-group label {
|
| 542 |
+
display: block;
|
| 543 |
+
font-size: 0.85rem;
|
| 544 |
+
font-weight: 700;
|
| 545 |
+
color: var(--text-medium);
|
| 546 |
+
margin-bottom: 8px;
|
| 547 |
+
}
|
| 548 |
+
|
| 549 |
+
.save-btn {
|
| 550 |
+
width: 100%;
|
| 551 |
+
padding: 16px;
|
| 552 |
+
background: var(--accent-green);
|
| 553 |
+
color: white;
|
| 554 |
+
border: none;
|
| 555 |
+
border-radius: 16px;
|
| 556 |
+
font-family: 'Nunito', sans-serif;
|
| 557 |
+
font-size: 1rem;
|
| 558 |
+
font-weight: 800;
|
| 559 |
+
cursor: pointer;
|
| 560 |
+
margin-top: 16px;
|
| 561 |
+
transition: all 0.2s;
|
| 562 |
+
}
|
| 563 |
+
|
| 564 |
+
.save-btn:hover {
|
| 565 |
+
background: #16a34a;
|
| 566 |
+
transform: translateY(-2px);
|
| 567 |
+
}
|
| 568 |
+
</style>
|
| 569 |
+
</head>
|
| 570 |
+
|
| 571 |
+
<body>
|
| 572 |
+
<!-- TOP HEADER -->
|
| 573 |
+
<div class="top-header">
|
| 574 |
+
<div class="greeting">👋 Bonjour!</div>
|
| 575 |
+
<div class="settings-icon" id="settings-trigger">
|
| 576 |
+
<i class="fa-solid fa-gear"></i>
|
| 577 |
+
</div>
|
| 578 |
+
</div>
|
| 579 |
+
|
| 580 |
+
<!-- MAIN CARD -->
|
| 581 |
+
<div class="main-card">
|
| 582 |
+
<div class="card-label">Traduction Instantanée</div>
|
| 583 |
+
<div class="main-value">Babel <span>Matrix</span></div>
|
| 584 |
+
|
| 585 |
+
<div class="language-display" id="current-lang">
|
| 586 |
+
🌍 Appuyez sur le micro pour parler...
|
| 587 |
+
</div>
|
| 588 |
+
|
| 589 |
+
<!-- Language Selector -->
|
| 590 |
+
<select class="lang-select" id="target-lang-quick">
|
| 591 |
+
<option value="French">🇫🇷 Français</option>
|
| 592 |
+
<option value="English">🇬🇧 English</option>
|
| 593 |
+
<option value="Moroccan Darija">🇲🇦 Darija</option>
|
| 594 |
+
<option value="Egyptian Arabic">🇪🇬 Masri</option>
|
| 595 |
+
<option value="Spanish">🇪🇸 Español</option>
|
| 596 |
+
<option value="German">🇩🇪 Deutsch</option>
|
| 597 |
+
<option value="Arabic">🇸🇦 العربية</option>
|
| 598 |
+
</select>
|
| 599 |
+
|
| 600 |
+
<!-- Action Buttons -->
|
| 601 |
+
<div class="action-row">
|
| 602 |
+
<div class="action-btn" id="listen-btn">
|
| 603 |
+
<div class="action-icon green">
|
| 604 |
+
<i class="fa-solid fa-play"></i>
|
| 605 |
+
</div>
|
| 606 |
+
<span class="action-label">Écouter</span>
|
| 607 |
+
</div>
|
| 608 |
+
<div class="action-btn" id="copy-btn">
|
| 609 |
+
<div class="action-icon yellow">
|
| 610 |
+
<i class="fa-solid fa-copy"></i>
|
| 611 |
+
</div>
|
| 612 |
+
<span class="action-label">Copier</span>
|
| 613 |
+
</div>
|
| 614 |
+
<div class="action-btn" id="share-btn">
|
| 615 |
+
<div class="action-icon purple">
|
| 616 |
+
<i class="fa-solid fa-share"></i>
|
| 617 |
+
</div>
|
| 618 |
+
<span class="action-label">Partager</span>
|
| 619 |
+
</div>
|
| 620 |
+
</div>
|
| 621 |
+
</div>
|
| 622 |
+
|
| 623 |
+
<!-- STATUS BAR -->
|
| 624 |
+
<div class="progress-card">
|
| 625 |
+
<div class="progress-icon">🎙️</div>
|
| 626 |
+
<div class="progress-info">
|
| 627 |
+
<div class="progress-title" id="status-text">Prêt</div>
|
| 628 |
+
<div class="progress-sub">Mode continu activé</div>
|
| 629 |
+
</div>
|
| 630 |
+
</div>
|
| 631 |
+
|
| 632 |
+
<!-- CHAT HISTORY -->
|
| 633 |
+
<div class="history-card">
|
| 634 |
+
<div class="history-header">
|
| 635 |
+
<span class="history-title">Historique</span>
|
| 636 |
+
<span class="show-all">Tout voir →</span>
|
| 637 |
+
</div>
|
| 638 |
+
<div id="chat-history">
|
| 639 |
+
<!-- Messages will appear here -->
|
| 640 |
+
<div class="empty-state" id="empty-hint" style="padding: 20px;">
|
| 641 |
+
<div style="font-size: 2rem;">💬</div>
|
| 642 |
+
<p style="color: var(--text-light); font-size: 0.9rem;">Vos traductions apparaîtront ici</p>
|
| 643 |
+
</div>
|
| 644 |
+
</div>
|
| 645 |
+
</div>
|
| 646 |
+
|
| 647 |
+
<!-- BOTTOM NAV BAR -->
|
| 648 |
+
<div class="bottom-bar">
|
| 649 |
+
<div class="nav-pill">
|
| 650 |
+
<div class="nav-item">
|
| 651 |
+
<i class="fa-solid fa-clock-rotate-left"></i>
|
| 652 |
+
</div>
|
| 653 |
+
<div class="nav-item active">
|
| 654 |
+
<i class="fa-solid fa-language"></i>
|
| 655 |
+
</div>
|
| 656 |
+
|
| 657 |
+
<!-- THE RECORD ORB -->
|
| 658 |
+
<div class="record-orb" id="record-btn">
|
| 659 |
+
<i class="fa-solid fa-microphone"></i>
|
| 660 |
+
</div>
|
| 661 |
+
|
| 662 |
+
<div class="nav-item">
|
| 663 |
+
<i class="fa-solid fa-brain"></i>
|
| 664 |
+
</div>
|
| 665 |
+
<div class="nav-item">
|
| 666 |
+
<i class="fa-solid fa-user"></i>
|
| 667 |
+
</div>
|
| 668 |
+
</div>
|
| 669 |
+
</div>
|
| 670 |
+
|
| 671 |
+
<!-- SETTINGS MODAL -->
|
| 672 |
+
<div class="modal" id="settings-modal">
|
| 673 |
+
<div class="modal-content">
|
| 674 |
+
<div class="modal-handle"></div>
|
| 675 |
+
<h2 class="modal-title">⚙️ Paramètres</h2>
|
| 676 |
+
|
| 677 |
+
<div class="form-group">
|
| 678 |
+
<label>Mode d'Intelligence</label>
|
| 679 |
+
<select class="lang-select" id="ai-model-selector">
|
| 680 |
+
<option value="gpt-4o-mini">🟢 OpenAI GPT-4o (Stable)</option>
|
| 681 |
+
<option value="gemini-flash-latest">⚡ Gemini Flash</option>
|
| 682 |
+
</select>
|
| 683 |
+
</div>
|
| 684 |
+
|
| 685 |
+
<div class="form-group">
|
| 686 |
+
<label>Moteur Vocal</label>
|
| 687 |
+
<select class="lang-select" id="tts-selector">
|
| 688 |
+
<option value="openai">🟢 OpenAI TTS</option>
|
| 689 |
+
<option value="gemini">💎 Gemini TTS</option>
|
| 690 |
+
</select>
|
| 691 |
+
</div>
|
| 692 |
+
|
| 693 |
+
<button class="save-btn" id="save-settings">Enregistrer</button>
|
| 694 |
+
</div>
|
| 695 |
+
</div>
|
| 696 |
+
|
| 697 |
+
<!-- SCRIPTS -->
|
| 698 |
+
<script src="/static/script.js"></script>
|
| 699 |
+
<script>
|
| 700 |
+
// Settings Modal Logic
|
| 701 |
+
document.getElementById('settings-trigger').addEventListener('click', () => {
|
| 702 |
+
document.getElementById('settings-modal').classList.add('visible');
|
| 703 |
+
});
|
| 704 |
+
|
| 705 |
+
document.getElementById('settings-modal').addEventListener('click', (e) => {
|
| 706 |
+
if (e.target === document.getElementById('settings-modal')) {
|
| 707 |
+
document.getElementById('settings-modal').classList.remove('visible');
|
| 708 |
+
}
|
| 709 |
+
});
|
| 710 |
+
|
| 711 |
+
document.getElementById('save-settings').addEventListener('click', () => {
|
| 712 |
+
const aiModel = document.getElementById('ai-model-selector').value;
|
| 713 |
+
const ttsEngine = document.getElementById('tts-selector').value;
|
| 714 |
+
localStorage.setItem('aiModel', aiModel);
|
| 715 |
+
localStorage.setItem('ttsEngine', ttsEngine);
|
| 716 |
+
document.getElementById('settings-modal').classList.remove('visible');
|
| 717 |
+
document.getElementById('status-text').innerText = '✅ Sauvegardé!';
|
| 718 |
+
setTimeout(() => document.getElementById('status-text').innerText = 'Prêt', 2000);
|
| 719 |
+
});
|
| 720 |
+
|
| 721 |
+
// Load saved values
|
| 722 |
+
document.getElementById('ai-model-selector').value = localStorage.getItem('aiModel') || 'gpt-4o-mini';
|
| 723 |
+
document.getElementById('tts-selector').value = localStorage.getItem('ttsEngine') || 'openai';
|
| 724 |
+
</script>
|
| 725 |
+
</body>
|
| 726 |
+
|
| 727 |
+
</html>
|