Perilon commited on
Commit
13bbc90
·
1 Parent(s): 5b4d2a9
Files changed (1) hide show
  1. flask_app.py +154 -41
flask_app.py CHANGED
@@ -1,5 +1,5 @@
1
  from flask import Flask, render_template, jsonify, request, send_from_directory, send_file, redirect, url_for, session
2
- import os, json, threading, time
3
  from datetime import datetime
4
  from extract_signed_segments_from_annotations import ClipExtractor, VideoClip
5
  import logging
@@ -8,9 +8,30 @@ from dotenv import load_dotenv
8
  # Load environment variables
9
  load_dotenv()
10
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
11
  app = Flask(__name__)
12
  app.secret_key = os.getenv('SECRET_KEY', 'dev_key_for_testing')
13
- logging.basicConfig(level=logging.INFO)
 
 
 
 
 
14
 
15
  # Directory paths
16
  VIDEO_DIR = os.path.abspath("data/videos")
@@ -28,8 +49,16 @@ for directory in [VIDEO_DIR, ANNOTATIONS_DIR, TEMP_DIR, WORD_TIMESTAMPS_DIR, ALI
28
  clip_extraction_status = {}
29
  transcription_progress_status = {}
30
 
31
- # Check if we're running on Hugging Face Spaces
32
- is_hf_space = os.getenv('SPACE_ID') is not None
 
 
 
 
 
 
 
 
33
 
34
  # Login required decorator
35
  def login_required(f):
@@ -37,6 +66,7 @@ def login_required(f):
37
  @wraps(f)
38
  def decorated_function(*args, **kwargs):
39
  if 'user' not in session:
 
40
  return redirect(url_for('login'))
41
  return f(*args, **kwargs)
42
  return decorated_function
@@ -66,7 +96,7 @@ def run_clip_extraction(video_id):
66
  else:
67
  update_extraction_progress(video_id, 1, 1)
68
  except Exception as e:
69
- logging.error(f"Error during clip extraction for {video_id}: {str(e)}")
70
  clip_extraction_status[video_id] = {"error": str(e)}
71
 
72
  def run_transcription(video_id):
@@ -76,13 +106,23 @@ def run_transcription(video_id):
76
 
77
  # Check if transcription already exists and is valid.
78
  if os.path.exists(output_path) and os.path.getsize(output_path) > 0:
79
- app.logger.info(f"Using cached transcription for video {video_id}.")
80
  transcription_progress_status[video_id] = {"status": "completed", "percent": 100}
81
  return
82
 
83
  video_path = os.path.join(base_dir, "data", "videos", f"{video_id}.mp4")
84
  transcription_progress_status[video_id] = {"status": "started", "percent": 10}
85
 
 
 
 
 
 
 
 
 
 
 
86
  # Run transcription via the imported function from get_transcription_with_amazon.py
87
  from get_transcription_with_amazon import get_word_timestamps
88
  word_timestamps = get_word_timestamps(video_path)
@@ -92,28 +132,35 @@ def run_transcription(video_id):
92
 
93
  transcription_progress_status[video_id] = {"status": "completed", "percent": 100}
94
  except Exception as e:
95
- app.logger.error(f"Error during transcription for {video_id}: {str(e)}")
96
  transcription_progress_status[video_id] = {"status": "error", "percent": 0, "message": str(e)}
97
 
98
  # Authentication routes
99
  @app.route('/login')
100
  def login():
101
- if os.getenv('SPACE_ID'):
102
- # For Hugging Face Spaces, check if we already have the username in headers
 
 
103
  username = request.headers.get('X-Spaces-Username')
 
 
104
  if username and is_allowed_user(username):
105
  session['user'] = {'name': username, 'is_hf': True}
106
  return redirect(url_for('index'))
107
- # Otherwise, redirect to auth which will be handled by HF middleware
108
- return redirect('/auth')
 
109
  else:
110
- # For local development, just set a mock user
111
  session['user'] = {'name': 'LocalDeveloper', 'is_mock': True}
112
  return redirect(url_for('index'))
113
 
114
  @app.route('/auth/callback')
115
  def auth_callback():
116
- # This route will be called by Hugging Face after successful authentication
 
 
117
  if is_hf_space:
118
  # In Hugging Face Spaces, the user info is available in the request headers
119
  username = request.headers.get('X-Spaces-Username')
@@ -126,49 +173,93 @@ def auth_callback():
126
 
127
  @app.route('/auth')
128
  def auth():
129
- # This route will be handled by Hugging Face Spaces middleware
130
- if not is_hf_space:
 
 
 
 
 
 
 
 
 
 
 
131
  session['user'] = {'name': 'LocalDeveloper', 'is_mock': True}
132
- return redirect(url_for('index'))
 
 
 
 
 
133
 
134
  @app.before_request
135
  def check_auth():
136
- # Skip authentication for login/logout routes
137
- if request.path in ['/login', '/logout', '/auth', '/auth/callback'] or request.path.startswith('/static/'):
 
138
  return
139
 
140
- # In Hugging Face Spaces, check the username header
 
 
141
  if is_hf_space:
 
142
  username = request.headers.get('X-Spaces-Username')
 
 
 
 
 
143
  if username and is_allowed_user(username):
144
- # Update the session with the current user
145
- if 'user' not in session or session['user'].get('name') != username:
146
- session['user'] = {'name': username, 'is_hf': True}
147
  return
148
-
149
- # If no valid user in session or headers, redirect to auth
150
- if 'user' not in session:
151
- return redirect('/auth')
152
- # For local development, we already set a mock user in the login route
153
  elif 'user' not in session:
154
  return redirect(url_for('login'))
155
 
156
  @app.route('/logout')
157
  def logout():
 
158
  session.clear() # Clear the entire session
159
  if is_hf_space:
160
  return redirect('/auth/logout')
161
  return redirect(url_for('login'))
162
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
163
  # Main application routes
164
  @app.route('/')
165
  @login_required
166
  def index():
 
167
  return redirect(url_for('select_video'))
168
 
169
  @app.route('/select_video')
170
  @login_required
171
  def select_video():
 
172
  if not os.path.exists(VIDEO_DIR):
173
  return render_template('error.html', message="Video directory not found.")
174
  videos = [f for f in os.listdir(VIDEO_DIR) if f.endswith('.mp4')]
@@ -178,11 +269,13 @@ def select_video():
178
  @app.route('/player/<video_id>')
179
  @login_required
180
  def player(video_id):
 
181
  return render_template('player.html', video_id=video_id, user=session.get('user'))
182
 
183
  @app.route('/videos')
184
  @login_required
185
  def get_videos():
 
186
  if not os.path.exists(VIDEO_DIR):
187
  return jsonify({'error': 'Video directory not found'}), 404
188
  videos = [f for f in os.listdir(VIDEO_DIR) if f.endswith(('.mp4', '.avi', '.mov'))]
@@ -193,6 +286,7 @@ def get_videos():
193
  @app.route('/video/<path:filename>')
194
  @login_required
195
  def serve_video(filename):
 
196
  if not os.path.exists(os.path.join(VIDEO_DIR, filename)):
197
  return jsonify({'error': 'Video not found'}), 404
198
  return send_from_directory(VIDEO_DIR, filename)
@@ -200,6 +294,7 @@ def serve_video(filename):
200
  @app.route('/save_annotations', methods=['POST'])
201
  @login_required
202
  def save_annotations():
 
203
  data = request.json
204
  if not data or 'video' not in data or 'timestamps' not in data:
205
  return jsonify({'success': False, 'message': 'Invalid data'}), 400
@@ -218,6 +313,7 @@ def save_annotations():
218
  @app.route('/get_annotations/<path:video_name>')
219
  @login_required
220
  def get_annotations(video_name):
 
221
  annotation_file = os.path.join(ANNOTATIONS_DIR, f"{video_name}_annotations.json")
222
  if not os.path.exists(annotation_file):
223
  return jsonify({'error': 'No annotations found'}), 404
@@ -228,6 +324,7 @@ def get_annotations(video_name):
228
  @app.route("/alignment/<video_id>")
229
  @login_required
230
  def alignment_mode(video_id):
 
231
  annotation_file = os.path.join(ANNOTATIONS_DIR, f"{video_id}_annotations.json")
232
  if not os.path.exists(annotation_file):
233
  return render_template("error.html", message="No annotations found for this video. Please annotate the video first.")
@@ -243,10 +340,11 @@ def alignment_mode(video_id):
243
  @app.route("/api/transcript/<video_id>")
244
  @login_required
245
  def get_transcript(video_id):
 
246
  timestamps_file = os.path.join(WORD_TIMESTAMPS_DIR, f"{video_id}_word_timestamps.json")
247
- app.logger.info(f"Attempting to load word timestamps from: {timestamps_file}")
248
  if not os.path.exists(timestamps_file):
249
- app.logger.warning(f"Word timestamps file not found: {timestamps_file}")
250
  return jsonify({
251
  "status": "error",
252
  "message": "No word timestamps found for this video"
@@ -260,14 +358,14 @@ def get_transcript(video_id):
260
  "start": float(item["start_time"]),
261
  "end": float(item["end_time"])
262
  } for item in word_data]
263
- app.logger.info(f"Successfully created transcript ({len(full_text)} characters)")
264
  return jsonify({
265
  "status": "success",
266
  "text": full_text,
267
  "words": words_with_times
268
  })
269
  except Exception as e:
270
- app.logger.error(f"Error processing word timestamps: {str(e)}")
271
  return jsonify({
272
  "status": "error",
273
  "message": f"Error processing word timestamps: {str(e)}"
@@ -276,10 +374,11 @@ def get_transcript(video_id):
276
  @app.route("/api/word_timestamps/<video_id>")
277
  @login_required
278
  def get_word_timestamps(video_id):
 
279
  timestamps_file = os.path.join(WORD_TIMESTAMPS_DIR, f"{video_id}_word_timestamps.json")
280
- app.logger.info(f"Attempting to load word timestamps from: {timestamps_file}")
281
  if not os.path.exists(timestamps_file):
282
- app.logger.warning(f"Word timestamps file not found: {timestamps_file}")
283
  return jsonify({
284
  "status": "error",
285
  "message": "No word timestamps found for this video"
@@ -287,13 +386,13 @@ def get_word_timestamps(video_id):
287
  try:
288
  with open(timestamps_file, 'r') as f:
289
  word_data = json.load(f)
290
- app.logger.info(f"Successfully loaded {len(word_data)} word timestamps")
291
  return jsonify({
292
  "status": "success",
293
  "words": word_data
294
  })
295
  except Exception as e:
296
- app.logger.error(f"Error processing word timestamps: {str(e)}")
297
  return jsonify({
298
  "status": "error",
299
  "message": f"Error processing word timestamps: {str(e)}"
@@ -302,6 +401,7 @@ def get_word_timestamps(video_id):
302
  @app.route("/api/clips/<video_id>")
303
  @login_required
304
  def get_video_clips(video_id):
 
305
  try:
306
  annotation_file = os.path.join(ANNOTATIONS_DIR, f"{video_id}_annotations.json")
307
  if not os.path.exists(annotation_file):
@@ -322,7 +422,7 @@ def get_video_clips(video_id):
322
  "clips": clips
323
  })
324
  except Exception as e:
325
- app.logger.error(f"Error getting clips: {str(e)}")
326
  return jsonify({
327
  "status": "error",
328
  "message": str(e)
@@ -331,13 +431,14 @@ def get_video_clips(video_id):
331
  @app.route("/clip/<video_id>/<int:clip_index>")
332
  @login_required
333
  def serve_clip(video_id, clip_index):
 
334
  clip_path = os.path.join(
335
  TEMP_DIR,
336
  f"{video_id}_clip_{clip_index:03d}.mp4"
337
  )
338
- app.logger.info(f"Attempting to serve clip: {clip_path}")
339
  if not os.path.exists(clip_path):
340
- app.logger.error(f"Clip not found: {clip_path}")
341
  return jsonify({
342
  "status": "error",
343
  "message": "Clip not found"
@@ -347,6 +448,7 @@ def serve_clip(video_id, clip_index):
347
  @app.route("/api/save_alignments", methods=["POST"])
348
  @login_required
349
  def save_alignments():
 
350
  try:
351
  data = request.json
352
  if not data or 'video_id' not in data or 'alignments' not in data:
@@ -365,7 +467,7 @@ def save_alignments():
365
  "message": "Alignments saved successfully"
366
  })
367
  except Exception as e:
368
- app.logger.error(f"Error saving alignments: {str(e)}")
369
  return jsonify({
370
  "success": False,
371
  "message": str(e)
@@ -374,6 +476,7 @@ def save_alignments():
374
  @app.route("/api/extract_clips/<video_id>")
375
  @login_required
376
  def extract_clips_for_video(video_id):
 
377
  status = clip_extraction_status.get(video_id, {})
378
  if status.get("percent", 0) < 100:
379
  thread = threading.Thread(target=run_clip_extraction, args=(video_id,))
@@ -386,15 +489,25 @@ def extract_clips_for_video(video_id):
386
  @app.route("/api/clip_progress/<video_id>")
387
  @login_required
388
  def clip_progress(video_id):
 
389
  progress = clip_extraction_status.get(video_id, {"current": 0, "total": 0, "percent": 0})
390
  return jsonify(progress)
391
 
392
  @app.route("/api/transcription_progress/<video_id>")
393
  @login_required
394
  def transcription_progress(video_id):
 
395
  progress = transcription_progress_status.get(video_id, {"status": "not started", "percent": 0})
396
  return jsonify(progress)
397
 
398
  if __name__ == '__main__':
399
- port = int(os.getenv('PORT', 5000))
400
- app.run(host='0.0.0.0', port=port, debug=True)
 
 
 
 
 
 
 
 
 
1
  from flask import Flask, render_template, jsonify, request, send_from_directory, send_file, redirect, url_for, session
2
+ import os, json, threading, time, signal, sys
3
  from datetime import datetime
4
  from extract_signed_segments_from_annotations import ClipExtractor, VideoClip
5
  import logging
 
8
  # Load environment variables
9
  load_dotenv()
10
 
11
+ # Configure logging first
12
+ logging.basicConfig(
13
+ level=logging.INFO,
14
+ format='%(asctime)s - %(name)s - %(levelname)s - %(message)s'
15
+ )
16
+ logger = logging.getLogger(__name__)
17
+
18
+ # Hugging Face specific configuration
19
+ is_hf_space = os.getenv('SPACE_ID') is not None
20
+ if is_hf_space:
21
+ logger.info("Running in Hugging Face Spaces environment")
22
+ # Allow insecure transport for development in HF
23
+ os.environ['OAUTHLIB_INSECURE_TRANSPORT'] = '1'
24
+ # Ensure port is set correctly
25
+ os.environ['PORT'] = '7860'
26
+
27
  app = Flask(__name__)
28
  app.secret_key = os.getenv('SECRET_KEY', 'dev_key_for_testing')
29
+
30
+ # Configure session for HF
31
+ if is_hf_space:
32
+ app.config['SESSION_COOKIE_SECURE'] = False
33
+ app.config['SESSION_COOKIE_HTTPONLY'] = True
34
+ app.config['PERMANENT_SESSION_LIFETIME'] = 86400 # 24 hours
35
 
36
  # Directory paths
37
  VIDEO_DIR = os.path.abspath("data/videos")
 
49
  clip_extraction_status = {}
50
  transcription_progress_status = {}
51
 
52
+ # Graceful shutdown handler
53
+ def graceful_shutdown(signum, frame):
54
+ """Handle graceful shutdown on signals."""
55
+ logger.info(f"Received signal {signum}, shutting down gracefully...")
56
+ # Clean up as needed here
57
+ sys.exit(0)
58
+
59
+ # Register signal handlers
60
+ signal.signal(signal.SIGTERM, graceful_shutdown)
61
+ signal.signal(signal.SIGINT, graceful_shutdown)
62
 
63
  # Login required decorator
64
  def login_required(f):
 
66
  @wraps(f)
67
  def decorated_function(*args, **kwargs):
68
  if 'user' not in session:
69
+ logger.info(f"User not in session, redirecting to login")
70
  return redirect(url_for('login'))
71
  return f(*args, **kwargs)
72
  return decorated_function
 
96
  else:
97
  update_extraction_progress(video_id, 1, 1)
98
  except Exception as e:
99
+ logger.error(f"Error during clip extraction for {video_id}: {str(e)}")
100
  clip_extraction_status[video_id] = {"error": str(e)}
101
 
102
  def run_transcription(video_id):
 
106
 
107
  # Check if transcription already exists and is valid.
108
  if os.path.exists(output_path) and os.path.getsize(output_path) > 0:
109
+ logger.info(f"Using cached transcription for video {video_id}.")
110
  transcription_progress_status[video_id] = {"status": "completed", "percent": 100}
111
  return
112
 
113
  video_path = os.path.join(base_dir, "data", "videos", f"{video_id}.mp4")
114
  transcription_progress_status[video_id] = {"status": "started", "percent": 10}
115
 
116
+ # Check if AWS credentials are available
117
+ if not os.environ.get('AWS_ACCESS_KEY_ID') or not os.environ.get('AWS_SECRET_ACCESS_KEY'):
118
+ logger.warning("AWS credentials not found. Transcription will not work properly.")
119
+ transcription_progress_status[video_id] = {
120
+ "status": "error",
121
+ "percent": 0,
122
+ "message": "AWS credentials missing"
123
+ }
124
+ return
125
+
126
  # Run transcription via the imported function from get_transcription_with_amazon.py
127
  from get_transcription_with_amazon import get_word_timestamps
128
  word_timestamps = get_word_timestamps(video_path)
 
132
 
133
  transcription_progress_status[video_id] = {"status": "completed", "percent": 100}
134
  except Exception as e:
135
+ logger.error(f"Error during transcription for {video_id}: {str(e)}")
136
  transcription_progress_status[video_id] = {"status": "error", "percent": 0, "message": str(e)}
137
 
138
  # Authentication routes
139
  @app.route('/login')
140
  def login():
141
+ """Handle login for both local and HF environments."""
142
+ logger.info(f"Login route called. Headers: {dict(request.headers)}")
143
+
144
+ if is_hf_space:
145
  username = request.headers.get('X-Spaces-Username')
146
+ logger.info(f"Username from headers in login: {username}")
147
+
148
  if username and is_allowed_user(username):
149
  session['user'] = {'name': username, 'is_hf': True}
150
  return redirect(url_for('index'))
151
+ else:
152
+ # Redirect to the HF auth endpoint
153
+ return redirect('/auth')
154
  else:
155
+ # For local development
156
  session['user'] = {'name': 'LocalDeveloper', 'is_mock': True}
157
  return redirect(url_for('index'))
158
 
159
  @app.route('/auth/callback')
160
  def auth_callback():
161
+ """This route will be called by Hugging Face after successful authentication."""
162
+ logger.info(f"Auth callback called. Headers: {dict(request.headers)}")
163
+
164
  if is_hf_space:
165
  # In Hugging Face Spaces, the user info is available in the request headers
166
  username = request.headers.get('X-Spaces-Username')
 
173
 
174
  @app.route('/auth')
175
  def auth():
176
+ """This route handles HF authentication."""
177
+ logger.info(f"Auth route called. Headers: {dict(request.headers)}")
178
+
179
+ # Check for the username in headers before proceeding
180
+ username = request.headers.get('X-Spaces-Username')
181
+ logger.info(f"Username from headers in auth: {username}")
182
+
183
+ if is_hf_space and username and is_allowed_user(username):
184
+ logger.info(f"Setting user in session: {username}")
185
+ session['user'] = {'name': username, 'is_hf': True}
186
+ return redirect(url_for('index'))
187
+ elif not is_hf_space:
188
+ # For local development
189
  session['user'] = {'name': 'LocalDeveloper', 'is_mock': True}
190
+ return redirect(url_for('index'))
191
+ else:
192
+ # For HF with no valid username yet, render a simple page with auth information
193
+ return render_template('error.html', message=
194
+ "Waiting for Hugging Face authentication. If you continue to see this message, "
195
+ "please make sure you're logged into Hugging Face and your username is allowed.")
196
 
197
  @app.before_request
198
  def check_auth():
199
+ """Check authentication before processing requests."""
200
+ # Skip authentication for certain routes and static files
201
+ if request.path in ['/login', '/logout', '/auth', '/auth/callback', '/debug'] or request.path.startswith('/static/'):
202
  return
203
 
204
+ # Log all request paths to help troubleshoot
205
+ logger.debug(f"Request path: {request.path}, User in session: {'user' in session}")
206
+
207
  if is_hf_space:
208
+ # Check for HF username header
209
  username = request.headers.get('X-Spaces-Username')
210
+
211
+ if 'user' in session:
212
+ logger.debug(f"User in session: {session['user']}")
213
+ return
214
+
215
  if username and is_allowed_user(username):
216
+ logger.info(f"Setting user from headers: {username}")
217
+ session['user'] = {'name': username, 'is_hf': True}
 
218
  return
219
+
220
+ # No valid user in session or headers
221
+ logger.info(f"No authenticated user, redirecting to /auth")
222
+ return redirect('/auth')
 
223
  elif 'user' not in session:
224
  return redirect(url_for('login'))
225
 
226
  @app.route('/logout')
227
  def logout():
228
+ """Clear session and redirect to login."""
229
  session.clear() # Clear the entire session
230
  if is_hf_space:
231
  return redirect('/auth/logout')
232
  return redirect(url_for('login'))
233
 
234
+ @app.route('/debug')
235
+ def debug_info():
236
+ """Return debug information."""
237
+ info = {
238
+ "session": dict(session) if session else None,
239
+ "headers": dict(request.headers),
240
+ "is_hf_space": is_hf_space,
241
+ "allowed_users": os.getenv('ALLOWED_USERS', 'Perilon'),
242
+ "app_config": {k: str(v) for k, v in app.config.items()},
243
+ "env_vars": {
244
+ "SPACE_ID": os.getenv('SPACE_ID'),
245
+ "PORT": os.getenv('PORT'),
246
+ "DEBUG": os.getenv('DEBUG'),
247
+ "AWS_KEYS_SET": bool(os.getenv('AWS_ACCESS_KEY_ID')) and bool(os.getenv('AWS_SECRET_ACCESS_KEY'))
248
+ }
249
+ }
250
+ return jsonify(info)
251
+
252
  # Main application routes
253
  @app.route('/')
254
  @login_required
255
  def index():
256
+ """Main entry point, redirects to video selection."""
257
  return redirect(url_for('select_video'))
258
 
259
  @app.route('/select_video')
260
  @login_required
261
  def select_video():
262
+ """Page to select a video for annotation."""
263
  if not os.path.exists(VIDEO_DIR):
264
  return render_template('error.html', message="Video directory not found.")
265
  videos = [f for f in os.listdir(VIDEO_DIR) if f.endswith('.mp4')]
 
269
  @app.route('/player/<video_id>')
270
  @login_required
271
  def player(video_id):
272
+ """Video player page for annotation."""
273
  return render_template('player.html', video_id=video_id, user=session.get('user'))
274
 
275
  @app.route('/videos')
276
  @login_required
277
  def get_videos():
278
+ """API endpoint to get available videos."""
279
  if not os.path.exists(VIDEO_DIR):
280
  return jsonify({'error': 'Video directory not found'}), 404
281
  videos = [f for f in os.listdir(VIDEO_DIR) if f.endswith(('.mp4', '.avi', '.mov'))]
 
286
  @app.route('/video/<path:filename>')
287
  @login_required
288
  def serve_video(filename):
289
+ """Serve a video file."""
290
  if not os.path.exists(os.path.join(VIDEO_DIR, filename)):
291
  return jsonify({'error': 'Video not found'}), 404
292
  return send_from_directory(VIDEO_DIR, filename)
 
294
  @app.route('/save_annotations', methods=['POST'])
295
  @login_required
296
  def save_annotations():
297
+ """Save annotation data."""
298
  data = request.json
299
  if not data or 'video' not in data or 'timestamps' not in data:
300
  return jsonify({'success': False, 'message': 'Invalid data'}), 400
 
313
  @app.route('/get_annotations/<path:video_name>')
314
  @login_required
315
  def get_annotations(video_name):
316
+ """Get annotations for a video."""
317
  annotation_file = os.path.join(ANNOTATIONS_DIR, f"{video_name}_annotations.json")
318
  if not os.path.exists(annotation_file):
319
  return jsonify({'error': 'No annotations found'}), 404
 
324
  @app.route("/alignment/<video_id>")
325
  @login_required
326
  def alignment_mode(video_id):
327
+ """Page for aligning sign language with transcribed text."""
328
  annotation_file = os.path.join(ANNOTATIONS_DIR, f"{video_id}_annotations.json")
329
  if not os.path.exists(annotation_file):
330
  return render_template("error.html", message="No annotations found for this video. Please annotate the video first.")
 
340
  @app.route("/api/transcript/<video_id>")
341
  @login_required
342
  def get_transcript(video_id):
343
+ """Get transcript for a video."""
344
  timestamps_file = os.path.join(WORD_TIMESTAMPS_DIR, f"{video_id}_word_timestamps.json")
345
+ logger.info(f"Attempting to load word timestamps from: {timestamps_file}")
346
  if not os.path.exists(timestamps_file):
347
+ logger.warning(f"Word timestamps file not found: {timestamps_file}")
348
  return jsonify({
349
  "status": "error",
350
  "message": "No word timestamps found for this video"
 
358
  "start": float(item["start_time"]),
359
  "end": float(item["end_time"])
360
  } for item in word_data]
361
+ logger.info(f"Successfully created transcript ({len(full_text)} characters)")
362
  return jsonify({
363
  "status": "success",
364
  "text": full_text,
365
  "words": words_with_times
366
  })
367
  except Exception as e:
368
+ logger.error(f"Error processing word timestamps: {str(e)}")
369
  return jsonify({
370
  "status": "error",
371
  "message": f"Error processing word timestamps: {str(e)}"
 
374
  @app.route("/api/word_timestamps/<video_id>")
375
  @login_required
376
  def get_word_timestamps(video_id):
377
+ """Get word-level timestamps for a video."""
378
  timestamps_file = os.path.join(WORD_TIMESTAMPS_DIR, f"{video_id}_word_timestamps.json")
379
+ logger.info(f"Attempting to load word timestamps from: {timestamps_file}")
380
  if not os.path.exists(timestamps_file):
381
+ logger.warning(f"Word timestamps file not found: {timestamps_file}")
382
  return jsonify({
383
  "status": "error",
384
  "message": "No word timestamps found for this video"
 
386
  try:
387
  with open(timestamps_file, 'r') as f:
388
  word_data = json.load(f)
389
+ logger.info(f"Successfully loaded {len(word_data)} word timestamps")
390
  return jsonify({
391
  "status": "success",
392
  "words": word_data
393
  })
394
  except Exception as e:
395
+ logger.error(f"Error processing word timestamps: {str(e)}")
396
  return jsonify({
397
  "status": "error",
398
  "message": f"Error processing word timestamps: {str(e)}"
 
401
  @app.route("/api/clips/<video_id>")
402
  @login_required
403
  def get_video_clips(video_id):
404
+ """Get clips for a video."""
405
  try:
406
  annotation_file = os.path.join(ANNOTATIONS_DIR, f"{video_id}_annotations.json")
407
  if not os.path.exists(annotation_file):
 
422
  "clips": clips
423
  })
424
  except Exception as e:
425
+ logger.error(f"Error getting clips: {str(e)}")
426
  return jsonify({
427
  "status": "error",
428
  "message": str(e)
 
431
  @app.route("/clip/<video_id>/<int:clip_index>")
432
  @login_required
433
  def serve_clip(video_id, clip_index):
434
+ """Serve a specific clip."""
435
  clip_path = os.path.join(
436
  TEMP_DIR,
437
  f"{video_id}_clip_{clip_index:03d}.mp4"
438
  )
439
+ logger.info(f"Attempting to serve clip: {clip_path}")
440
  if not os.path.exists(clip_path):
441
+ logger.error(f"Clip not found: {clip_path}")
442
  return jsonify({
443
  "status": "error",
444
  "message": "Clip not found"
 
448
  @app.route("/api/save_alignments", methods=["POST"])
449
  @login_required
450
  def save_alignments():
451
+ """Save alignment data."""
452
  try:
453
  data = request.json
454
  if not data or 'video_id' not in data or 'alignments' not in data:
 
467
  "message": "Alignments saved successfully"
468
  })
469
  except Exception as e:
470
+ logger.error(f"Error saving alignments: {str(e)}")
471
  return jsonify({
472
  "success": False,
473
  "message": str(e)
 
476
  @app.route("/api/extract_clips/<video_id>")
477
  @login_required
478
  def extract_clips_for_video(video_id):
479
+ """Extract clips and start transcription for a video."""
480
  status = clip_extraction_status.get(video_id, {})
481
  if status.get("percent", 0) < 100:
482
  thread = threading.Thread(target=run_clip_extraction, args=(video_id,))
 
489
  @app.route("/api/clip_progress/<video_id>")
490
  @login_required
491
  def clip_progress(video_id):
492
+ """Get clip extraction progress."""
493
  progress = clip_extraction_status.get(video_id, {"current": 0, "total": 0, "percent": 0})
494
  return jsonify(progress)
495
 
496
  @app.route("/api/transcription_progress/<video_id>")
497
  @login_required
498
  def transcription_progress(video_id):
499
+ """Get transcription progress."""
500
  progress = transcription_progress_status.get(video_id, {"status": "not started", "percent": 0})
501
  return jsonify(progress)
502
 
503
  if __name__ == '__main__':
504
+ try:
505
+ port = int(os.getenv('PORT', 5000))
506
+ print(f"Starting app on port {port}, debug mode: {app.debug}")
507
+ # Explicitly create the session directory if needed
508
+ os.makedirs('flask_session', exist_ok=True)
509
+ app.run(host='0.0.0.0', port=port, debug=True)
510
+ except Exception as e:
511
+ print(f"Error starting the application: {e}")
512
+ import traceback
513
+ traceback.print_exc()