dcrey7 commited on
Commit
f5365fb
·
verified ·
1 Parent(s): 2d5ac34

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +262 -105
app.py CHANGED
@@ -1,6 +1,5 @@
1
  from flask import Flask, render_template, request, jsonify
2
- from flask_socketio import SocketIO, emit, join_room
3
- from flask_cors import CORS
4
  import os
5
  import requests
6
  import json
@@ -10,182 +9,340 @@ from dotenv import load_dotenv
10
  import logging
11
  from werkzeug.utils import secure_filename
12
  import random
 
13
 
14
- # Initialize Flask with CORS
15
  app = Flask(__name__)
16
- CORS(app)
17
  app.config['SECRET_KEY'] = os.urandom(24)
18
  app.config['MAX_CONTENT_LENGTH'] = 16 * 1024 * 1024
19
 
20
- # Configure Socket.IO for Hugging Face Spaces
21
- socketio = SocketIO(app,
22
- cors_allowed_origins="*",
23
- async_mode='eventlet',
24
- logger=True,
25
- engineio_logger=True
26
- )
27
 
28
  # Load environment variables
29
  load_dotenv()
30
  MISTRAL_API_KEY = os.getenv('MISTRAL_API_KEY')
31
  ELEVENLABS_API_KEY = os.getenv('ELEVENLABS_API_KEY')
32
 
33
- # Configure logging
34
- logging.basicConfig(level=logging.INFO)
 
 
 
35
  logger = logging.getLogger(__name__)
36
 
37
  class GameState:
38
- """Manages all active game sessions with WebSocket room support"""
39
 
40
  def __init__(self):
41
  self.games = {}
42
  self.cleanup_interval = 3600
43
-
44
  def create_game(self):
45
- game_id = str(uuid.uuid4())
46
- self.games[game_id] = {
47
- 'players': [],
48
- 'current_phase': 'setup',
49
- 'recordings': {},
50
- 'impostor': None,
51
- 'votes': {},
52
- 'question': None,
53
- 'impostor_answer': None,
54
- 'modified_recording': None,
55
- 'round_number': 1,
56
- 'start_time': datetime.now().isoformat(),
57
- 'completed_rounds': [],
58
- 'score': {'impostor_wins': 0, 'player_wins': 0},
59
- 'socket_room': f'game_{game_id}'
60
- }
61
- return game_id
 
 
 
 
 
 
 
 
 
 
 
 
 
 
62
 
63
  def cleanup_inactive_games(self):
 
64
  current_time = datetime.now()
65
  for game_id, game in list(self.games.items()):
66
- if (current_time - datetime.fromisoformat(game['start_time'])).total_seconds() > 7200:
 
67
  del self.games[game_id]
 
68
 
 
69
  game_state = GameState()
70
 
71
- # WebSocket Handlers
 
 
 
 
72
  @socketio.on('connect')
73
  def handle_connect():
74
- logger.info('Client connected: %s', request.sid)
 
 
75
 
76
  @socketio.on('disconnect')
77
  def handle_disconnect():
78
- logger.info('Client disconnected: %s', request.sid)
 
79
 
80
  @socketio.on('create_game')
81
  def handle_create_game():
 
82
  try:
83
  game_id = game_state.create_game()
 
 
84
  emit('game_created', {
85
- 'status': 'success',
86
  'gameId': game_id,
87
- 'message': 'Game created successfully'
88
  })
89
- logger.info('Created game: %s', game_id)
90
  except Exception as e:
91
- logger.error('Game creation failed: %s', str(e))
92
- emit('game_created', {
93
- 'status': 'error',
94
- 'error': 'Game creation failed',
95
  'details': str(e)
96
  })
97
 
98
  @socketio.on('join_game')
99
  def handle_join_game(data):
 
100
  try:
101
- game_id = data.get('game_id')
102
- player_name = data.get('player_name')
103
-
104
  if not game_id or not player_name:
105
- raise ValueError('Missing game ID or player name')
106
-
107
- if game_id not in game_state.games:
108
- raise KeyError('Game not found')
109
-
110
- game = game_state.games[game_id]
111
 
 
112
  if len(game['players']) >= 5:
113
- raise ValueError('Game is full')
114
 
 
115
  player_id = len(game['players']) + 1
116
- new_player = {
117
  'id': player_id,
118
  'name': player_name,
119
  'socket_id': request.sid
120
  }
121
- game['players'].append(new_player)
122
- join_room(game['socket_room'])
123
 
 
 
 
 
 
124
  emit('player_joined', {
125
- 'status': 'success',
126
- 'player': new_player
127
- }, room=game['socket_room'])
128
-
129
- logger.info('Player %s joined game %s', player_name, game_id)
130
-
131
- except Exception as e:
132
- logger.error('Join game error: %s', str(e))
133
- emit('join_failed', {
134
- 'status': 'error',
135
- 'error': str(e)
136
- })
137
 
