Oviya commited on
Commit
8dec77d
·
1 Parent(s): 8246563

add listen

Browse files
Files changed (2) hide show
  1. listen.py +409 -0
  2. verification.py +2 -0
listen.py ADDED
@@ -0,0 +1,409 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # listen.py
2
+ from flask import Flask, Blueprint, jsonify, send_file, abort, request, send_from_directory
3
+ from flask_cors import CORS
4
+ from moviepy.editor import VideoFileClip
5
+ from google.cloud import speech
6
+ import os
7
+ print(f"GOOGLE_APPLICATION_CREDENTIALS: {os.getenv('GOOGLE_APPLICATION_CREDENTIALS')}")
8
+ import uuid
9
+ import requests
10
+ from pydub import AudioSegment
11
+ import ffmpeg
12
+ import re
13
+ import io # for streaming S3 bytes in HF/AWS mode
14
+
15
+ # Optional (only used in AWS mode)
16
+ try:
17
+ import boto3
18
+ from botocore.exceptions import BotoCoreError, ClientError
19
+ except Exception:
20
+ boto3 = None
21
+ BotoCoreError = ClientError = Exception
22
+
23
+ # ---------- Blueprint ----------
24
+ listen_bp = Blueprint("listen", __name__)
25
+
26
+ # Directories for video, audio, and transcripts (local working paths)
27
+ VIDEO_FOLDER = 'static/videos'
28
+ AUDIO_FOLDER = 'static/audio'
29
+ TRANSCRIPT_FOLDER = 'static/transcripts'
30
+
31
+ # Ensure local working dirs exist
32
+ os.makedirs(VIDEO_FOLDER, exist_ok=True)
33
+ os.makedirs(AUDIO_FOLDER, exist_ok=True)
34
+ os.makedirs(TRANSCRIPT_FOLDER, exist_ok=True)
35
+
36
+ # ---------------- Cohere configuration (migrated to v2 Chat) ----------------
37
+ COHERE_API_KEY = os.getenv("COHERE_API_KEY", "")
38
+ COHERE_API_URL = 'https://api.cohere.com/v2/chat'
39
+ # ---------------------------------------------------------------------------
40
+
41
+ # ---------------------- storage mode helpers ----------------------
42
+ def _is_aws_video_mode() -> bool:
43
+ """
44
+ Switch to S3 on Hugging Face / prod. Local stays on disk.
45
+ """
46
+ if os.getenv("USE_AWS_VIDEO", "0") == "1":
47
+ return True
48
+ if os.getenv("SPACE_ID"): # set on Hugging Face Spaces
49
+ return True
50
+ if os.getenv("ENV", "dev").lower() == "prod":
51
+ return True
52
+ return False
53
+
54
+ def _s3_clients():
55
+ if boto3 is None:
56
+ raise RuntimeError("boto3 is required in AWS video mode but not available")
57
+ region = os.getenv("AWS_DEFAULT_REGION", "eu-north-1")
58
+ s3 = boto3.client("s3", region_name=region)
59
+ return s3
60
+
61
+ def _video_s3_bucket():
62
+ bucket = os.getenv("S3_BUCKET_NAME")
63
+ if not bucket:
64
+ raise RuntimeError("S3_BUCKET_NAME is not set")
65
+ return bucket
66
+
67
+ def _video_s3_key(filename: str) -> str:
68
+ # Prefix under which listen.py stores videos in the same bucket
69
+ prefix = os.getenv("LISTEN_S3_PREFIX", "listen")
70
+ prefix = prefix.strip().strip("/")
71
+ return f"{prefix}/{filename}"
72
+
73
+ # -------------------------------------------------------------------
74
+
75
+ # Google Cloud Speech-to-Text Configuration
76
+ speech_client = speech.SpeechClient()
77
+
78
+ # ------------- Cohere v2 helper (extract text from chat response) -------------
79
+ def _extract_text_v2(resp_json: dict) -> str:
80
+ """
81
+ Cohere v2 /chat returns:
82
+ { "message": { "content": [ { "type": "text", "text": "..." }, ... ] } }
83
+ This pulls the first text block.
84
+ """
85
+ msg = resp_json.get("message", {})
86
+ content = msg.get("content", [])
87
+ for block in content:
88
+ if isinstance(block, dict) and block.get("type") == "text":
89
+ text = (block.get("text") or "").strip()
90
+ if text:
91
+ return text
92
+ return ""
93
+ # -----------------------------------------------------------------------------
94
+
95
+ # Convert video to audio
96
+ def convert_video_to_audio(video_path, audio_path):
97
+ try:
98
+ # Using moviepy to extract audio from video
99
+ video = VideoFileClip(video_path)
100
+ video.audio.write_audiofile(audio_path, codec='mp3')
101
+ return audio_path
102
+ except Exception as e:
103
+ print(f"Error converting video to audio: {str(e)}")
104
+ return None
105
+
106
+ # Re-encode MP3 to ensure proper format
107
+ def reencode_mp3(input_audio_path, output_audio_path):
108
+ try:
109
+ # Using pydub to convert and re-encode MP3 (ensuring correct encoding)
110
+ audio = AudioSegment.from_mp3(input_audio_path)
111
+ audio.export(output_audio_path, format="mp3", codec="libmp3lame", parameters=["-q:a", "0"])
112
+ return output_audio_path
113
+ except Exception as e:
114
+ print(f"Error re-encoding MP3: {str(e)}")
115
+ return None
116
+
117
+ # Helper function to convert audio to the proper MP3 encoding if necessary
118
+ def convert_audio_to_mp3(input_file_path, output_file_path):
119
+ """
120
+ Converts the audio file to a valid MP3 format with proper encoding.
121
+ """
122
+ try:
123
+ ffmpeg.input(input_file_path).output(output_file_path, acodec='libmp3lame', audio_bitrate='128k').run()
124
+ return True
125
+ except Exception as e:
126
+ print(f"Error during audio conversion: {e}")
127
+ return False
128
+
129
+ # Function to compress audio dynamically
130
+ def compress_audio(input_file_path, output_file_path, target_bitrate="128k"):
131
+ audio = AudioSegment.from_file(input_file_path)
132
+ audio.export(output_file_path, format="mp3", bitrate=target_bitrate)
133
+ return output_file_path
134
+
135
+ # ---------------------------- Routes (Blueprint) ----------------------------
136
+
137
+ @listen_bp.route('/', methods=['GET'])
138
+ def home():
139
+ return "Welcome to the Flask app! The server is running."
140
+
141
+ @listen_bp.route('/videos', methods=['GET'])
142
+ def list_videos():
143
+ """
144
+ List available videos for users to watch.
145
+ """
146
+ # If you maintain a VIDEOS list elsewhere, return it here.
147
+ # Returning empty list so the endpoint stays valid.
148
+ return jsonify([]), 200
149
+
150
+ @listen_bp.route('/videos/<filename>')
151
+ def serve_video(filename):
152
+ """
153
+ Local: serve file from disk.
154
+ HF/AWS: fetch object from S3 and stream bytes (no redirect).
155
+ """
156
+ if _is_aws_video_mode():
157
+ try:
158
+ s3 = _s3_clients()
159
+ bucket = _video_s3_bucket()
160
+ key = _video_s3_key(filename)
161
+ obj = s3.get_object(Bucket=bucket, Key=key)
162
+ data = obj["Body"].read()
163
+ return send_file(
164
+ io.BytesIO(data),
165
+ mimetype="video/mp4",
166
+ download_name=filename,
167
+ as_attachment=False
168
+ )
169
+ except (BotoCoreError, ClientError, Exception) as e:
170
+ print(f"S3 fetch failed for {filename}: {e}")
171
+ abort(404)
172
+
173
+ # Local
174
+ video_path = os.path.join(VIDEO_FOLDER, filename)
175
+ if not os.path.exists(video_path):
176
+ print(f"Video file not found: {filename}")
177
+ abort(404)
178
+
179
+ return send_file(video_path, mimetype='video/mp4')
180
+
181
+ @listen_bp.route('/upload-video', methods=['POST'])
182
+ def upload_video():
183
+ """
184
+ Local: save to static/videos.
185
+ HF/AWS: upload to S3 (no local original).
186
+ """
187
+ print("Received upload request.")
188
+
189
+ if 'video' not in request.files:
190
+ print("No video file provided in the request.")
191
+ return jsonify({'error': 'No video file provided'}), 400
192
+
193
+ video = request.files['video']
194
+ if video.filename == '':
195
+ print("Empty filename detected.")
196
+ return jsonify({'error': 'No selected file'}), 400
197
+
198
+ try:
199
+ filename = f"{uuid.uuid4()}.mp4"
200
+
201
+ if _is_aws_video_mode():
202
+ try:
203
+ s3 = _s3_clients()
204
+ bucket = _video_s3_bucket()
205
+ key = _video_s3_key(filename)
206
+ s3.put_object(
207
+ Bucket=bucket,
208
+ Key=key,
209
+ Body=video.stream.read(),
210
+ ContentType="video/mp4"
211
+ )
212
+ print(f"Uploaded to S3: s3://{bucket}/{key}")
213
+ except (BotoCoreError, ClientError, Exception) as e:
214
+ print(f"S3 upload error: {e}")
215
+ return jsonify({'error': 'Failed to upload to S3'}), 500
216
+ else:
217
+ # Save locally
218
+ video_path = os.path.join(VIDEO_FOLDER, filename)
219
+ print(f"Saving video: {filename}")
220
+ video.save(video_path)
221
+ print(f"Video saved successfully at {video_path}")
222
+
223
+ return jsonify({'message': 'Video uploaded successfully!', 'filename': filename}), 200
224
+
225
+ except Exception as e:
226
+ print(f"Error saving video: {str(e)}")
227
+ return jsonify({'error': 'Failed to save video'}), 500
228
+
229
+ @listen_bp.route('/generate-questions-dynamicvideo', methods=['POST'])
230
+ def generate_questions():
231
+ try:
232
+ data = request.json
233
+ video_filename = data.get('filename')
234
+
235
+ if not video_filename:
236
+ print("Error: No filename provided in request.")
237
+ return jsonify({"error": "Filename is required"}), 400
238
+
239
+ # Resolve a local readable path for processing
240
+ video_path = os.path.join(VIDEO_FOLDER, video_filename)
241
+
242
+ if _is_aws_video_mode():
243
+ # Download object bytes to a local working file path
244
+ try:
245
+ s3 = _s3_clients()
246
+ bucket = _video_s3_bucket()
247
+ key = _video_s3_key(video_filename)
248
+ obj = s3.get_object(Bucket=bucket, Key=key)
249
+ data_bytes = obj["Body"].read()
250
+ with open(video_path, "wb") as f:
251
+ f.write(data_bytes)
252
+ except (BotoCoreError, ClientError, Exception) as e:
253
+ print(f"S3 download error for {video_filename}: {e}")
254
+ return jsonify({"error": "Video file not found"}), 404
255
+ else:
256
+ if not os.path.exists(video_path):
257
+ print(f"Error: Video file {video_filename} not found at {video_path}")
258
+ return jsonify({"error": "Video file not found"}), 404
259
+
260
+ print(f"Processing video: {video_filename}")
261
+
262
+ # Convert video to audio
263
+ audio_filename = f"{uuid.uuid4()}.mp3"
264
+ audio_path = os.path.join(AUDIO_FOLDER, audio_filename)
265
+
266
+ if not convert_video_to_audio(video_path, audio_path):
267
+ print("Error: Video to audio conversion failed.")
268
+ return jsonify({"error": "Failed to convert video to audio"}), 500
269
+
270
+ # Transcribe audio using Google Cloud Speech-to-Text
271
+ with open(audio_path, 'rb') as audio_file:
272
+ audio_content = audio_file.read()
273
+
274
+ audio = speech.RecognitionAudio(content=audio_content)
275
+ config = speech.RecognitionConfig(
276
+ encoding=speech.RecognitionConfig.AudioEncoding.MP3,
277
+ sample_rate_hertz=16000,
278
+ language_code="en-US",
279
+ )
280
+
281
+ response = speech_client.recognize(config=config, audio=audio)
282
+ transcripts = [result.alternatives[0].transcript for result in response.results]
283
+
284
+ if not transcripts:
285
+ print("Error: No transcription results found.")
286
+ return jsonify({"error": "No transcription results found"}), 500
287
+
288
+ transcription_text = " ".join(transcripts)
289
+ print(f"Transcription successful: {transcription_text[:200]}...") # Print first 200 chars
290
+
291
+ # ---------------- Cohere v2 Chat call (minimal change) ----------------
292
+ headers = {
293
+ "Authorization": f"Bearer {COHERE_API_KEY}",
294
+ "Content-Type": "application/json"
295
+ }
296
+
297
+ prompt_text = (
298
+ "Generate exactly three multiple-choice questions based on this text:\n"
299
+ f"{transcription_text}\n\n"
300
+ "Rules:\n"
301
+ "- Each question starts with a number and a period (e.g., 1.)\n"
302
+ "- Each question has exactly four options labeled A., B., C., and D.\n"
303
+ "- After the options, add a line 'Correct answer: <A|B|C|D>'\n"
304
+ "- Output plain text only."
305
+ )
306
+
307
+ cohere_payload = {
308
+ "model": "command-r-08-2024",
309
+ "messages": [
310
+ {"role": "user", "content": prompt_text}
311
+ ],
312
+ "max_tokens": 300,
313
+ "temperature": 0.9
314
+ }
315
+
316
+ cohere_response = requests.post(
317
+ COHERE_API_URL,
318
+ json=cohere_payload,
319
+ headers=headers,
320
+ timeout=60
321
+ )
322
+
323
+ if cohere_response.status_code != 200:
324
+ print(f"Error: Cohere API response failed: {cohere_response.text}")
325
+ return jsonify({"error": "Failed to generate questions"}), 500
326
+
327
+ raw_text = _extract_text_v2(cohere_response.json())
328
+ if not raw_text:
329
+ print("Error: No questions text returned by Cohere Chat API.")
330
+ return jsonify({"error": "No questions generated"}), 500
331
+ # ---------------------------------------------------------------------
332
+
333
+ # Extract raw text and parse questions
334
+ structured_questions = parse_questions(raw_text)
335
+
336
+ return jsonify({"questions": structured_questions}), 200
337
+
338
+ except Exception as e:
339
+ print(f"Critical Error: {e}")
340
+ return jsonify({"error": "An error occurred while generating questions"}), 500
341
+
342
+ def parse_questions(response_text):
343
+ # Split the text into individual question blocks
344
+ question_blocks = response_text.split("\n\n")
345
+ questions = []
346
+
347
+ # Process each question block
348
+ for block in question_blocks:
349
+ print("\nProcessing Block:", block) # Debug: Log each question block
350
+
351
+ # Split the block into lines
352
+ lines = block.strip().split("\n")
353
+ print("Split Lines:", lines) # Debug: Log split lines of the block
354
+
355
+ # Ensure the block contains a question
356
+ if len(lines) < 2:
357
+ print("Skipping Invalid Block") # Debug: Log invalid blocks
358
+ continue
359
+
360
+ # Extract the question text
361
+ question_line = lines[0]
362
+ question_text = question_line.split(". ", 1)[1] if ". " in question_line else question_line
363
+ print("Question Text:", question_text) # Debug: Log extracted question text
364
+
365
+ # Extract the options and find the correct answer
366
+ options = []
367
+ correct_answer_letter = None
368
+ for line in lines[1:]:
369
+ line = line.strip()
370
+ # Handle A., B., C., D. and also a) / A) formats
371
+ if line.lower().startswith("correct answer:"):
372
+ correct_answer_letter = line.split(":")[-1].strip()
373
+ continue
374
+ match = re.match(r"^(?:[a-dA-D][\).]?\s)?(.+)$", line)
375
+ if match:
376
+ option_text = match.group(1).strip()
377
+ # We already handled "Correct answer:" above, so only options get appended
378
+ if not line.lower().startswith("correct answer:"):
379
+ options.append(option_text)
380
+
381
+ print("Extracted Options:", options) # Debug: Log extracted options
382
+ print("Correct Answer Letter:", correct_answer_letter) # Debug: Log the correct answer letter
383
+
384
+ # Map the correct answer text
385
+ correct_answer_text = ""
386
+ if correct_answer_letter:
387
+ option_index = ord(correct_answer_letter.upper()) - ord('A') # Convert 'A'→0, 'B'→1, etc.
388
+ if 0 <= option_index < len(options):
389
+ correct_answer_text = options[option_index]
390
+ print("Mapped Correct Answer Text:", correct_answer_text) # Debug: Log mapped answer
391
+
392
+ # Append the parsed question to the list
393
+ if question_text and options:
394
+ questions.append({
395
+ "question": question_text,
396
+ "options": options,
397
+ "answer": correct_answer_text # Use the full answer text
398
+ })
399
+
400
+ print("\nFinal Questions:", questions) # Debug: Log final parsed questions
401
+ return questions
402
+
403
+ # ---------- Standalone (local testing) ----------
404
+ if __name__ == '__main__':
405
+ app = Flask(__name__)
406
+ CORS(app)
407
+ app.config["COHERE_API_KEY"] = os.getenv("COHERE_API_KEY", COHERE_API_KEY)
408
+ app.register_blueprint(listen_bp, url_prefix='')
409
+ app.run(host='0.0.0.0', port=5012, debug=True)
verification.py CHANGED
@@ -310,12 +310,14 @@ from reading import reading_bp
310
  from writting import writting_bp # match the exact file name on Linux
