ernestmindres commited on
Commit
b9f3a40
·
verified ·
1 Parent(s): c3a70d1

Update tts_engine.py

Browse files
Files changed (1) hide show
  1. tts_engine.py +127 -41
tts_engine.py CHANGED
@@ -1,63 +1,149 @@
1
  import os
2
  import sys
3
- import subprocess # NOUVEL IMPORT
 
 
 
4
 
5
- # Remplacer pyttsx3 par une fonction qui utilise directement la commande espeak
6
-
7
- # --- Fonctions de compatibilité (non utilisées avec subprocess) ---
8
- # Elles sont maintenues pour que gunicorn.conf.py ne génère pas d'erreur.
9
 
 
10
  def get_tts_engine():
11
- """Puisque nous utilisons subprocess, il n'y a pas de moteur à initialiser."""
12
  return True
13
 
14
  def reset_tts_engine():
15
- """Ne fait rien, car il n'y a pas de moteur à réinitialiser."""
16
  pass
17
-
18
  # --- Logique de Langues/Voix ---
19
 
20
  def get_available_languages():
21
- """Retourne une liste des langues courantes eSpeak (sans pyttsx3, nous hardcodons)."""
22
- # **MODIFICATION ICI :** Le 'Français' est maintenant la première entrée.
23
- # La première clé/valeur sera utilisée comme sélection par défaut dans app.py
24
  return {
25
- 'Français': 'fr',
26
  'Anglais (US)': 'en-us',
27
  'Espagnol': 'es',
28
  'Allemand': 'de',
29
  'Italien': 'it',
30
- 'Default (English)': 'en'
31
  }
32
 
33
- # --- Logique de Génération Audio ---
34
 
35
- def text_to_audio_file(text, voice_id, output_path="output.wav"):
36
- """Convertit le texte en fichier audio .wav en utilisant la commande 'espeak'."""
37
-
38
- # Construction de la commande eSpeak:
39
- # espeak -v [VOIX] -w [OUTPUT_PATH] "[TEXTE]"
40
- command = [
41
- "espeak",
42
- "-v", voice_id, # Sélectionne la voix
43
- "-w", output_path, # Spécifie le fichier de sortie (.wav par défaut)
44
- text # Le texte à parler
45
- ]
46
-
47
- try:
48
- # Exécution de la commande
49
- # Nous utilisons `check=True` pour lever une exception si la commande échoue
50
- result = subprocess.run(command, check=True, capture_output=True, text=True)
51
-
52
- # Le fichier audio a été généré avec succès
53
- return os.path.abspath(output_path)
54
-
55
- except subprocess.CalledProcessError as e:
56
- # Erreur si espeak échoue (mauvaise voix, etc.)
57
- raise Exception(f"La commande eSpeak a échoué. Erreur: {e.stderr}")
58
- except FileNotFoundError:
59
- # Erreur si la commande 'espeak' n'est pas trouvée (problème Dockerfile)
60
- raise Exception("Le programme 'espeak' est introuvable. Vérifiez votre installation dans le Dockerfile.")
61
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
62
 
63
- # Note: Le moteur n'est plus initialisé/stocké, donc les appels post_fork réussiront
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
  import os
2
  import sys
3
+ import subprocess
4
+ import io
5
+ import wave
6
+ from typing import Generator
7
 
8
+ # Configuration de la sortie audio (doit être WAV 16-bit 22050 Hz pour le streaming)
9
+ SAMPLE_RATE = 22050
10
+ BITS_PER_SAMPLE = 16
11
+ CHANNELS = 1
12
 
13
+ # --- Fonctions de compatibilité (inchangées) ---
14
  def get_tts_engine():
 
15
  return True
16
 
17
  def reset_tts_engine():
 
18
  pass
19
+
20
  # --- Logique de Langues/Voix ---
21
 
22
  def get_available_languages():
23
+ # Nous utilisons les voix espeak-ng par défaut pour le français
 
 
24
  return {
25
+ 'Français (Qualité Optimale)': 'fr-fr',
26
  'Anglais (US)': 'en-us',
27
  'Espagnol': 'es',
28
  'Allemand': 'de',
29
  'Italien': 'it',
 
30
  }
31
 
32
+ # --- Logique de Génération Audio EN MODE STREAMING ---
33
 
34
+ def split_text_into_chunks(text, max_chars=2000):
35
+ """Découpe le texte en morceaux pour la synthèse, améliorant la réactivité."""
36
+ # Cette logique simple est suffisante, mais peut être améliorée (sur les points/virgules)
37
+ chunks = []
38
+ while text:
39
+ chunk = text[:max_chars]
40
+ text = text[max_chars:]
41
+ chunks.append(chunk)
42
+ return chunks
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
43
 