138
- # REST API Endpoints
139
- @app.route('/')
140
- def home():
141
- return render_template('index.html')
142
 
143
  @app.route('/api/start_game', methods=['POST'])
144
- def start_game():
 
145
  try:
146
  data = request.get_json()
147
- game_id = data.get('game_id')
 
 
 
 
 
148
 
149
- if not game_id or game_id not in game_state.games:
150
- return jsonify({'status': 'error', 'error': 'Invalid game ID'}), 400
151
-
152
- game = game_state.games[game_id]
 
 
 
153
  game['current_phase'] = 'recording'
154
 
155
- socketio.emit('round_start', {
156
- 'phase': 'recording',
157
- 'duration': 30
158
- }, room=game['socket_room'])
159
-
160
- return jsonify({'status': 'success', 'message': 'Game started'})
161
 
 
 
 
 
 
 
 
 
 
 
162
  except Exception as e:
163
- logger.error('Start game error: %s', str(e))
164
- return jsonify({'status': 'error', 'error': str(e)}), 500
 
 
 
 
165
 
166
- # Voice Processing Functions
167
  async def generate_question():
168
- # Implementation remains same as before
169
- pass
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
170
 
171
- async def generate_impostor_answer(question):
172
- # Implementation remains same as before
173
- pass
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
174
 
175
- async def clone_voice(audio_file):
176
- # Implementation remains same as before
177
- pass
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
178
 
179
- async def generate_cloned_speech(voice_id, text):
180
- # Implementation remains same as before
181
- pass
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
182
 
183
  if __name__ == '__main__':
 
184
  os.makedirs('temp', exist_ok=True)
185
- socketio.run(app,
186
- host='0.0.0.0',
187
- port=7860,
188
- debug=True,
189
- allow_unsafe_werkzeug=True,
190
- use_reloader=False
191
- )
 
1
  from flask import Flask, render_template, request, jsonify
2
+ from flask_socketio import SocketIO, emit, join_room, leave_room
 
3
  import os
4
  import requests
5
  import json
 
9
  import logging
10
  from werkzeug.utils import secure_filename
11
  import random
12
+ import asyncio
13
 
14
+ # Initialize Flask and configure core settings
15
  app = Flask(__name__)
 
16
  app.config['SECRET_KEY'] = os.urandom(24)
17
  app.config['MAX_CONTENT_LENGTH'] = 16 * 1024 * 1024
18
 
19
+ # Initialize SocketIO with CORS support and logging
20
+ socketio = SocketIO(app, cors_allowed_origins="*", logger=True, engineio_logger=True, async_mode='eventlet')
 
 
 
 
 
21
 
22
  # Load environment variables
23
  load_dotenv()
24
  MISTRAL_API_KEY = os.getenv('MISTRAL_API_KEY')
25
  ELEVENLABS_API_KEY = os.getenv('ELEVENLABS_API_KEY')
26
 
27
+ # Configure logging with more detailed format
28
+ logging.basicConfig(
29
+ level=logging.INFO,
30
+ format='%(asctime)s - %(name)s - %(levelname)s - %(message)s'
31
+ )
32
  logger = logging.getLogger(__name__)
33
 
34
  class GameState:
35
+ """Manages the state of all active game sessions."""
36
 
37
  def __init__(self):
38
  self.games = {}
39
  self.cleanup_interval = 3600
40
+
41
  def create_game(self):
42
+ """Creates a new game session with proper initialization."""
43
+ try:
44
+ game_id = str(uuid.uuid4())
45
+ self.games[game_id] = {
46
+ 'players': [],
47
+ 'current_phase': 'setup',
48
+ 'recordings': {},
49
+ 'impostor': None,
50
+ 'votes': {},
51
+ 'question': None,
52
+ 'impostor_answer': None,
53
+ 'modified_recording': None,
54
+ 'round_number': 1,
55
+ 'start_time': datetime.now().isoformat(),
56
+ 'completed_rounds': [],
57
+ 'score': {'impostor_wins': 0, 'player_wins': 0},
58
+ 'room': game_id # Add room for socket management
59
+ }
60
+ logger.info(f"Successfully created game with ID: {game_id}")
61
+ return game_id
62
+ except Exception as e:
63
+ logger.error(f"Error creating game: {str(e)}")
64
+ raise
65
+
66
+ def get_game(self, game_id):
67
+ """Safely retrieves a game by ID."""
68
+ game = self.games.get(game_id)
69
+ if not game:
70
+ logger.error(f"Game not found: {game_id}")
71
+ raise ValueError("Game not found")
72
+ return game
73
 
74
  def cleanup_inactive_games(self):
75
+ """Removes inactive game sessions."""
76
  current_time = datetime.now()
