ernestmindres commited on
Commit
17ddbdc
·
verified ·
1 Parent(s): 8469580

Upload 5 files

Browse files
Files changed (5) hide show
  1. Dockerfile +55 -0
  2. app.py +81 -0
  3. entrypoint.sh +19 -0
  4. requirements.txt +12 -0
  5. tts_engine.py +70 -0
Dockerfile ADDED
@@ -0,0 +1,55 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # ÉTAPE 1: Image de base
2
+ FROM python:3.11-slim
3
+
4
+ # ÉTAPE 2: Configuration et Dossier de travail
5
+ # Ligne supprimée (ENV PORT 8080) pour laisser Hugging Face Spaces injecter le port correct ($PORT, généralement 7860).
6
+ ENV FLASK_APP app.py
7
+ ENV GUNICORN_WORKERS 4
8
+ ENV GUNICORN_THREADS 2
9
+
10
+ # Création et utilisation du répertoire /app
11
+ WORKDIR /app
12
+
13
+ # ÉTAPE 3: Installation des dépendances (OPTIMISATION CACHING)
14
+ # Copie uniquement de requirements.txt pour mettre en cache l'installation
15
+ COPY requirements.txt .
16
+
17
+ # Installation des dépendances du système (eSpeak) et Python
18
+ RUN apt-get update && \
19
+ apt-get install -y espeak libespeak1 && \
20
+ pip install --no-cache-dir -r requirements.txt \
21
+ && rm requirements.txt
22
+
23
+ # ÉTAPE 4: Copie de l'Application et des Fichiers
24
+ # Nous copions tous les fichiers de l'application et nous assurons que
25
+ # l'utilisateur 'user' en est le propriétaire.
26
+
27
+ # CORRECTION MAJEURE : Ajout du dossier templates
28
+ # Ceci est l'étape essentielle pour que Flask trouve vos fichiers HTML
29
+ COPY templates /app/templates
30
+
31
+ # Copie des autres fichiers (y compris app.py, votre point d'entrée)
32
+ COPY app.py .
33
+ COPY tts_engine.py .
34
+
35
+ # Copie du script d'entrée
36
+ COPY entrypoint.sh .
37
+
38
+ # NOUVEAU: CORRECTION DES FINS DE LIGNE (Résout 'exec ./entrypoint.sh: no such file or directory')
39
+ # Supprime le caractère de retour chariot (\r)
40
+ RUN sed -i 's/\r$//' entrypoint.sh
41
+
42
+ # Le rendre exécutable
43
+ RUN chmod +x entrypoint.sh
44
+
45
+ # ÉTAPE 5: Sécurité et Exécution
46
+ # Création et bascule vers l'utilisateur non-root ('user') pour la sécurité
47
+ RUN useradd -ms /bin/bash user
48
+ RUN chown -R user:user /app
49
+ USER user
50
+
51
+ # Indique à Docker que le conteneur écoute sur ce port
52
+ EXPOSE $PORT
53
+
54
+ # Lance l'application via le script d'entrée
55
+ CMD ["./entrypoint.sh"]
app.py ADDED
@@ -0,0 +1,81 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # app.py
2
+ from flask import Flask, render_template, request, send_file, redirect, url_for
3
+ from werkzeug.utils import secure_filename
4
+ from tts_engine import get_available_languages, text_to_audio_file
5
+ import os
6
+ import io
7
+ import tempfile
8
+ # Note: Nous allons ignorer les autres imports inutilisés (config, auth, etc.) du Dockerfile pour simplifier.
9
+
10
+ app = Flask(__name__)
11
+
12
+ # Configuration simple pour l'upload
13
+ UPLOAD_FOLDER = 'uploads'
14
+ os.makedirs(UPLOAD_FOLDER, exist_ok=True)
15
+ app.config['UPLOAD_FOLDER'] = UPLOAD_FOLDER
16
+ app.config['MAX_CONTENT_LENGTH'] = 16 * 1024 * 1024 # Limite 16MB
17
+
18
+ # Fichier temporaire pour l'audio généré
19
+ TEMP_AUDIO_FILE = "tts_output.wav"
20
+
21
+ @app.route('/', methods=['GET'])
22
+ def index():
23
+ """Route principale: Affiche le formulaire et la liste des langues."""
24
+ # Récupérer les langues pour le menu déroulant
25
+ languages = get_available_languages()
26
+
27
+ # Afficher la template HTML
28
+ return render_template('index.html', languages=languages)
29
+
30
+ @app.route('/read', methods=['POST'])
31
+ def read_text():
32
+ """Route de traitement: Reçoit le texte/fichier et la langue, génère l'audio."""
33
+
34
+ # 1. Récupération des données
35
+ text_input = request.form.get('text_input', '').strip()
36
+ voice_id = request.form.get('language_select')
37
+ uploaded_file = request.files.get('file_input')
38
+
39
+ text_to_speak = ""
40
+
41
+ # 2. Logique de lecture de fichier/texte
42
+ if uploaded_file and uploaded_file.filename != '':
43
+ filename = secure_filename(uploaded_file.filename)
44
+ # Gestion simple pour les fichiers .txt uniquement pour l'instant
45
+ if filename.endswith('.txt'):
46
+ try:
47
+ # Lecture du contenu du fichier
48
+ text_to_speak = uploaded_file.read().decode('utf-8')
49
+ except Exception as e:
50
+ return f"Erreur de lecture du fichier: {e}", 400
51
+ else:
52
+ # Le PDF nécessiterait l'installation de PyPDF2 et un traitement plus complexe.
53
+ return "Type de fichier non supporté. Veuillez utiliser un fichier .txt.", 400
54
+
55
+ elif text_input:
56
+ text_to_speak = text_input
57
+
58
+ if not text_to_speak:
59
+ return "Veuillez entrer du texte ou télécharger un fichier.", 400
60
+
61
+ if not voice_id:
62
+ # Fallback au cas où aucune voix n'est sélectionnée (devrait être géré par l'interface)
63
+ voices = get_available_languages()
64
+ voice_id = next(iter(voices.values()), None)
65
+
66
+ # 3. Génération du fichier audio
67
+ try:
68
+ # Utiliser un fichier temporaire unique dans le conteneur
69
+ temp_dir = tempfile.gettempdir()
70
+ audio_path = os.path.join(temp_dir, TEMP_AUDIO_FILE)
71
+
72
+ text_to_audio_file(text_to_speak, voice_id, audio_path)
73
+
74
+ # 4. Envoi du fichier audio au client
75
+ # Flask envoie le fichier et le supprime après (mieux que de le garder sur le disque)
76
+ return send_file(audio_path, mimetype="audio/wav", as_attachment=False)
77
+
78
+ except Exception as e:
79
+ print(f"Erreur lors de la synthèse vocale: {e}")
80
+ return f"Erreur lors de la génération de l'audio: {e}", 500
81
+
entrypoint.sh ADDED
@@ -0,0 +1,19 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/bin/bash
2
+ # entrypoint.sh
3
+
4
+ # ... (Afficher les commandes exécutées, set -e, etc.)
5
+
6
+ echo "--- Démarrage de l'Application Gunicorn (Base de Données Baserow) ---"
7
+
8
+ # Définir le port par défaut de Hugging Face si $PORT est vide
9
+ export APP_PORT=${PORT:-7860}
10
+
11
+ # 1. Démarrer le serveur Flask/Gunicorn en premier plan
12
+ echo "Démarrage du serveur Gunicorn sur le port $APP_PORT avec Eventlet..."
13
+
14
+ # MODIFICATION MAJEURE: Utiliser le worker 'eventlet' pour supporter SocketIO (WebSockets)
15
+ # Nous pointons toujours vers app:app, mais Gunicorn utilise le worker Eventlet
16
+ # qui enveloppera l'application Flask et SocketIO.
17
+ exec gunicorn --workers 1 --worker-class eventlet app:app -b 0.0.0.0:$APP_PORT
18
+ # Note sur les workers: Eventlet et Gevent sont monocœurs, donc on met workers=1
19
+ # ou on s'assure que le nombre de workers est bas pour éviter des conflits de PTY.
requirements.txt ADDED
@@ -0,0 +1,12 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ flask
2
+ bcrypt
3
+ requests
4
+ gunicorn
5
+ Flask-CORS
6
+ python-dotenv
7
+ stripe
8
+ dnspython
9
+ werkzeug
10
+ flask-socketio
11
+ eventlet
12
+ pyttsx3
tts_engine.py ADDED
@@ -0,0 +1,70 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # tts_engine.py
2
+ import pyttsx3
3
+ import os
4
+ import io
5
+
6
+ # Initialisation du moteur pyttsx3
7
+ # Attention: L'initialisation du moteur est lente. Il est préférable de le faire une seule fois.
8
+ _engine = None
9
+
10
+ def get_tts_engine():
11
+ """Initialise et retourne le moteur pyttsx3."""
12
+ global _engine
13
+ if _engine is None:
14
+ try:
15
+ # Utilise 'espeak' comme moteur par défaut sur Linux (via Docker)
16
+ _engine = pyttsx3.init()
17
+ # Optimisation: Réglage des propriétés du moteur (optionnel)
18
+ _engine.setProperty('rate', 150) # Vitesse de lecture
19
+ except Exception as e:
20
+ print(f"Erreur d'initialisation pyttsx3: {e}")
21
+ _engine = None # S'assure qu'on ne garde pas une référence cassée
22
+ return _engine
23
+
24
+ def get_available_languages():
25
+ """Récupère la liste des langues/voix disponibles."""
26
+ engine = get_tts_engine()
27
+ if not engine:
28
+ return {}
29
+
30
+ voices = engine.getProperty('voices')
31
+ languages = {}
32
+
33
+ # On essaie de mapper la voix eSpeak à un code de langue/nom
34
+ for voice in voices:
35
+ # L'ID de la voix eSpeak contient souvent le code de langue (ex: 'french', 'en-us')
36
+ voice_id = voice.id.split('+')[0].replace(' ', '').lower()
37
+
38
+ if 'en' in voice_id or 'english' in voice.name.lower():
39
+ languages['English'] = voice.id
40
+ elif 'fr' in voice_id or 'french' in voice.name.lower():
41
+ languages['Français'] = voice.id
42
+ elif 'es' in voice_id or 'spanish' in voice.name.lower():
43
+ languages['Español'] = voice.id
44
+
45
+ # Ajout d'une entrée par défaut pour chaque voix, si non déjà mappée
46
+ if voice.name not in languages and voice.name != 'default':
47
+ languages[voice.name] = voice.id
48
+
49
+ # S'assurer d'avoir au moins une option si le mapping est faible
50
+ if not languages and voices:
51
+ languages['Default'] = voices[0].id
52
+
53
+ return languages
54
+
55
+ def text_to_audio_file(text, voice_id, output_path="output.wav"):
56
+ """Convertit le texte en fichier audio avec la voix spécifiée."""
57
+ engine = get_tts_engine()
58
+ if not engine:
59
+ raise Exception("Moteur TTS non initialisé.")
60
+
61
+ # 1. Sélection de la voix
62
+ engine.setProperty('voice', voice_id)
63
+
64
+ # 2. Sauvegarde du fichier
65
+ engine.save_to_file(text, output_path)
66
+ engine.runAndWait()
67
+
68
+ # NOTE: eSpeak génère par défaut un fichier .wav.
69
+ return os.path.abspath(output_path)
70
+