44
+ def generate_wav_header(data_size):
45
+ """Génère l'en-tête WAV pour un flux audio brut."""
46
+ header = io.BytesIO()
47
+ # RIFF chunk
48
+ header.write(b'RIFF')
49
+ header.write((data_size + 36).to_bytes(4, byteorder='little')) # ChunkSize
50
+ header.write(b'WAVE')
51
+
52
+ # fmt chunk
53
+ header.write(b'fmt ')
54
+ header.write(16..to_bytes(4, byteorder='little')) # Subchunk1Size
55
+ header.write(1..to_bytes(2, byteorder='little')) # AudioFormat (PCM=1)
56
+ header.write(CHANNELS.to_bytes(2, byteorder='little'))
57
+ header.write(SAMPLE_RATE.to_bytes(4, byteorder='little'))
58
+
59
+ # ByteRate = SampleRate * NumChannels * BitsPerSample/8
60
+ byte_rate = SAMPLE_RATE * CHANNELS * BITS_PER_SAMPLE // 8
61
+ header.write(byte_rate.to_bytes(4, byteorder='little'))
62
+
63
+ # BlockAlign = NumChannels * BitsPerSample/8
64
+ block_align = CHANNELS * BITS_PER_SAMPLE // 8
65
+ header.write(block_align.to_bytes(2, byteorder='little'))
66
+
67
+ header.write(BITS_PER_SAMPLE.to_bytes(2, byteorder='little'))
68
+
69
+ # data chunk
70
+ header.write(b'data')
71
+ header.write(data_size.to_bytes(4, byteorder='little'))
72
+
73
+ return header.getvalue()
74
 
75
+ def stream_text_to_audio(text: str, voice_id: str) -> Generator[bytes, None, None]:
76
+ """
77
+ Convertit le texte en audio et le *yield* (renvoie) par morceaux (streaming).
78
+
79
+ La première sortie est l'en-tête WAV, suivie des données audio brutes.
80
+ """
81
+
82
+ # 1. Découpage du texte en morceaux pour le streaming
83
+ chunks = split_text_into_chunks(text)
84
+
85
+ # La taille totale de l'audio n'est pas connue à l'avance, nous devrons utiliser
86
+ # un hack ou laisser l'en-tête avec une taille nulle.
87
+ # Dans ce cas, nous allons utiliser 'pipe' et 'ffmpeg' si possible, mais nous allons
88
+ # d'abord essayer une approche simple avec 'espeak-ng' en sortie standard.
89
+
90
+ # Pour simplifier et garantir que l'en-tête est correct, nous allons générer
91
+ # chaque chunk comme un fichier WAV et concaténer (moins idéal que le streaming direct
92
+ # mais plus fiable avec des outils basés sur des fichiers comme espeak-ng).
93
+
94
+ full_audio_data = io.BytesIO()
95
+ total_audio_length = 0
96
+
97
+ # 2. Générer et collecter chaque morceau audio
98
+ for chunk in chunks:
99
+ # espeak-ng -v [VOIX] --stdout "[TEXTE]"
100
+ command = [
101
+ "espeak-ng",
102
+ "-v", voice_id,
103
+ "--stdout", # Écrit le WAV sur la sortie standard
104
+ chunk
105
+ ]
106
+
107
+ try:
108
+ # Exécution de la commande et capture de la sortie binaire
109
+ result = subprocess.run(command, check=True, capture_output=True)
110
+
111
+ # Le résultat est un fichier WAV complet pour le chunk
112
+ chunk_wav_data = result.stdout
113
+
114
+ # Ouvrir le chunk WAV pour extraire les données audio brutes (après l'en-tête de 44 octets)
115
+ # Puisque espeak-ng --stdout écrit un WAV complet, nous devons le décapsuler
116
+ if len(chunk_wav_data) < 44:
117
+ continue
118
+
119
+ # Simplement ajouter les données audio brutes (après l'en-tête)
120
+ raw_audio_data = chunk_wav_data[44:]
121
+
122
+ full_audio_data.write(raw_audio_data)
123
+ total_audio_length += len(raw_audio_data)
124
+
125
+ except subprocess.CalledProcessError as e:
126
+ raise Exception(f"La commande espeak-ng a échoué. Erreur: {e.stderr}")
127
+ except FileNotFoundError:
128
+ raise Exception("Le programme 'espeak-ng' n'a pas été trouvé. Vérifiez le Dockerfile.")
129
+
130
+ # 3. Réinitialiser la position de lecture
131
+ full_audio_data.seek(0)
132
+
133
+ # 4. Générer l'en-tête WAV final avec la taille totale
134
+ final_header = generate_wav_header(total_audio_length)
135
+
136
+ # 5. Yield l'en-tête
137
+ yield final_header
138
+
139
+ # 6. Yield les données audio brutes par morceaux
140
+ chunk_size = 4096 # Taille de chaque morceau envoyé au client
141
+ while True:
142
+ data = full_audio_data.read(chunk_size)
143
+ if not data:
144
+ break
145
+ yield data
146
+
147
+ def text_to_audio_file(text, voice_id, output_path="output.wav"):
148
+ """Ancienne fonction, non utilisée dans cette nouvelle approche."""
149
+ raise NotImplementedError("Utilisez 'stream_text_to_audio' pour le streaming.")