77
  for game_id, game in list(self.games.items()):
78
+ start_time = datetime.fromisoformat(game['start_time'])
79
+ if (current_time - start_time).total_seconds() > 7200: # 2 hours
80
  del self.games[game_id]
81
+ logger.info(f"Cleaned up inactive game: {game_id}")
82
 
83
+ # Initialize global game state
84
  game_state = GameState()
85
 
86
+ @app.route('/')
87
+ def home():
88
+ """Serves the main game page."""
89
+ return render_template('index.html')
90
+
91
  @socketio.on('connect')
92
  def handle_connect():
93
+ """Handles client connection."""
94
+ logger.info(f"Client connected: {request.sid}")
95
+ emit('connection_success', {'status': 'connected'})
96
 
97
  @socketio.on('disconnect')
98
  def handle_disconnect():
99
+ """Handles client disconnection."""
100
+ logger.info(f"Client disconnected: {request.sid}")
101
 
102
  @socketio.on('create_game')
103
  def handle_create_game():
104
+ """Handles game creation request."""
105
  try:
106
  game_id = game_state.create_game()
107
+ join_room(game_id) # Create socket room for the game
108
+ logger.info(f"Created and joined game room: {game_id}")
109
  emit('game_created', {
 
110
  'gameId': game_id,
111
+ 'status': 'success'
112
  })
 
113
  except Exception as e:
114
+ logger.error(f"Error in game creation: {str(e)}")
115
+ emit('game_error', {
116
+ 'error': 'Failed to create game',
 
117
  'details': str(e)
118
  })
119
 
120
  @socketio.on('join_game')
121
  def handle_join_game(data):
122
+ """Handles player joining a game."""
123
  try:
124
+ game_id = data.get('gameId')
125
+ player_name = data.get('playerName')
126
+
127
  if not game_id or not player_name:
128
+ raise ValueError("Missing game ID or player name")
129
+
130
+ game = game_state.get_game(game_id)
 
 
 
131
 
132
+ # Validate player count
133
  if len(game['players']) >= 5:
134
+ raise ValueError("Game is full")
135
 
136
+ # Add player to game
137
  player_id = len(game['players']) + 1
138
+ player = {
139
  'id': player_id,
140
  'name': player_name,
141
  'socket_id': request.sid
142
  }
143
+ game['players'].append(player)
 
144
 
145
+ # Join socket room
146
+ join_room(game_id)
147
+ logger.info(f"Player {player_name} (ID: {player_id}) joined game {game_id}")
148
+
149
+ # Broadcast to all players in the game
150
  emit('player_joined', {
151
+ 'playerId': player_id,
152
+ 'playerName': player_name,
153
+ 'status': 'success'
154
+ }, room=game_id)
 
 
 
 
 
 
 
 
155
 
156
+ except Exception as e:
157
+ error_msg = str(e)
158
+ logger.error(f"Error in handle_join_game: {error_msg}")
159
+ emit('game_error', {'error': error_msg})
160
 
161
  @app.route('/api/start_game', methods=['POST'])
162
+ async def start_game():
163
+ """Initializes a new game round."""
164
  try:
165
  data = request.get_json()
166
+ game_id = data.get('gameId')
167
+
168
+ if not game_id:
169
+ raise ValueError("Missing game ID")
170
+
171
+ game = game_state.get_game(game_id)
172
 
173
+ # Validate player count
174
+ if len(game['players']) < 3:
175
+ raise ValueError("Need at least 3 players to start")
176
+
177
+ # Generate question using Mistral AI
178
+ question = await generate_question()
179
+ game['question'] = question
180
  game['current_phase'] = 'recording'
181
 
182
+ logger.info(f"Started game {game_id} with question: {question}")
 
 
 
 
 
183
 
184
+ # Notify all players in the game room
185
+ socketio.emit('round_started', {
186
+ 'question': question
187
+ }, room=game_id)
188
+
189
+ return jsonify({
190
+ 'status': 'success',
191
+ 'question': question
192
+ })
193
+
194
  except Exception as e:
195
+ error_msg = str(e)
196
+ logger.error(f"Error starting game: {error_msg}")
197
+ return jsonify({
198
+ 'status': 'error',
199
+ 'error': error_msg
200
+ }), 500
201
 
 
202
  async def generate_question():
