Aller au contenu principal

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 :

CodecChromeFirefoxSafariEdgeRecommande
MP3OuiOuiOuiOuiOui
WAVOuiOuiOuiOuiOui
PCM brutNonNonNonNonNon
mulaw/alawNonNonNonNonNon

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