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)
|