Integration navigateur et bonnes pratiques
Deployer le TTS dans une application web
L’integration du TTS dans un navigateur presente des defis specifiques : compatibilite des codecs, gestion de la memoire, particularites Safari, et performance. Cette derniere lecon vous donne les cles pour une integration robuste en production.
Compatibilite des codecs navigateur
Tous les navigateurs ne supportent pas tous les formats audio. Choisissez le bon codec pour votre audience :
| Codec | Chrome | Firefox | Safari | Edge | Recommande |
|---|---|---|---|---|---|
| MP3 | Oui | Oui | Oui | Oui | Oui |
| WAV | Oui | Oui | Oui | Oui | Oui |
| PCM brut | Non | Non | Non | Non | Non |
| mulaw/alaw | Non | Non | Non | Non | Non |
Regle simple : utilisez MP3 ou WAV pour le navigateur. Les formats PCM, mulaw et alaw sont destines aux applications serveur et a la telephonie, pas a la lecture web.
// Demander du MP3 a l'API (via votre backend)
const response = await fetch("/api/tts", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
text: "Bonjour depuis le navigateur.",
voice_id: "eve",
language: "fr"
// Pas besoin de specifier output_format : MP3 est le defaut
})
});
Le probleme AudioContext sur Safari
Safari impose une restriction de securite : un AudioContext ne peut etre cree que dans un gestionnaire d’evenement utilisateur (clic, touche, tap). Si vous essayez de creer un AudioContext au chargement de la page, Safari le bloquera silencieusement.
Mauvaise approche
// Ne fonctionne PAS sur Safari
const audioContext = new AudioContext(); // Bloque !
async function playTTS() {
const audio = await fetchTTS("Bonjour");
// audioContext est suspendu sur Safari...
}
Bonne approche
let audioContext = null;
// Creer l'AudioContext lors du premier clic utilisateur
document.getElementById("play-btn").addEventListener("click", async () => {
if (!audioContext) {
audioContext = new AudioContext();
}
// Reprendre si suspendu
if (audioContext.state === "suspended") {
await audioContext.resume();
}
const response = await fetch("/api/tts", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
text: "Bonjour depuis Safari.",
voice_id: "eve",
language: "fr"
})
});
const arrayBuffer = await response.arrayBuffer();
const audioBuffer = await audioContext.decodeAudioData(arrayBuffer);
const source = audioContext.createBufferSource();
source.buffer = audioBuffer;
source.connect(audioContext.destination);
source.start(0);
});
Cette approche fonctionne sur tous les navigateurs, pas seulement Safari. C’est une bonne pratique universelle.
Gestion des Blob URLs et fuites memoire
Quand vous creez des Blob URLs pour lire de l’audio, chaque URL consomme de la memoire jusqu’a ce qu’elle soit explicitement liberee. Sans nettoyage, votre application accumulera des fuites memoire.
Le probleme
// Chaque appel cree un Blob URL qui n'est jamais libere
async function playTTS(text) {
const response = await fetch("/api/tts", { ... });
const blob = await response.blob();
const url = URL.createObjectURL(blob); // Fuite memoire !
const audio = new Audio(url);
audio.play();
}
La solution
async function playTTS(text) {
const response = await fetch("/api/tts", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ text, voice_id: "eve", language: "fr" })
});
const blob = await response.blob();
const url = URL.createObjectURL(blob);
const audio = new Audio(url);
// Liberer la memoire quand la lecture est terminee
audio.addEventListener("ended", () => {
URL.revokeObjectURL(url);
});
// Aussi liberer en cas d'erreur
audio.addEventListener("error", () => {
URL.revokeObjectURL(url);
});
audio.play();
}
Pour les applications qui generent beaucoup d’audio, maintenez une liste des URLs actives et nettoyez-les periodiquement :
class AudioManager {
constructor() {
this.activeUrls = new Set();
}
async play(text) {
const response = await fetch("/api/tts", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ text, voice_id: "eve", language: "fr" })
});
const blob = await response.blob();
const url = URL.createObjectURL(blob);
this.activeUrls.add(url);
const audio = new Audio(url);
audio.addEventListener("ended", () => this.release(url));
audio.addEventListener("error", () => this.release(url));
audio.play();
return audio;
}
release(url) {
URL.revokeObjectURL(url);
this.activeUrls.delete(url);
}
cleanup() {
// Liberer toutes les URLs actives
for (const url of this.activeUrls) {
URL.revokeObjectURL(url);
}
this.activeUrls.clear();
}
}
Bonnes pratiques de production
Rate limiting cote backend
Protegez votre backend contre les abus en limitant les requetes par utilisateur :
const rateLimit = require("express-rate-limit");
const ttsLimiter = rateLimit({
windowMs: 60 * 1000, // 1 minute
max: 10, // 10 requetes par minute par IP
message: { error: "Trop de requetes TTS" }
});
app.post("/api/tts", ttsLimiter, async (req, res) => {
// ... proxy vers l'API xAI
});
Validation du texte
Validez et assainissez le texte avant de l’envoyer a l’API :
function validateTTSInput(text) {
if (!text || typeof text !== "string") {
throw new Error("Texte requis");
}
if (text.length > 15000) {
throw new Error("Texte trop long (max 15 000 caracteres)");
}
// Supprimer les caracteres de controle
return text.replace(/[\x00-\x08\x0B\x0C\x0E-\x1F]/g, "");
}
Cache audio
Pour les textes repetes (messages d’accueil, instructions standard), cachez l’audio genere :
const audioCache = new Map();
async function getTTS(text, voice, language) {
const cacheKey = `${text}:${voice}:${language}`;
if (audioCache.has(cacheKey)) {
return audioCache.get(cacheKey);
}
const audio = await fetchTTSFromAPI(text, voice, language);
audioCache.set(cacheKey, audio);
return audio;
}
Pre-generation
Pour les contenus connus a l’avance (e-learning, IVR), pre-generez les fichiers audio au moment du build plutot qu’a la volee.
Points cles a retenir
- Utilisez MP3 ou WAV pour le navigateur, jamais PCM/mulaw/alaw
- Sur Safari, creez l’AudioContext dans un gestionnaire d’evenement utilisateur
- Liberez toujours les Blob URLs avec
URL.revokeObjectURL()apres usage - Implementez un rate limiting sur votre proxy backend
- Validez et assainissez le texte avant l’envoi a l’API
- Cachez l’audio des textes repetes pour economiser des appels API
- Pre-generez l’audio des contenus connus a l’avance