311
  from vocabularyBuilder import vocab_bp
312
  from findingword import finding_bp
 
313
  app.register_blueprint(movie_bp, url_prefix="/media")
314
  app.register_blueprint(questions_bp, url_prefix="/media")
315
  app.register_blueprint(reading_bp, url_prefix="/media")
316
  app.register_blueprint(writting_bp, url_prefix="/media")
317
  app.register_blueprint(vocab_bp, url_prefix="/media")
318
  app.register_blueprint(finding_bp, url_prefix="/media")
 
319
  # app.register_blueprint(questions_bp, url_prefix="/media") # <-- add this
320
  # ------------------------------------------------------------------------------
321
  # Local run (Gunicorn will import `verification:app` on Spaces)
 
310
  from writting import writting_bp # match the exact file name on Linux
311
  from vocabularyBuilder import vocab_bp
312
  from findingword import finding_bp
313
+ from listen import listen_bp
314
  app.register_blueprint(movie_bp, url_prefix="/media")
315
  app.register_blueprint(questions_bp, url_prefix="/media")
316
  app.register_blueprint(reading_bp, url_prefix="/media")
317
  app.register_blueprint(writting_bp, url_prefix="/media")
318
  app.register_blueprint(vocab_bp, url_prefix="/media")
319
  app.register_blueprint(finding_bp, url_prefix="/media")
320
+ app.register_blueprint(listen_bp, url_prefix="/media")
321
  # app.register_blueprint(questions_bp, url_prefix="/media") # <-- add this
322
  # ------------------------------------------------------------------------------
323
  # Local run (Gunicorn will import `verification:app` on Spaces)