Spaces:
Sleeping
Sleeping
Upload 5 files
Browse files- Dockerfile +55 -0
- app.py +81 -0
- entrypoint.sh +19 -0
- requirements.txt +12 -0
- 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 |
+
|