V2.5 : VOICI LES MODIFICATIONS MAJEURS APPRTÉES DANS CETTE VERSION :
-> (BÉTA) : SYSTÈME D'ANNONCES EN GARE VIA UN MODEL IA DE SIMONE HERAULT.
This commit is contained in:
1 parent
ad3fc8938c
commit
ad999aa44f
14 files changed
+517
-62
No files matched your search
@@ -0,0 +1,110 @@
|
||||
import { useEffect, useRef } from 'react';
|
||||
import supabase from '../lib/supabase';
|
||||
|
||||
export default function useAnnonceSonoreRealtime(gareId) {
|
||||
const audioRef = useRef(null);
|
||||
const lastAnnonceIdRef = useRef(null);
|
||||
|
||||
useEffect(() => {
|
||||
if (!supabase || !gareId) return;
|
||||
|
||||
console.debug(`[useAnnonceSonoreRealtime] Setup for gareId: ${gareId}`);
|
||||
|
||||
const playAnnonce = (newRecord) => {
|
||||
// 1. Couper l'audio en cours de lecture
|
||||
if (audioRef.current) {
|
||||
try { audioRef.current.pause(); } catch (e) {}
|
||||
audioRef.current = null;
|
||||
}
|
||||
|
||||
// 2. Couper le TTS en cours de lecture
|
||||
if (typeof window !== 'undefined' && window.speechSynthesis) {
|
||||
try { window.speechSynthesis.cancel(); } catch (e) {}
|
||||
}
|
||||
|
||||
// 3. Diffuser l'annonce
|
||||
if (newRecord.audio_path) {
|
||||
const { data } = supabase.storage.from('banque-annonces').getPublicUrl(newRecord.audio_path);
|
||||
const publicUrl = data?.publicUrl;
|
||||
|
||||
if (publicUrl) {
|
||||
console.debug(`[useAnnonceSonoreRealtime] Lecture audio de : ${publicUrl}`);
|
||||
const audio = new Audio(publicUrl);
|
||||
audioRef.current = audio;
|
||||
audio.play().catch((err) => {
|
||||
console.error('[useAnnonceSonoreRealtime] Erreur lecture audio:', err);
|
||||
});
|
||||
}
|
||||
} else if (newRecord.tts_texte) {
|
||||
if (typeof window !== 'undefined' && window.speechSynthesis) {
|
||||
console.debug(`[useAnnonceSonoreRealtime] Lecture TTS de : "${newRecord.tts_texte}"`);
|
||||
const utterance = new SpeechSynthesisUtterance(newRecord.tts_texte);
|
||||
utterance.lang = 'fr-FR';
|
||||
|
||||
const voices = window.speechSynthesis.getVoices();
|
||||
const frenchVoices = voices.filter(v => v.lang && (v.lang.includes('fr-FR') || v.lang.startsWith('fr')));
|
||||
const claireVoice = frenchVoices.find(v => v.name && v.name.toLowerCase().includes('claire'));
|
||||
|
||||
utterance.voice = claireVoice || frenchVoices[0] || null;
|
||||
window.speechSynthesis.speak(utterance);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// --- FALLBACK POLLING (Seule méthode utilisée) ---
|
||||
// Puisque le WebSocket vers ton instance auto-hébergée (wss://...) est bloqué par le reverse proxy,
|
||||
// on désactive l'abonnement Realtime pour éviter les erreurs "CHANNEL_ERROR" dans ta console.
|
||||
// À la place, on interroge la base de données toutes les 2.5 secondes.
|
||||
const fetchLatest = async () => {
|
||||
try {
|
||||
const { data, error } = await supabase
|
||||
.from('annonces_diffusions')
|
||||
.select('*')
|
||||
.eq('gare_id', gareId)
|
||||
.order('created_at', { ascending: false })
|
||||
.limit(1);
|
||||
|
||||
if (!error && data && data.length > 0) {
|
||||
const latest = data[0];
|
||||
|
||||
// Si c'est une annonce qu'on n'a pas encore vue
|
||||
if (latest.id !== lastAnnonceIdRef.current) {
|
||||
const now = new Date();
|
||||
const createdAt = new Date(latest.created_at);
|
||||
const diffSeconds = (now.getTime() - createdAt.getTime()) / 1000;
|
||||
|
||||
// On ne joue l'annonce que si elle a été insérée il y a moins de 15 secondes
|
||||
if (diffSeconds < 15) {
|
||||
console.debug('[useAnnonceSonoreRealtime] Annonce spontanée détectée :', latest);
|
||||
lastAnnonceIdRef.current = latest.id;
|
||||
playAnnonce(latest);
|
||||
} else {
|
||||
// Si elle est trop vieille, on l'ignore silencieusement
|
||||
lastAnnonceIdRef.current = latest.id;
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (err) {
|
||||
// Ignorer les erreurs réseau silencieusement
|
||||
}
|
||||
};
|
||||
|
||||
// On lance le polling toutes les 2.5 secondes pour une réactivité maximale
|
||||
const pollInterval = setInterval(fetchLatest, 2500);
|
||||
// On fait aussi un fetch immédiat au démarrage pour initialiser l'ID
|
||||
fetchLatest();
|
||||
|
||||
return () => {
|
||||
console.debug('[useAnnonceSonoreRealtime] Nettoyage...');
|
||||
clearInterval(pollInterval);
|
||||
|
||||
if (audioRef.current) {
|
||||
try { audioRef.current.pause(); } catch (e) {}
|
||||
audioRef.current = null;
|
||||
}
|
||||
if (typeof window !== 'undefined' && window.speechSynthesis) {
|
||||
try { window.speechSynthesis.cancel(); } catch (e) {}
|
||||
}
|
||||
};
|
||||
}, [gareId]);
|
||||
}
|
||||
@@ -0,0 +1,92 @@
|
||||
import { useEffect, useRef } from 'react';
|
||||
|
||||
// Hook pour générer automatiquement les annonces sur l'afficheur (fallback TTS robuste)
|
||||
export default function useAutoAnnonce(horaires, currentTime, isArrival = false) {
|
||||
const announcedRefs = useRef(new Set());
|
||||
const isPlayingRef = useRef(false);
|
||||
|
||||
useEffect(() => {
|
||||
if (!horaires || horaires.length === 0 || !currentTime) return;
|
||||
|
||||
// Déterminer le délai d'annonce (2 min avant l'arrivée ou le départ)
|
||||
const TRIGGER_MINUTES = 2;
|
||||
|
||||
const checkAndPlay = () => {
|
||||
if (isPlayingRef.current) return; // Ne pas interrompre si une annonce joue déjà
|
||||
|
||||
for (const train of horaires) {
|
||||
const numTrain = train.num_train || train.numero_train || train.numero;
|
||||
if (!numTrain) continue;
|
||||
|
||||
const timeStr = isArrival
|
||||
? (train.heure_arrivee || train.arrival_time || train.time || train.heure)
|
||||
: (train.heure_depart || train.departure_time || train.time || train.heure);
|
||||
|
||||
if (!timeStr) continue;
|
||||
|
||||
// Si timeStr est juste "HH:MM", on crée une date du jour avec cette heure
|
||||
let trainTime;
|
||||
if (typeof timeStr === 'string' && timeStr.length <= 5 && timeStr.includes(':')) {
|
||||
const [hh, mm] = timeStr.split(':');
|
||||
trainTime = new Date(currentTime);
|
||||
trainTime.setHours(parseInt(hh, 10), parseInt(mm, 10), 0, 0);
|
||||
} else {
|
||||
trainTime = new Date(timeStr);
|
||||
}
|
||||
|
||||
if (isNaN(trainTime.getTime())) continue;
|
||||
|
||||
const diffMs = trainTime.getTime() - currentTime.getTime();
|
||||
const diffMins = Math.floor(diffMs / 60000);
|
||||
|
||||
// Si on est dans la minute cible et que l'annonce n'a pas encore été jouée
|
||||
if (diffMins === TRIGGER_MINUTES && !announcedRefs.current.has(numTrain)) {
|
||||
announcedRefs.current.add(numTrain);
|
||||
|
||||
const gareObj = isArrival
|
||||
? (train.gare_depart_nom || train.gare_depart || train.origine)
|
||||
: (train.gare_arrivee_nom || train.gare_arrivee || train.destination);
|
||||
|
||||
const gareName = typeof gareObj === 'object' && gareObj !== null ? gareObj.nom : gareObj;
|
||||
|
||||
let voieText = train.voie ? `Voie ${train.voie}.` : "";
|
||||
const typeTrain = train.type_train || "train";
|
||||
const gareText = gareName || "destination inconnue";
|
||||
|
||||
const text = isArrival
|
||||
? `${voieText} Le ${typeTrain} numéro ${numTrain} en provenance de ${gareText}, va entrer en gare. Éloignez-vous de la bordure du quai.`
|
||||
: `${voieText} Le ${typeTrain} numéro ${numTrain} à destination de ${gareText}, va partir. Prenez garde à la fermeture des portes.`;
|
||||
|
||||
// Lancement de l'annonce TTS
|
||||
isPlayingRef.current = true;
|
||||
|
||||
if (typeof window !== 'undefined' && window.speechSynthesis) {
|
||||
const utterance = new SpeechSynthesisUtterance(text);
|
||||
utterance.lang = 'fr-FR';
|
||||
const voices = window.speechSynthesis.getVoices();
|
||||
const frenchVoices = voices.filter(v => v.lang && (v.lang.includes('fr-FR') || v.lang.startsWith('fr')));
|
||||
const claireVoice = frenchVoices.find(v => v.name && v.name.toLowerCase().includes('claire'));
|
||||
utterance.voice = claireVoice || frenchVoices[0] || null;
|
||||
|
||||
utterance.onend = () => {
|
||||
isPlayingRef.current = false;
|
||||
};
|
||||
utterance.onerror = () => {
|
||||
isPlayingRef.current = false;
|
||||
};
|
||||
|
||||
console.debug(`[useAutoAnnonce] Lecture TTS Automatique: "${text}"`);
|
||||
window.speechSynthesis.speak(utterance);
|
||||
} else {
|
||||
isPlayingRef.current = false;
|
||||
}
|
||||
|
||||
// On s'arrête après avoir lancé une annonce pour éviter de tout superposer
|
||||
break;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
checkAndPlay();
|
||||
}, [horaires, currentTime, isArrival]);
|
||||
}
|
||||
Reference in new issue
Block a user