203
+ """Generates an engaging question using Mistral AI."""
204
+ try:
205
+ headers = {
206
+ 'Authorization': f'Bearer {MISTRAL_API_KEY}',
207
+ 'Content-Type': 'application/json'
208
+ }
209
+
210
+ payload = {
211
+ 'messages': [{
212
+ 'role': 'user',
213
+ 'content': '''Generate an engaging personal question for a social game.
214
+ The question should:
215
+ 1. Encourage creative and unique responses
216
+ 2. Be open-ended but not too philosophical
217
+ 3. Be answerable in 15-30 seconds
218
+ 4. Be appropriate for all ages
219
+ 5. Spark interesting conversation
220
 
221
+ Generate only the question, without any additional text.'''
222
+ }]
223
+ }
224
+
225
+ response = requests.post(
226
+ 'https://api.mistral.ai/v1/chat/completions',
227
+ headers=headers,
228
+ json=payload,
229
+ timeout=10
230
+ )
231
+
232
+ if response.status_code == 200:
233
+ question = response.json()['choices'][0]['message']['content'].strip()
234
+ logger.info(f"Generated question: {question}")
235
+ return question
236
+
237
+ logger.error(f"Mistral API error: {response.status_code}")
238
+ return random.choice([
239
+ "What's your favorite childhood memory?",
240
+ "What's the most interesting place you've ever visited?",
241
+ "What's a skill you'd love to master and why?",
242
+ "What's the best piece of advice you've ever received?"
243
+ ])
244
+
245
+ except Exception as e:
246
+ logger.error(f"Error generating question: {str(e)}")
247
+ return "What is your favorite memory from your childhood?"
248
 
249
+ @app.route('/api/submit_recording', methods=['POST'])
250
+ async def submit_recording():
251
+ """Handles voice recording submissions."""
252
+ try:
253
+ game_id = request.form.get('gameId')
254
+ player_id = request.form.get('playerId')
255
+ audio_file = request.files.get('audio')
256
+
257
+ if not all([game_id, player_id, audio_file]):
258
+ raise ValueError("Missing required data")
259
+
260
+ game = game_state.get_game(game_id)
261
+
262
+ # Save the recording
263
+ filename = secure_filename(f"recording_{game_id}_{player_id}.wav")
264
+ filepath = os.path.join('temp', filename)
265
+ audio_file.save(filepath)
266
+
267
+ game['recordings'][player_id] = filepath
268
+ logger.info(f"Saved recording for player {player_id} in game {game_id}")
269
+
270
+ # Notify all players about the new recording
271
+ socketio.emit('recording_submitted', {
272
+ 'playerId': player_id,
273
+ 'status': 'success'
274
+ }, room=game_id)
275
+
276
+ return jsonify({'status': 'success'})
277
+
278
+ except Exception as e:
279
+ error_msg = str(e)
280
+ logger.error(f"Error submitting recording: {error_msg}")
281
+ return jsonify({
282
+ 'status': 'error',
283
+ 'error': error_msg
284
+ }), 500
285
 
286
+ @socketio.on('submit_vote')
287
+ def handle_vote(data):
288
+ """Processes player votes and determines round outcome."""
289
+ try:
290
+ game_id = data.get('gameId')
291
+ voter_id = data.get('voterId')
292
+ vote_for = data.get('voteFor')
293
+
294
+ if not all([game_id, voter_id, vote_for]):
295
+ raise ValueError("Missing vote data")
296
+
297
+ game = game_state.get_game(game_id)
298
+ game['votes'][voter_id] = vote_for
299
+
300
+ # Check if all players have voted
301
+ if len(game['votes']) == len(game['players']):
302
+ # Calculate results
303
+ votes_count = {}
304
+ for vote in game['votes'].values():
305
+ votes_count[vote] = votes_count.get(vote, 0) + 1
306
+
307
+ most_voted = max(votes_count.items(), key=lambda x: x[1])[0]
308
+
309
+ # Update scores
310
+ if most_voted == game['impostor']:
311
+ game['score']['player_wins'] += 1
312
+ result = 'players_win'
313
+ else:
314
+ game['score']['impostor_wins'] += 1
315
+ result = 'impostor_wins'
316
+
317
+ # Store round results
318
+ game['completed_rounds'].append({
319
+ 'round_number': game['round_number'],
320
+ 'impostor': game['impostor'],
321
+ 'votes': game['votes'].copy(),
322
+ 'most_voted': most_voted,
323
+ 'result': result
324
+ })
325
+
326
+ # Emit results to all players
327
+ emit('round_result', {
328
+ 'impostor': game['impostor'],
329
+ 'most_voted': most_voted,
330
+ 'votes': game['votes'],
331
+ 'score': game['score'],
332
+ 'result': result
333
+ }, room=game_id)
334
+
335
+ logger.info(f"Round completed for game {game_id}. Result: {result}")
336
+
337
+ except Exception as e:
338
+ error_msg = str(e)
339
+ logger.error(f"Error processing vote: {error_msg}")
340
+ emit('game_error', {'error': error_msg})
341
 
342
  if __name__ == '__main__':
343
+ # Create temporary directory for recordings
344
  os.makedirs('temp', exist_ok=True)
345
+
346
+ # Start the server
347
+ logger.info("Starting server...")
348
+ socketio.run(app, host='0.0.0.0', port=7860, debug=True)