Update app.py
Browse files
app.py
CHANGED
|
@@ -1,283 +1,127 @@
|
|
| 1 |
-
import
|
| 2 |
-
import
|
| 3 |
-
import
|
| 4 |
-
import tempfile
|
| 5 |
-
import logging
|
| 6 |
-
import json
|
| 7 |
-
import time
|
| 8 |
-
from flask import Flask, render_template, request, jsonify, send_file, stream_with_context, Response
|
| 9 |
-
from google import genai
|
| 10 |
-
import aiohttp
|
| 11 |
-
from pydub import AudioSegment
|
| 12 |
-
|
| 13 |
-
# Configure logging
|
| 14 |
-
logging.basicConfig(level=logging.DEBUG)
|
| 15 |
-
logger = logging.getLogger(__name__)
|
| 16 |
|
| 17 |
app = Flask(__name__)
|
| 18 |
-
app.secret_key =
|
| 19 |
-
|
| 20 |
-
# Configure Gemini API
|
| 21 |
-
api_key = os.environ.get("GEMINI_API_KEY")
|
| 22 |
-
if not api_key:
|
| 23 |
-
logger.warning("GEMINI_API_KEY not found in environment variables. Using default value for development.")
|
| 24 |
-
api_key = "YOUR_API_KEY" # This will be replaced with env var in production
|
| 25 |
-
|
| 26 |
-
# Define available voices
|
| 27 |
-
AVAILABLE_VOICES = [
|
| 28 |
-
"Puck", "Charon", "Kore", "Fenrir",
|
| 29 |
-
"Aoede", "Leda", "Orus", "Zephyr"
|
| 30 |
-
]
|
| 31 |
-
language_code="fr-FR"
|
| 32 |
|
| 33 |
-
#
|
| 34 |
-
|
| 35 |
-
|
| 36 |
-
"
|
| 37 |
-
"
|
| 38 |
-
|
| 39 |
-
}
|
|
|
|
|
|
|
|
|
|
| 40 |
|
| 41 |
-
def update_progress(current, total, message):
|
| 42 |
-
"""Update the global progress tracker."""
|
| 43 |
-
global generation_progress
|
| 44 |
-
generation_progress = {
|
| 45 |
-
"status": "in_progress" if current < total else "complete",
|
| 46 |
-
"current": current,
|
| 47 |
-
"total": total,
|
| 48 |
-
"message": message
|
| 49 |
-
}
|
| 50 |
-
def create_async_enumerate(async_iterator):
|
| 51 |
-
"""Create an async enumerate function since it's not built-in."""
|
| 52 |
-
i = 0
|
| 53 |
-
async def async_iter():
|
| 54 |
-
nonlocal i
|
| 55 |
-
async for item in async_iterator:
|
| 56 |
-
yield i, item
|
| 57 |
-
i += 1
|
| 58 |
-
return async_iter()
|
| 59 |
-
|
| 60 |
-
async def generate_speech(text, selected_voice):
|
| 61 |
-
"""Generate speech from text using Gemini AI."""
|
| 62 |
-
try:
|
| 63 |
-
client = genai.Client(api_key=api_key)
|
| 64 |
-
model = "gemini-2.0-flash-live-001"
|
| 65 |
-
|
| 66 |
-
# Configure the voice settings
|
| 67 |
-
speech_config = genai.types.SpeechConfig(
|
| 68 |
-
language_code=language_code,
|
| 69 |
-
voice_config=genai.types.VoiceConfig(
|
| 70 |
-
prebuilt_voice_config=genai.types.PrebuiltVoiceConfig(
|
| 71 |
-
voice_name=selected_voice
|
| 72 |
-
)
|
| 73 |
-
)
|
| 74 |
-
)
|
| 75 |
-
|
| 76 |
-
config = genai.types.LiveConnectConfig(
|
| 77 |
-
response_modalities=["AUDIO"],
|
| 78 |
-
speech_config=speech_config
|
| 79 |
-
)
|
| 80 |
-
|
| 81 |
-
# Create a temporary file to store the audio
|
| 82 |
-
with tempfile.NamedTemporaryFile(suffix=".wav", delete=False) as tmp_file:
|
| 83 |
-
temp_filename = tmp_file.name
|
| 84 |
-
|
| 85 |
-
async with client.aio.live.connect(model=model, config=config) as session:
|
| 86 |
-
# Open the WAV file for writing
|
| 87 |
-
wf = wave.open(temp_filename, "wb")
|
| 88 |
-
wf.setnchannels(1)
|
| 89 |
-
wf.setsampwidth(2)
|
| 90 |
-
wf.setframerate(24000)
|
| 91 |
-
|
| 92 |
-
# Send the text to Gemini
|
| 93 |
-
await session.send_client_content(
|
| 94 |
-
turns={"role": "user", "parts": [{"text": text}]},
|
| 95 |
-
turn_complete=True
|
| 96 |
-
)
|
| 97 |
-
|
| 98 |
-
# Receive the audio data and write it to the file
|
| 99 |
-
async for idx, response in create_async_enumerate(session.receive()):
|
| 100 |
-
if response.data is not None:
|
| 101 |
-
wf.writeframes(response.data)
|
| 102 |
-
|
| 103 |
-
wf.close()
|
| 104 |
-
|
| 105 |
-
return temp_filename
|
| 106 |
-
|
| 107 |
-
except Exception as e:
|
| 108 |
-
logger.error(f"Error generating speech: {str(e)}")
|
| 109 |
-
raise e
|
| 110 |
|
| 111 |
@app.route('/')
|
| 112 |
def index():
|
| 113 |
-
|
| 114 |
-
return render_template('index.html', voices=AVAILABLE_VOICES)
|
| 115 |
|
| 116 |
-
@app.route('/
|
| 117 |
-
|
| 118 |
-
|
| 119 |
-
|
| 120 |
-
data = request.json
|
| 121 |
-
text = data.get('text', '')
|
| 122 |
-
voice = data.get('voice', 'Kore') # Default voice
|
| 123 |
-
|
| 124 |
-
if not text:
|
| 125 |
-
return jsonify({"error": "Text is required"}), 400
|
| 126 |
-
|
| 127 |
-
if voice not in AVAILABLE_VOICES:
|
| 128 |
-
return jsonify({"error": "Invalid voice selection"}), 400
|
| 129 |
|
| 130 |
-
|
| 131 |
-
|
| 132 |
-
|
| 133 |
-
return jsonify({
|
| 134 |
-
"status": "success",
|
| 135 |
-
"message": "Audio generated successfully",
|
| 136 |
-
"audioUrl": f"/audio/{os.path.basename(audio_file)}"
|
| 137 |
-
})
|
| 138 |
|
| 139 |
-
|
| 140 |
-
|
| 141 |
-
|
| 142 |
-
|
| 143 |
-
|
| 144 |
-
|
| 145 |
-
|
| 146 |
-
try:
|
| 147 |
-
temp_dir = tempfile.gettempdir()
|
| 148 |
-
file_path = os.path.join(temp_dir, filename)
|
| 149 |
-
|
| 150 |
-
if not os.path.exists(file_path):
|
| 151 |
-
return jsonify({"error": "Audio file not found"}), 404
|
| 152 |
-
|
| 153 |
-
return send_file(file_path, mimetype="audio/wav", as_attachment=False)
|
| 154 |
|
| 155 |
-
|
| 156 |
-
|
| 157 |
-
return jsonify({"error": str(e)}), 500
|
| 158 |
|
| 159 |
-
|
| 160 |
-
|
| 161 |
-
|
| 162 |
-
|
| 163 |
-
|
| 164 |
-
|
| 165 |
-
|
| 166 |
-
|
| 167 |
-
|
| 168 |
-
|
| 169 |
-
|
| 170 |
-
|
| 171 |
-
|
| 172 |
-
|
| 173 |
-
|
| 174 |
-
|
| 175 |
-
|
| 176 |
-
|
| 177 |
-
|
| 178 |
-
return jsonify({
|
| 179 |
-
"status": "started",
|
| 180 |
-
"message": "Génération du podcast commencée. Suivez la progression sur l'interface."
|
| 181 |
-
})
|
| 182 |
|
| 183 |
-
|
| 184 |
-
|
| 185 |
-
|
| 186 |
-
|
| 187 |
|
| 188 |
-
async def generate_podcast_background(scenario):
|
| 189 |
-
"""Generate a podcast in the background."""
|
| 190 |
try:
|
| 191 |
-
|
| 192 |
-
|
| 193 |
-
|
| 194 |
-
update_progress(0, total_characters, f"Préparation du podcast avec {total_characters} personnages...")
|
| 195 |
-
|
| 196 |
-
audio_segments = []
|
| 197 |
-
podcast_filename = None
|
| 198 |
-
|
| 199 |
-
for idx, character in enumerate(characters):
|
| 200 |
-
character_name = character.get('name', 'Unknown')
|
| 201 |
-
voice = character.get('voice', 'Kore')
|
| 202 |
-
text = character.get('text', '')
|
| 203 |
-
|
| 204 |
-
update_progress(idx, total_characters, f"Génération de l'audio pour {character_name} ({idx+1}/{total_characters})...")
|
| 205 |
-
|
| 206 |
-
if voice not in AVAILABLE_VOICES:
|
| 207 |
-
logger.warning(f"Voice {voice} not available. Using default voice Kore for {character_name}.")
|
| 208 |
-
voice = 'Kore'
|
| 209 |
-
|
| 210 |
-
# Generate speech for this character
|
| 211 |
-
try:
|
| 212 |
-
audio_file = await generate_speech(text, voice)
|
| 213 |
-
audio_segments.append(audio_file)
|
| 214 |
-
except Exception as e:
|
| 215 |
-
logger.error(f"Error generating speech for {character_name}: {str(e)}")
|
| 216 |
-
update_progress(0, 0, f"Erreur lors de la génération pour {character_name}: {str(e)}")
|
| 217 |
-
return
|
| 218 |
-
|
| 219 |
-
update_progress(total_characters, total_characters, "Assemblage des segments audio...")
|
| 220 |
-
|
| 221 |
-
# Combine all audio segments into one file
|
| 222 |
-
combined = AudioSegment.empty()
|
| 223 |
-
|
| 224 |
-
for audio_file in audio_segments:
|
| 225 |
-
segment = AudioSegment.from_wav(audio_file)
|
| 226 |
-
combined += segment
|
| 227 |
-
# Add a short silence between segments (500ms)
|
| 228 |
-
combined += AudioSegment.silent(duration=500)
|
| 229 |
-
|
| 230 |
-
# Export the combined audio
|
| 231 |
-
with tempfile.NamedTemporaryFile(suffix=".wav", delete=False) as output_file:
|
| 232 |
-
podcast_filename = output_file.name
|
| 233 |
-
combined.export(podcast_filename, format="wav")
|
| 234 |
-
|
| 235 |
-
update_progress(total_characters + 1, total_characters + 1, f"Podcast généré avec succès! audio:{os.path.basename(podcast_filename)}")
|
| 236 |
-
|
| 237 |
-
except Exception as e:
|
| 238 |
-
logger.error(f"Error in podcast background task: {str(e)}")
|
| 239 |
-
update_progress(0, 0, f"Erreur: {str(e)}")
|
| 240 |
|
| 241 |
-
@app.route('/podcast-status')
|
| 242 |
-
def podcast_status():
|
| 243 |
-
"""Get the current status of the podcast generation."""
|
| 244 |
-
global generation_progress
|
| 245 |
-
|
| 246 |
-
# If status is complete and contains an audioUrl in the message, extract it
|
| 247 |
-
if generation_progress["status"] == "complete" and "audio:" in generation_progress["message"]:
|
| 248 |
-
message_parts = generation_progress["message"].split("audio:")
|
| 249 |
-
if len(message_parts) > 1:
|
| 250 |
-
audio_filename = message_parts[1].strip()
|
| 251 |
-
return jsonify({
|
| 252 |
-
"status": "complete",
|
| 253 |
-
"message": message_parts[0].strip(),
|
| 254 |
-
"audioUrl": f"/audio/{audio_filename}"
|
| 255 |
-
})
|
| 256 |
-
|
| 257 |
-
# Otherwise just return the current progress
|
| 258 |
-
return jsonify(generation_progress)
|
| 259 |
|
| 260 |
-
|
| 261 |
-
|
| 262 |
-
"""Get the current progress of podcast generation."""
|
| 263 |
-
return jsonify(generation_progress)
|
| 264 |
|
| 265 |
-
|
| 266 |
-
|
| 267 |
-
|
| 268 |
-
|
| 269 |
-
|
| 270 |
-
|
| 271 |
-
|
| 272 |
-
if not os.path.exists(file_path):
|
| 273 |
-
return jsonify({"error": "Audio file not found"}), 404
|
| 274 |
-
|
| 275 |
-
# Check if this is a podcast or simple speech
|
| 276 |
-
download_name = "gemini_podcast.wav"
|
| 277 |
-
|
| 278 |
-
return send_file(file_path, mimetype="audio/wav", as_attachment=True,
|
| 279 |
-
download_name=download_name)
|
| 280 |
|
| 281 |
-
|
| 282 |
-
|
| 283 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from flask import Flask, render_template, request, jsonify, session
|
| 2 |
+
from stockfish import Stockfish # Assurez-vous que le chemin est correct si besoin
|
| 3 |
+
import chess # python-chess
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 4 |
|
| 5 |
app = Flask(__name__)
|
| 6 |
+
app.secret_key = 'super_secret_key_for_session' # Important pour les sessions
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 7 |
|
| 8 |
+
# Pour simplifier, une instance globale. Pour une prod, gérer par session/jeu.
|
| 9 |
+
# Assurez-vous que le binaire stockfish est dans votre PATH ou spécifiez le chemin.
|
| 10 |
+
try:
|
| 11 |
+
stockfish_path = "stockfish" # ou "/usr/games/stockfish" ou chemin vers votre binaire
|
| 12 |
+
stockfish = Stockfish(path=stockfish_path, parameters={"Threads": 2, "Hash": 128})
|
| 13 |
+
except Exception as e:
|
| 14 |
+
print(f"Erreur à l'initialisation de Stockfish: {e}")
|
| 15 |
+
print("Veuillez vérifier que Stockfish est installé et accessible via le PATH, ou spécifiez le chemin correct.")
|
| 16 |
+
# Vous pourriez vouloir quitter l'application ou avoir un mode dégradé.
|
| 17 |
+
stockfish = None
|
| 18 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 19 |
|
| 20 |
@app.route('/')
|
| 21 |
def index():
|
| 22 |
+
return render_template('index.html')
|
|
|
|
| 23 |
|
| 24 |
+
@app.route('/new_game', methods=['POST'])
|
| 25 |
+
def new_game():
|
| 26 |
+
if not stockfish:
|
| 27 |
+
return jsonify({"error": "Stockfish non initialisé"}), 500
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 28 |
|
| 29 |
+
data = request.json
|
| 30 |
+
mode = data.get('mode', 'human') # 'ai' or 'human'
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 31 |
|
| 32 |
+
initial_fen = "rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR w KQkq - 0 1"
|
| 33 |
+
session['fen'] = initial_fen
|
| 34 |
+
session['mode'] = mode
|
| 35 |
+
session['turn'] = 'w' # White's turn
|
| 36 |
+
session['history'] = []
|
| 37 |
+
|
| 38 |
+
stockfish.set_fen_position(initial_fen)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 39 |
|
| 40 |
+
# python-chess board (optionnel mais recommandé)
|
| 41 |
+
session['board_pgn'] = chess.Board().fen() # Stocker le FEN pour reconstruire
|
|
|
|
| 42 |
|
| 43 |
+
return jsonify({
|
| 44 |
+
"fen": initial_fen,
|
| 45 |
+
"turn": session['turn'],
|
| 46 |
+
"message": "Nouvelle partie commencée."
|
| 47 |
+
})
|
| 48 |
+
|
| 49 |
+
@app.route('/move', methods=['POST'])
|
| 50 |
+
def handle_move():
|
| 51 |
+
if not stockfish:
|
| 52 |
+
return jsonify({"error": "Stockfish non initialisé"}), 500
|
| 53 |
+
|
| 54 |
+
if 'fen' not in session:
|
| 55 |
+
return jsonify({"error": "Aucune partie en cours. Commencez une nouvelle partie."}), 400
|
| 56 |
+
|
| 57 |
+
data = request.json
|
| 58 |
+
move_uci = data.get('move') # e.g., "e2e4"
|
| 59 |
+
|
| 60 |
+
# Reconstruire l'état du board python-chess (si utilisé)
|
| 61 |
+
board = chess.Board(session['fen'])
|
|
|
|
|
|
|
|
|
|
|
|
|
| 62 |
|
| 63 |
+
# Validation du tour
|
| 64 |
+
current_player_color = 'w' if board.turn == chess.WHITE else 'b'
|
| 65 |
+
if session['turn'] != current_player_color:
|
| 66 |
+
return jsonify({"error": "Pas votre tour.", "fen": session['fen'], "turn": session['turn']}), 400
|
| 67 |
|
|
|
|
|
|
|
| 68 |
try:
|
| 69 |
+
move_obj = board.parse_uci(move_uci)
|
| 70 |
+
except ValueError:
|
| 71 |
+
return jsonify({"error": "Format de coup invalide.", "fen": session['fen'], "turn": session['turn']}), 400
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 72 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 73 |
|
| 74 |
+
if not stockfish.is_move_correct(move_uci) or move_obj not in board.legal_moves:
|
| 75 |
+
return jsonify({"error": "Coup illégal.", "fen": session['fen'], "turn": session['turn']}), 400
|
|
|
|
|
|
|
| 76 |
|
| 77 |
+
# Appliquer le coup humain
|
| 78 |
+
stockfish.make_moves_from_current_position([move_uci])
|
| 79 |
+
board.push(move_obj)
|
| 80 |
+
session['fen'] = stockfish.get_fen_position() # ou board.fen()
|
| 81 |
+
session['history'].append(move_uci)
|
| 82 |
+
session['turn'] = 'b' if current_player_color == 'w' else 'w'
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 83 |
|
| 84 |
+
ai_move_uci = None
|
| 85 |
+
game_status = "En cours"
|
| 86 |
+
|
| 87 |
+
if board.is_checkmate():
|
| 88 |
+
game_status = f"Mat! {'Les Blancs' if board.turn == chess.BLACK else 'Les Noirs'} gagnent."
|
| 89 |
+
elif board.is_stalemate() or board.is_insufficient_material() or board.is_seventyfive_moves() or board.is_fivefold_repetition():
|
| 90 |
+
game_status = "Pat!"
|
| 91 |
+
|
| 92 |
+
# Mode IA
|
| 93 |
+
if session['mode'] == 'ai' and game_status == "En cours" and ( (board.turn == chess.BLACK and current_player_color == 'w') or \
|
| 94 |
+
(board.turn == chess.WHITE and current_player_color == 'b') ) : # Tour de l'IA
|
| 95 |
+
# S'assurer que stockfish a la bonne position si on utilise board.fen()
|
| 96 |
+
stockfish.set_fen_position(board.fen())
|
| 97 |
+
ai_move_uci = stockfish.get_best_move_time(1000) # 1 seconde de réflexion
|
| 98 |
+
if ai_move_uci:
|
| 99 |
+
ai_move_obj = board.parse_uci(ai_move_uci)
|
| 100 |
+
stockfish.make_moves_from_current_position([ai_move_uci]) # Stockfish est déjà à jour
|
| 101 |
+
board.push(ai_move_obj)
|
| 102 |
+
session['fen'] = stockfish.get_fen_position() # ou board.fen()
|
| 103 |
+
session['history'].append(ai_move_uci)
|
| 104 |
+
session['turn'] = 'w' if session['turn'] == 'b' else 'b'
|
| 105 |
+
|
| 106 |
+
if board.is_checkmate():
|
| 107 |
+
game_status = f"Mat! {'Les Blancs' if board.turn == chess.BLACK else 'Les Noirs'} gagnent."
|
| 108 |
+
elif board.is_stalemate() or board.is_insufficient_material() or board.is_seventyfive_moves() or board.is_fivefold_repetition():
|
| 109 |
+
game_status = "Pat!"
|
| 110 |
+
else: # Si l'IA ne retourne pas de coup (ce qui peut arriver si elle est matée/patée)
|
| 111 |
+
if board.is_checkmate():
|
| 112 |
+
game_status = f"Mat! {'Les Blancs' if board.turn == chess.BLACK else 'Les Noirs'} gagnent."
|
| 113 |
+
elif board.is_stalemate():
|
| 114 |
+
game_status = "Pat!"
|
| 115 |
+
|
| 116 |
+
|
| 117 |
+
return jsonify({
|
| 118 |
+
"fen": session['fen'],
|
| 119 |
+
"turn": session['turn'],
|
| 120 |
+
"last_move_human": move_uci,
|
| 121 |
+
"last_move_ai": ai_move_uci,
|
| 122 |
+
"game_status": game_status,
|
| 123 |
+
"history": session['history']
|
| 124 |
+
})
|
| 125 |
+
|
| 126 |
+
if __name__ == '__main__':
|
| 127 |
+
app.run(debug=True)
|