Bug fixes
Browse files- 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 |
-
|
|
|
|
|
|
|
|
|
|
|
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 |
-
#
|
32 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
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 |
-
|
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 |
-
|
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 |
-
|
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 |
-
|
102 |
-
|
|
|
|
|
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 |
-
|
108 |
-
|
|
|
109 |
else:
|
110 |
-
# For local development
|
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 |
-
|
|
|
|
|
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 |
-
|
130 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
131 |
session['user'] = {'name': 'LocalDeveloper', 'is_mock': True}
|
132 |
-
|
|
|
|
|
|
|
|
|
|
|
133 |
|
134 |
@app.before_request
|
135 |
def check_auth():
|
136 |
-
|
137 |
-
|
|
|
138 |
return
|
139 |
|
140 |
-
#
|
|
|
|
|
141 |
if is_hf_space:
|
|
|
142 |
username = request.headers.get('X-Spaces-Username')
|
|
|
|
|
|
|
|
|
|
|
143 |
if username and is_allowed_user(username):
|
144 |
-
|
145 |
-
|
146 |
-
session['user'] = {'name': username, 'is_hf': True}
|
147 |
return
|
148 |
-
|
149 |
-
#
|
150 |
-
|
151 |
-
|
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 |
-
|
248 |
if not os.path.exists(timestamps_file):
|
249 |
-
|
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 |
-
|
264 |
return jsonify({
|
265 |
"status": "success",
|
266 |
"text": full_text,
|
267 |
"words": words_with_times
|
268 |
})
|
269 |
except Exception as e:
|
270 |
-
|
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 |
-
|
281 |
if not os.path.exists(timestamps_file):
|
282 |
-
|
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 |
-
|
291 |
return jsonify({
|
292 |
"status": "success",
|
293 |
"words": word_data
|
294 |
})
|
295 |
except Exception as e:
|
296 |
-
|
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 |
-
|
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 |
-
|
339 |
if not os.path.exists(clip_path):
|
340 |
-
|
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 |
-
|
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 |
-
|
400 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
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()
|