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:
Ferrovia committed 2026-07-02 22:22:37 +02:00
1 parent ad3fc8938c
commit ad999aa44f
14 files changed
+517 -62

No files matched your search

+110
View File
@@ -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]);
}
+92
View File
@@ -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]);
}