Spaces:
Running
Voici une version améliorée de ton code avec plus de fonctionnalités et un meilleur design :
Browse fileshtml
<!DOCTYPE html>
<html lang="fr">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Reconnaissance Vocale Française - Version Avancée</title>
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
min-height: 100vh;
display: flex;
justify-content: center;
align-items: center;
padding: 20px;
}
.container {
background: rgba(255, 255, 255, 0.95);
backdrop-filter: blur(10px);
border-radius: 24px;
padding: 40px;
box-shadow: 0 20px 60px rgba(0, 0, 0, 0.3);
max-width: 800px;
width: 100%;
}
.header {
text-align: center;
margin-bottom: 40px;
}
.header h1 {
color: #2d3436;
font-size: 2.8rem;
margin-bottom: 10px;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
}
.header p {
color: #636e72;
font-size: 1.2rem;
}
.micro-controls {
display: flex;
flex-direction: column;
align-items: center;
gap: 30px;
margin-bottom: 40px;
}
.micro-btn {
width: 120px;
height: 120px;
border-radius: 50%;
border: none;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
font-size: 3.5rem;
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
transition: all 0.3s ease;
box-shadow: 0 10px 30px rgba(102, 126, 234, 0.4);
}
.micro-btn:hover {
transform: scale(1.05);
box-shadow: 0 15px 40px rgba(102, 126, 234, 0.6);
}
.micro-btn.listening {
animation: pulse 1.5s infinite;
background: linear-gradient(135deg, #ff6b6b 0%, #ee5a24 100%);
box-shadow: 0 10px 30px rgba(255, 107, 107, 0.4);
}
@keyframes pulse {
0% { transform: scale(1); }
50% { transform: scale(1.1); }
100% { transform: scale(1); }
}
.status-indicator {
display: flex;
align-items: center;
gap: 15px;
font-size: 1.2rem;
color: #636e72;
}
.status-dot {
width: 12px;
height: 12px;
border-radius: 50%;
background: #4cd137;
}
.status-dot.inactive {
background: #e84118;
}
.status-dot.listening {
background: #fbc531;
animation: blink 1s infinite;
}
@keyframes blink {
0%, 100% { opacity: 1; }
50% { opacity: 0.3; }
}
.controls {
display: flex;
justify-content: center;
gap: 20px;
margin-bottom: 40px;
}
.control-btn {
padding: 12px 28px;
border: none;
border-radius: 12px;
background: #f5f6fa;
color: #2d3436;
font-size: 1rem;
cursor: pointer;
display: flex;
align-items: center;
gap: 10px;
transition: all 0.3s ease;
}
.control-btn:hover {
background: #dfe6e9;
transform: translateY(-2px);
}
.control-btn i {
font-size: 1.2rem;
}
.result-container {
background: #f8f9fa;
border-radius: 16px;
padding: 30px;
margin-bottom: 30px;
border: 2px solid #e9ecef;
min-height: 200px;
}
.result-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 20px;
}
.result-header h2 {
color: #2d3436;
font-size: 1.5rem;
}
.result-content {
font-size: 1.3rem;
line-height: 1.6;
color: #2d3436;
min-height: 100px;
white-space: pre-wrap;
word-wrap: break-word;
padding: 15px;
background: white;
border-radius: 10px;
border: 1px solid #dfe6e9;
}
.stats {
display: flex;
justify-content: space-between;
background: #f8f9fa;
padding: 20px;
border-radius: 16px;
margin-top: 20px;
}
.stat-item {
text-align: center;
}
.stat-value {
font-size: 2rem;
font-weight: bold;
color: #667eea;
display: block;
}
.stat-label {
color: #636e72;
font-size: 0.9rem;
}
.notification {
position: fixed;
bottom: 20px;
right: 20px;
background: #00b894;
color: white;
padding: 15px 25px;
border-radius: 10px;
box-shadow: 0 5px 15px rgba(0, 0, 0, 0.2);
display: none;
z-index: 1000;
}
@media (max-width: 768px) {
.container {
padding: 20px;
}
.header h1 {
font-size: 2rem;
}
.controls {
flex-direction: column;
align-items: center;
}
.micro-btn {
width: 100px;
height: 100px;
font-size: 2.8rem;
}
.stats {
flex-direction: column;
gap: 15px;
}
}
</style>
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css">
</head>
<body>
<div class="container">
<div class="header">
<h1><i class="fas fa-microphone-alt"></i> Reconnaissance Vocale Française</h1>
<p>Parlez, le système transcrit automatiquement en texte</p>
</div>
<div class="micro-controls">
<button id="micro-btn" class="micro-btn" aria-label="Activer/désactiver le microphone">
<i class="fas fa-microphone"></i>
</button>
<div class="status-indicator">
<div id="status-dot" class="status-dot inactive"></div>
<span id="status-text">Microphone désactivé</span>
</div>
</div>
<div class="controls">
<button id="clear-btn" class="control-btn">
<i class="fas fa-trash-alt"></i> Effacer tout
</button>
<button id="copy-btn" class="control-btn">
<i class="fas fa-copy"></i> Copier le texte
</button>
<button id="save-btn" class="control-btn">
<i class="fas fa-save"></i> Sauvegarder
</button>
</div>
<div class="result-container">
<div class="result-header">
<h2><i class="fas fa-comment-alt"></i> Texte transcrit</h2>
<div class="language-selector">
<select id="language-select" style="padding: 8px; border-radius: 8px; border: 1px solid #ddd;">
<option value="fr-FR">Français (France)</option>
<option value="fr-CA">Français (Canada)</option>
<option value="fr-BE">Français (Belgique)</option>
<option value="fr-CH">Français (Suisse)</option>
<option value="en-US">Anglais (USA)</option>
<option value="es-ES">Espagnol (Espagne)</option>
</select>
</div>
</div>
<div id="result" class="result-content" aria-live="polite" aria-atomic="true">
Votre texte apparaîtra ici...
</div>
</div>
<div class="stats">
<div class="stat-item">
<span id="word-count" class="stat-value">0</span>
<span class="stat-label">Mots</span>
</div>
<div class="stat-item">
<span id="char-count" class="stat-value">0</span>
<span class="stat-label">Caractères</span>
</div>
<div class="stat-item">
<span id="line-count" class="stat-value">0</span>
<span class="stat-label">Lignes</span>
</div>
<div class="stat-item">
<span id="session-count" class="stat-value">0</span>
<span class="stat-label">Sessions</span>
</div>
</div>
</div>
<div id="notification" class="notification">
<i class="fas fa-check-circle"></i> Texte copié dans le presse-papier !
</div>
<script>
// Éléments DOM
const btn = document.getElementById('micro-btn');
const resultDiv = document.getElementById('result');
const statusDot = document.getElementById('status-dot');
const statusText = document.getElementById('status-text');
const clearBtn = document.getElementById('clear-btn');
const c
- index.html +2 -2
- script.js +182 -2
- styles.css +14 -0
|
@@ -94,10 +94,10 @@
|
|
| 94 |
<i class="fas fa-plug"></i>
|
| 95 |
</button>
|
| 96 |
<input type="text" id="chatInput" placeholder="Écrire à Rosalinda..." />
|
| 97 |
-
<button id="micBtn" title="
|
| 98 |
<i class="fas fa-microphone"></i>
|
| 99 |
</button>
|
| 100 |
-
|
| 101 |
<i class="fas fa-paper-plane"></i>
|
| 102 |
</button>
|
| 103 |
</div>
|
|
|
|
| 94 |
<i class="fas fa-plug"></i>
|
| 95 |
</button>
|
| 96 |
<input type="text" id="chatInput" placeholder="Écrire à Rosalinda..." />
|
| 97 |
+
<button id="micBtn" title="Activer/Désactiver le micro (Ctrl+Espace)">
|
| 98 |
<i class="fas fa-microphone"></i>
|
| 99 |
</button>
|
| 100 |
+
<button id="sendBtn" title="Envoyer">
|
| 101 |
<i class="fas fa-paper-plane"></i>
|
| 102 |
</button>
|
| 103 |
</div>
|
|
@@ -193,8 +193,188 @@ sendBtn.addEventListener('click', sendMessage);
|
|
| 193 |
chatInput.addEventListener('keydown', (e) => {
|
| 194 |
if (e.key === 'Enter') sendMessage();
|
| 195 |
});
|
| 196 |
-
micBtn.
|
| 197 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 198 |
});
|
| 199 |
connectBtn.addEventListener('click', () => {
|
| 200 |
alert('Fonction "Connexion" à implémenter (OAuth, clés API, etc.).');
|
|
|
|
| 193 |
chatInput.addEventListener('keydown', (e) => {
|
| 194 |
if (e.key === 'Enter') sendMessage();
|
| 195 |
});
|
| 196 |
+
const micBtn = document.getElementById('micBtn');
|
| 197 |
+
const languageSelect = document.createElement('select');
|
| 198 |
+
languageSelect.id = 'languageSelect';
|
| 199 |
+
languageSelect.style.marginLeft = '8px';
|
| 200 |
+
languageSelect.style.padding = '6px 8px';
|
| 201 |
+
languageSelect.style.borderRadius = '6px';
|
| 202 |
+
languageSelect.style.border = '1px solid rgb(226 232 240)';
|
| 203 |
+
languageSelect.style.background = 'rgb(248 250 252)';
|
| 204 |
+
languageSelect.style.color = 'inherit';
|
| 205 |
+
languageSelect.innerHTML = `
|
| 206 |
+
<option value="fr-FR">Français (France)</option>
|
| 207 |
+
<option value="fr-CA">Français (Canada)</option>
|
| 208 |
+
<option value="fr-BE">Français (Belgique)</option>
|
| 209 |
+
<option value="fr-CH">Français (Suisse)</option>
|
| 210 |
+
<option value="en-US">English (US)</option>
|
| 211 |
+
<option value="es-ES">Español (España)</option>
|
| 212 |
+
`;
|
| 213 |
+
const chatInputParent = chatInput.parentElement;
|
| 214 |
+
if (chatInputParent && !document.getElementById('languageSelect')) {
|
| 215 |
+
chatInputParent.insertBefore(languageSelect, chatInput);
|
| 216 |
+
}
|
| 217 |
+
|
| 218 |
+
let isListening = false;
|
| 219 |
+
let recognition = null;
|
| 220 |
+
let interimTranscript = '';
|
| 221 |
+
let lastFinalTranscript = '';
|
| 222 |
+
const SpeechRecognitionAPI = window.SpeechRecognition || window.webkitSpeechRecognition;
|
| 223 |
+
|
| 224 |
+
function initSpeechRecognition() {
|
| 225 |
+
if (!SpeechRecognitionAPI) {
|
| 226 |
+
aiActivityText.textContent = 'Reconnaissance vocale non supportée';
|
| 227 |
+
aiActivityDot.style.background = 'rgb(239,68,68)';
|
| 228 |
+
micBtn.disabled = true;
|
| 229 |
+
micBtn.title = 'Navigateur non compatible';
|
| 230 |
+
micBtn.style.opacity = '0.6';
|
| 231 |
+
micBtn.style.cursor = 'not-allowed';
|
| 232 |
+
return;
|
| 233 |
+
}
|
| 234 |
+
recognition = new SpeechRecognitionAPI();
|
| 235 |
+
recognition.lang = languageSelect.value;
|
| 236 |
+
recognition.continuous = true;
|
| 237 |
+
recognition.interimResults = true;
|
| 238 |
+
recognition.maxAlternatives = 1;
|
| 239 |
+
|
| 240 |
+
recognition.onstart = () => {
|
| 241 |
+
isListening = true;
|
| 242 |
+
micBtn.classList.add('listening');
|
| 243 |
+
micBtn.innerHTML = '<i class="fas fa-microphone-slash"></i>';
|
| 244 |
+
aiActivityText.textContent = 'Écoute en cours...';
|
| 245 |
+
aiActivityDot.style.background = 'rgb(234,179,8)';
|
| 246 |
+
setAIActivity('Écoute en cours...', true);
|
| 247 |
+
};
|
| 248 |
+
|
| 249 |
+
recognition.onend = () => {
|
| 250 |
+
isListening = false;
|
| 251 |
+
micBtn.classList.remove('listening');
|
| 252 |
+
micBtn.innerHTML = '<i class="fas fa-microphone"></i>';
|
| 253 |
+
aiActivityText.textContent = 'IA en veille';
|
| 254 |
+
aiActivityDot.style.background = 'rgb(100,116,139)';
|
| 255 |
+
setAIActivity('IA en veille', false);
|
| 256 |
+
interimTranscript = '';
|
| 257 |
+
};
|
| 258 |
+
|
| 259 |
+
recognition.onerror = (event) => {
|
| 260 |
+
console.error('SpeechRecognition error:', event.error);
|
| 261 |
+
let message = 'Erreur de reconnaissance vocale.';
|
| 262 |
+
switch (event.error) {
|
| 263 |
+
case 'no-speech':
|
| 264 |
+
message = 'Aucune parole détectée. Essayez de parler plus fort.';
|
| 265 |
+
break;
|
| 266 |
+
case 'audio-capture':
|
| 267 |
+
message = 'Aucun microphone détecté.';
|
| 268 |
+
break;
|
| 269 |
+
case 'not-allowed':
|
| 270 |
+
message = 'Accès au microphone refusé.';
|
| 271 |
+
break;
|
| 272 |
+
case 'network':
|
| 273 |
+
message = 'Erreur réseau.';
|
| 274 |
+
break;
|
| 275 |
+
}
|
| 276 |
+
addMessage('ai', message);
|
| 277 |
+
recognition.stop();
|
| 278 |
+
};
|
| 279 |
+
|
| 280 |
+
recognition.onresult = (event) => {
|
| 281 |
+
interimTranscript = '';
|
| 282 |
+
for (let i = event.resultIndex; i < event.results.length; i++) {
|
| 283 |
+
const transcript = event.results[i][0].transcript.trim();
|
| 284 |
+
if (event.results[i].isFinal) {
|
| 285 |
+
lastFinalTranscript = transcript;
|
| 286 |
+
addMessage('user', transcript);
|
| 287 |
+
setAIActivity('Rosalinda réfléchit...', true);
|
| 288 |
+
sendToAI(transcript);
|
| 289 |
+
} else {
|
| 290 |
+
interimTranscript += transcript + ' ';
|
| 291 |
+
}
|
| 292 |
+
}
|
| 293 |
+
if (interimTranscript) {
|
| 294 |
+
codeOutput.textContent = `// En écoute...\n${interimTranscript}`;
|
| 295 |
+
}
|
| 296 |
+
};
|
| 297 |
+
}
|
| 298 |
+
|
| 299 |
+
function sendToAI(text) {
|
| 300 |
+
fetch('https://api.openai.com/v1/chat/completions', {
|
| 301 |
+
method: 'POST',
|
| 302 |
+
headers: {
|
| 303 |
+
'Content-Type': 'application/json',
|
| 304 |
+
'Authorization': `Bearer ${OPENAI_API_KEY}`
|
| 305 |
+
},
|
| 306 |
+
body: JSON.stringify({
|
| 307 |
+
model: 'gpt-3.5-turbo',
|
| 308 |
+
messages: [
|
| 309 |
+
{ role: 'system', content: 'Tu es Rosalinda, une assistante IA utile et concise.' },
|
| 310 |
+
...getChatHistory(),
|
| 311 |
+
{ role: 'user', content: text }
|
| 312 |
+
],
|
| 313 |
+
temperature: 0.7
|
| 314 |
+
})
|
| 315 |
+
})
|
| 316 |
+
.then(response => {
|
| 317 |
+
if (!response.ok) throw new Error(`Erreur API: ${response.status}`);
|
| 318 |
+
return response.json();
|
| 319 |
+
})
|
| 320 |
+
.then(data => {
|
| 321 |
+
const assistantMessage = data.choices?.[0]?.message?.content ?? 'Désolé, aucune réponse.';
|
| 322 |
+
addMessage('ai', assistantMessage);
|
| 323 |
+
codeOutput.textContent = assistantMessage;
|
| 324 |
+
})
|
| 325 |
+
.catch(error => {
|
| 326 |
+
console.error(error);
|
| 327 |
+
addMessage('ai', 'Une erreur est survenue lors de la communication avec l\'IA.');
|
| 328 |
+
})
|
| 329 |
+
.finally(() => {
|
| 330 |
+
setAIActivity('IA en veille', false);
|
| 331 |
+
});
|
| 332 |
+
}
|
| 333 |
+
|
| 334 |
+
async function toggleListening() {
|
| 335 |
+
if (!recognition) initSpeechRecognition();
|
| 336 |
+
if (!recognition) return;
|
| 337 |
+
|
| 338 |
+
try {
|
| 339 |
+
if (isListening) {
|
| 340 |
+
recognition.stop();
|
| 341 |
+
} else {
|
| 342 |
+
const stream = await navigator.mediaDevices.getUserMedia({ audio: true }).catch(() => null);
|
| 343 |
+
if (!stream) {
|
| 344 |
+
addMessage('ai', 'Impossible d\'accéder au microphone. Vérifiez les permissions.');
|
| 345 |
+
return;
|
| 346 |
+
}
|
| 347 |
+
stream.getTracks().forEach(t => t.stop());
|
| 348 |
+
recognition.start();
|
| 349 |
+
}
|
| 350 |
+
} catch (err) {
|
| 351 |
+
console.error(err);
|
| 352 |
+
addMessage('ai', 'Erreur lors du démarrage de la reconnaissance vocale.');
|
| 353 |
+
}
|
| 354 |
+
}
|
| 355 |
+
|
| 356 |
+
function stopListening() {
|
| 357 |
+
if (recognition && isListening) recognition.stop();
|
| 358 |
+
}
|
| 359 |
+
|
| 360 |
+
micBtn.addEventListener('click', toggleListening);
|
| 361 |
+
languageSelect.addEventListener('change', () => {
|
| 362 |
+
if (recognition) recognition.lang = languageSelect.value;
|
| 363 |
+
});
|
| 364 |
+
|
| 365 |
+
document.addEventListener('keydown', (e) => {
|
| 366 |
+
if (e.code === 'Space' && (e.ctrlKey || e.metaKey)) {
|
| 367 |
+
e.preventDefault();
|
| 368 |
+
toggleListening();
|
| 369 |
+
}
|
| 370 |
+
if (e.key === 'Escape' && isListening) {
|
| 371 |
+
stopListening();
|
| 372 |
+
}
|
| 373 |
+
});
|
| 374 |
+
|
| 375 |
+
// Stop listening on tab change to avoid dangling permissions
|
| 376 |
+
document.addEventListener('visibilitychange', () => {
|
| 377 |
+
if (document.hidden) stopListening();
|
| 378 |
});
|
| 379 |
connectBtn.addEventListener('click', () => {
|
| 380 |
alert('Fonction "Connexion" à implémenter (OAuth, clés API, etc.).');
|
|
@@ -181,6 +181,20 @@ body { margin: 0; font-family: Inter, ui-sans-serif, system-ui, -apple-system, S
|
|
| 181 |
.chat-input button:hover { background: rgb(241 245 249); }
|
| 182 |
.dark .chat-input button:hover { background: rgb(30 41 59); }
|
| 183 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 184 |
/* Colonne 3 : Output */
|
| 185 |
.output {
|
| 186 |
width: 35%;
|
|
|
|
| 181 |
.chat-input button:hover { background: rgb(241 245 249); }
|
| 182 |
.dark .chat-input button:hover { background: rgb(30 41 59); }
|
| 183 |
|
| 184 |
+
/* Micro button listening state */
|
| 185 |
+
#micBtn.listening {
|
| 186 |
+
background: rgb(239, 68, 68); /* red-500 */
|
| 187 |
+
border-color: rgb(239, 68, 68);
|
| 188 |
+
animation: micPulse 1.2s infinite;
|
| 189 |
+
}
|
| 190 |
+
@keyframes micPulse {
|
| 191 |
+
0% { transform: scale(1); box-shadow: 0 0 0 0 rgba(239,68,68,0.6); }
|
| 192 |
+
70% { transform: scale(1.03); box-shadow: 0 0 0 10px rgba(239,68,68,0); }
|
| 193 |
+
100% { transform: scale(1); box-shadow: 0 0 0 0 rgba(239,68,68,0); }
|
| 194 |
+
}
|
| 195 |
+
|
| 196 |
+
/* Small status dot improvements (used for activity too) */
|
| 197 |
+
.dot.listening { background: rgb(234,179,8); } /* amber-500 */
|
| 198 |
/* Colonne 3 : Output */
|
| 199 |
.output {
|
| 200 |
width: 35%;
|