husseinelsaadi commited on
Commit
81341b4
·
1 Parent(s): fd40bd4
backend/routes/interview_api.py CHANGED
@@ -40,7 +40,7 @@ def start_interview():
40
  # when generating URLs. If audio generation fails (for example because
41
  # network access is unavailable), we simply omit the audio file and allow
42
  # the frontend to fall back to text‑only mode.
43
- audio_dir = os.path.join(current_app.static_folder, "audio")
44
  os.makedirs(audio_dir, exist_ok=True)
45
 
46
  audio_filename = f"q_{uuid.uuid4().hex}.wav"
@@ -49,11 +49,13 @@ def start_interview():
49
  # Generate audio synchronously. The function returns None on error.
50
  audio_out = edge_tts_to_file_sync(question, audio_path)
51
 
52
- response = {"question": question}
 
53
  if audio_out:
54
- # Use url_for to build the URL relative to the configured static folder.
55
- response["audio_url"] = url_for("static", filename=f"audio/{audio_filename}")
56
- return jsonify(response)
 
57
 
58
  @interview_api.route("/transcribe_audio", methods=["POST"])
59
  @login_required
@@ -96,7 +98,7 @@ def process_answer():
96
  next_question = f"Follow‑up question {question_idx + 2}: Can you elaborate on your experience with relevant technologies?"
97
 
98
  # Prepare audio output directory inside the app's static folder
99
- audio_dir = os.path.join(current_app.static_folder, "audio")
100
  os.makedirs(audio_dir, exist_ok=True)
101
 
102
  audio_filename = f"q_{uuid.uuid4().hex}.wav"
 
40
  # when generating URLs. If audio generation fails (for example because
41
  # network access is unavailable), we simply omit the audio file and allow
42
  # the frontend to fall back to text‑only mode.
43
+ audio_dir = "/tmp/audio"
44
  os.makedirs(audio_dir, exist_ok=True)
45
 
46
  audio_filename = f"q_{uuid.uuid4().hex}.wav"
 
49
  # Generate audio synchronously. The function returns None on error.
50
  audio_out = edge_tts_to_file_sync(question, audio_path)
51
 
52
+ from flask import send_file # ⬅ add this at the top if not already imported
53
+
54
  if audio_out:
55
+ return send_file(audio_path, mimetype="audio/wav")
56
+ else:
57
+ return jsonify({"question": question})
58
+
59
 
60
  @interview_api.route("/transcribe_audio", methods=["POST"])
61
  @login_required
 
98
  next_question = f"Follow‑up question {question_idx + 2}: Can you elaborate on your experience with relevant technologies?"
99
 
100
  # Prepare audio output directory inside the app's static folder
101
+ audio_dir = "/tmp/audio"
102
  os.makedirs(audio_dir, exist_ok=True)
103
 
104
  audio_filename = f"q_{uuid.uuid4().hex}.wav"
backend/templates/interview.html CHANGED
@@ -1,5 +1,6 @@
1
  <!DOCTYPE html>
2
  <html lang="en">
 
3
  <head>
4
  <meta charset="UTF-8">
5
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
@@ -347,6 +348,7 @@
347
  opacity: 0;
348
  transform: translateY(20px);
349
  }
 
350
  to {
351
  opacity: 1;
352
  transform: translateY(0);
@@ -354,8 +356,15 @@
354
  }
355
 
356
  @keyframes pulse {
357
- 0%, 100% { transform: scale(1); }
358
- 50% { transform: scale(1.1); }
 
 
 
 
 
 
 
359
  }
360
 
361
  @keyframes ripple {
@@ -363,6 +372,7 @@
363
  transform: scale(1);
364
  opacity: 1;
365
  }
 
366
  100% {
367
  transform: scale(1.4);
368
  opacity: 0;
@@ -370,12 +380,21 @@
370
  }
371
 
372
  @keyframes recordPulse {
373
- 0%, 100% { transform: scale(1); }
374
- 50% { transform: scale(1.05); }
 
 
 
 
 
 
 
375
  }
376
 
377
  @keyframes spin {
378
- to { transform: rotate(360deg); }
 
 
379
  }
380
 
381
  @media (max-width: 768px) {
@@ -390,7 +409,8 @@
390
  font-size: 1.5rem;
391
  }
392
 
393
- .message-bubble, .user-bubble {
 
394
  max-width: 85%;
395
  }
396
 
@@ -404,6 +424,7 @@
404
  }
405
  </style>
406
  </head>
 
407
  <body>
408
  <div class="interview-container">
409
  <div class="header">
@@ -436,7 +457,8 @@
436
  <div class="recording-status" id="recordingStatus">
437
  Click the microphone to record your answer
438
  </div>
439
- <div class="transcript-area" id="transcriptArea" contenteditable="true" placeholder="Your transcribed answer will appear here..."></div>
 
440
  <div class="action-buttons">
441
  <button class="btn btn-primary" id="confirmButton" disabled>
442
  <span>Confirm Answer</span>
@@ -506,12 +528,12 @@
506
  this.micButton.addEventListener('mousedown', () => this.startRecording());
507
  this.micButton.addEventListener('mouseup', () => this.stopRecording());
508
  this.micButton.addEventListener('mouseleave', () => this.stopRecording());
509
-
510
  this.micButton.addEventListener('touchstart', (e) => {
511
  e.preventDefault();
512
  this.startRecording();
513
  });
514
-
515
  this.micButton.addEventListener('touchend', (e) => {
516
  e.preventDefault();
517
  this.stopRecording();
@@ -519,7 +541,7 @@
519
 
520
  this.confirmButton.addEventListener('click', () => this.submitAnswer());
521
  this.retryButton.addEventListener('click', () => this.resetRecording());
522
-
523
  this.transcriptArea.addEventListener('input', () => {
524
  const hasText = this.transcriptArea.textContent.trim().length > 0;
525
  this.confirmButton.disabled = !hasText;
@@ -555,22 +577,32 @@
555
  throw new Error(`HTTP error! status: ${response.status}`);
556
  }
557
 
558
- const data = await response.json();
559
-
560
- if (data.error) {
561
- this.showError(data.error);
562
- return;
 
 
 
 
 
 
 
 
 
 
 
 
563
  }
564
 
565
- this.displayQuestion(data.question, data.audio_url);
566
- this.interviewData.questions.push(data.question);
567
-
568
  } catch (error) {
569
  console.error('Error starting interview:', error);
570
  this.showError('Failed to start interview. Please try again.');
571
  }
572
  }
573
 
 
574
  displayQuestion(question, audioUrl = null) {
575
  // Remove loading message
576
  const loadingMsg = document.getElementById('loadingMessage');
@@ -613,7 +645,7 @@
613
  enableControls() {
614
  this.micButton.disabled = false;
615
  this.recordingStatus.textContent = 'Click and hold to record your answer';
616
-
617
  // Remove talking animation from avatar
618
  const avatars = this.chatArea.querySelectorAll('.ai-avatar');
619
  avatars.forEach(avatar => avatar.classList.remove('talking'));
@@ -643,7 +675,7 @@
643
  this.micButton.classList.add('recording');
644
  this.micIcon.textContent = '🔴';
645
  this.recordingStatus.textContent = 'Recording... Release to stop';
646
-
647
  } catch (error) {
648
  console.error('Error starting recording:', error);
649
  this.recordingStatus.textContent = 'Microphone access denied. Please allow microphone access and try again.';
@@ -678,7 +710,7 @@
678
  }
679
 
680
  const data = await response.json();
681
-
682
  if (data.error) {
683
  this.recordingStatus.textContent = data.error;
684
  return;
@@ -692,7 +724,7 @@
692
  } else {
693
  this.recordingStatus.textContent = 'No speech detected. Please try recording again.';
694
  }
695
-
696
  } catch (error) {
697
  console.error('Error processing recording:', error);
698
  this.recordingStatus.textContent = 'Error processing audio. Please try again.';
@@ -734,29 +766,51 @@
734
  throw new Error(`HTTP error! status: ${response.status}`);
735
  }
736
 
737
- const data = await response.json();
738
-
739
- if (data.error) {
740
- this.showError(data.error);
741
- return;
742
- }
743
 
744
- if (data.success) {
745
  this.interviewData.answers.push(answer);
746
- this.interviewData.evaluations.push(data.evaluation);
 
 
 
747
 
748
- if (data.isComplete) {
749
  this.showInterviewSummary();
750
  } else {
751
  this.currentQuestionIndex++;
752
- this.displayQuestion(data.nextQuestion, data.audioUrl);
753
- this.interviewData.questions.push(data.nextQuestion);
754
  this.resetForNextQuestion();
755
  }
756
  } else {
757
- this.showError('Failed to process answer. Please try again.');
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
758
  }
 
759
 
 
 
 
 
 
760
  } catch (error) {
761
  console.error('Error submitting answer:', error);
762
  this.showError('Connection error. Please try again.');
@@ -794,7 +848,7 @@
794
  this.interviewData.questions.forEach((question, index) => {
795
  const answer = this.interviewData.answers[index] || 'No answer provided';
796
  const evaluation = this.interviewData.evaluations[index] || {};
797
-
798
  summaryHtml += `
799
  <div class="summary-item">
800
  <h4>Question ${index + 1}:</h4>
@@ -818,21 +872,21 @@
818
  const errorDiv = document.createElement('div');
819
  errorDiv.className = 'error-message';
820
  errorDiv.textContent = message;
821
-
822
  // Insert at the top of chat area
823
  this.chatArea.insertBefore(errorDiv, this.chatArea.firstChild);
824
-
825
  // Remove after 5 seconds
826
  setTimeout(() => {
827
  if (errorDiv.parentNode) {
828
  errorDiv.parentNode.removeChild(errorDiv);
829
  }
830
  }, 5000);
831
-
832
  // Also update recording status
833
  this.recordingStatus.textContent = message;
834
  this.recordingStatus.style.color = '#ff4757';
835
-
836
  setTimeout(() => {
837
  this.recordingStatus.style.color = '#666';
838
  }, 3000);
@@ -845,7 +899,7 @@
845
  });
846
 
847
  // Add placeholder attribute support for contenteditable
848
- document.addEventListener('DOMContentLoaded', function() {
849
  const transcriptArea = document.getElementById('transcriptArea');
850
  const placeholder = transcriptArea.getAttribute('placeholder');
851
 
@@ -859,14 +913,14 @@
859
  }
860
  }
861
 
862
- transcriptArea.addEventListener('focus', function() {
863
  if (transcriptArea.textContent === placeholder) {
864
  transcriptArea.textContent = '';
865
  transcriptArea.style.color = '#333';
866
  }
867
  });
868
 
869
- transcriptArea.addEventListener('blur', function() {
870
  if (transcriptArea.textContent.trim() === '') {
871
  transcriptArea.style.color = '#999';
872
  transcriptArea.textContent = placeholder;
@@ -878,4 +932,5 @@
878
  });
879
  </script>
880
  </body>
881
- </html>
 
 
1
  <!DOCTYPE html>
2
  <html lang="en">
3
+
4
  <head>
5
  <meta charset="UTF-8">
6
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
 
348
  opacity: 0;
349
  transform: translateY(20px);
350
  }
351
+
352
  to {
353
  opacity: 1;
354
  transform: translateY(0);
 
356
  }
357
 
358
  @keyframes pulse {
359
+
360
+ 0%,
361
+ 100% {
362
+ transform: scale(1);
363
+ }
364
+
365
+ 50% {
366
+ transform: scale(1.1);
367
+ }
368
  }
369
 
370
  @keyframes ripple {
 
372
  transform: scale(1);
373
  opacity: 1;
374
  }
375
+
376
  100% {
377
  transform: scale(1.4);
378
  opacity: 0;
 
380
  }
381
 
382
  @keyframes recordPulse {
383
+
384
+ 0%,
385
+ 100% {
386
+ transform: scale(1);
387
+ }
388
+
389
+ 50% {
390
+ transform: scale(1.05);
391
+ }
392
  }
393
 
394
  @keyframes spin {
395
+ to {
396
+ transform: rotate(360deg);
397
+ }
398
  }
399
 
400
  @media (max-width: 768px) {
 
409
  font-size: 1.5rem;
410
  }
411
 
412
+ .message-bubble,
413
+ .user-bubble {
414
  max-width: 85%;
415
  }
416
 
 
424
  }
425
  </style>
426
  </head>
427
+
428
  <body>
429
  <div class="interview-container">
430
  <div class="header">
 
457
  <div class="recording-status" id="recordingStatus">
458
  Click the microphone to record your answer
459
  </div>
460
+ <div class="transcript-area" id="transcriptArea" contenteditable="true"
461
+ placeholder="Your transcribed answer will appear here..."></div>
462
  <div class="action-buttons">
463
  <button class="btn btn-primary" id="confirmButton" disabled>
464
  <span>Confirm Answer</span>
 
528
  this.micButton.addEventListener('mousedown', () => this.startRecording());
529
  this.micButton.addEventListener('mouseup', () => this.stopRecording());
530
  this.micButton.addEventListener('mouseleave', () => this.stopRecording());
531
+
532
  this.micButton.addEventListener('touchstart', (e) => {
533
  e.preventDefault();
534
  this.startRecording();
535
  });
536
+
537
  this.micButton.addEventListener('touchend', (e) => {
538
  e.preventDefault();
539
  this.stopRecording();
 
541
 
542
  this.confirmButton.addEventListener('click', () => this.submitAnswer());
543
  this.retryButton.addEventListener('click', () => this.resetRecording());
544
+
545
  this.transcriptArea.addEventListener('input', () => {
546
  const hasText = this.transcriptArea.textContent.trim().length > 0;
547
  this.confirmButton.disabled = !hasText;
 
577
  throw new Error(`HTTP error! status: ${response.status}`);
578
  }
579
 
580
+ const contentType = response.headers.get("Content-Type");
581
+ if (contentType.includes("audio")) {
582
+ const audioBlob = await response.blob();
583
+ const audioUrl = URL.createObjectURL(audioBlob);
584
+
585
+ const fallbackText = "Let's begin. Can you introduce yourself?";
586
+ this.displayQuestion(fallbackText, audioUrl);
587
+ this.interviewData.questions.push(fallbackText);
588
+ } else {
589
+ const data = await response.json();
590
+ if (data.error) {
591
+ this.showError(data.error);
592
+ return;
593
+ }
594
+
595
+ this.displayQuestion(data.question, data.audio_url);
596
+ this.interviewData.questions.push(data.question);
597
  }
598
 
 
 
 
599
  } catch (error) {
600
  console.error('Error starting interview:', error);
601
  this.showError('Failed to start interview. Please try again.');
602
  }
603
  }
604
 
605
+
606
  displayQuestion(question, audioUrl = null) {
607
  // Remove loading message
608
  const loadingMsg = document.getElementById('loadingMessage');
 
645
  enableControls() {
646
  this.micButton.disabled = false;
647
  this.recordingStatus.textContent = 'Click and hold to record your answer';
648
+
649
  // Remove talking animation from avatar
650
  const avatars = this.chatArea.querySelectorAll('.ai-avatar');
651
  avatars.forEach(avatar => avatar.classList.remove('talking'));
 
675
  this.micButton.classList.add('recording');
676
  this.micIcon.textContent = '🔴';
677
  this.recordingStatus.textContent = 'Recording... Release to stop';
678
+
679
  } catch (error) {
680
  console.error('Error starting recording:', error);
681
  this.recordingStatus.textContent = 'Microphone access denied. Please allow microphone access and try again.';
 
710
  }
711
 
712
  const data = await response.json();
713
+
714
  if (data.error) {
715
  this.recordingStatus.textContent = data.error;
716
  return;
 
724
  } else {
725
  this.recordingStatus.textContent = 'No speech detected. Please try recording again.';
726
  }
727
+
728
  } catch (error) {
729
  console.error('Error processing recording:', error);
730
  this.recordingStatus.textContent = 'Error processing audio. Please try again.';
 
766
  throw new Error(`HTTP error! status: ${response.status}`);
767
  }
768
 
769
+ const contentType = response.headers.get("Content-Type");
770
+ if (contentType.includes("audio")) {
771
+ const audioBlob = await response.blob();
772
+ const audioUrl = URL.createObjectURL(audioBlob);
 
 
773
 
774
+ const fallback = `Follow-up question ${this.currentQuestionIndex + 2}: Can you elaborate more?`;
775
  this.interviewData.answers.push(answer);
776
+ this.interviewData.evaluations.push({
777
+ score: "N/A",
778
+ feedback: "No feedback available in fallback mode."
779
+ });
780
 
781
+ if (this.currentQuestionIndex >= 2) {
782
  this.showInterviewSummary();
783
  } else {
784
  this.currentQuestionIndex++;
785
+ this.displayQuestion(fallback, audioUrl);
786
+ this.interviewData.questions.push(fallback);
787
  this.resetForNextQuestion();
788
  }
789
  } else {
790
+ const data = await response.json();
791
+ if (data.success) {
792
+ this.interviewData.answers.push(answer);
793
+ this.interviewData.evaluations.push(data.evaluation);
794
+
795
+ if (data.isComplete) {
796
+ this.showInterviewSummary();
797
+ } else {
798
+ this.currentQuestionIndex++;
799
+ this.displayQuestion(data.nextQuestion, data.audioUrl);
800
+ this.interviewData.questions.push(data.nextQuestion);
801
+ this.resetForNextQuestion();
802
+ }
803
+ } else {
804
+ this.showError('Failed to process answer. Please try again.');
805
+ }
806
  }
807
+
808
 
809
+ if (data.error) {
810
+ this.showError(data.error);
811
+ return;
812
+ }
813
+
814
  } catch (error) {
815
  console.error('Error submitting answer:', error);
816
  this.showError('Connection error. Please try again.');
 
848
  this.interviewData.questions.forEach((question, index) => {
849
  const answer = this.interviewData.answers[index] || 'No answer provided';
850
  const evaluation = this.interviewData.evaluations[index] || {};
851
+
852
  summaryHtml += `
853
  <div class="summary-item">
854
  <h4>Question ${index + 1}:</h4>
 
872
  const errorDiv = document.createElement('div');
873
  errorDiv.className = 'error-message';
874
  errorDiv.textContent = message;
875
+
876
  // Insert at the top of chat area
877
  this.chatArea.insertBefore(errorDiv, this.chatArea.firstChild);
878
+
879
  // Remove after 5 seconds
880
  setTimeout(() => {
881
  if (errorDiv.parentNode) {
882
  errorDiv.parentNode.removeChild(errorDiv);
883
  }
884
  }, 5000);
885
+
886
  // Also update recording status
887
  this.recordingStatus.textContent = message;
888
  this.recordingStatus.style.color = '#ff4757';
889
+
890
  setTimeout(() => {
891
  this.recordingStatus.style.color = '#666';
892
  }, 3000);
 
899
  });
900
 
901
  // Add placeholder attribute support for contenteditable
902
+ document.addEventListener('DOMContentLoaded', function () {
903
  const transcriptArea = document.getElementById('transcriptArea');
904
  const placeholder = transcriptArea.getAttribute('placeholder');
905
 
 
913
  }
914
  }
915
 
916
+ transcriptArea.addEventListener('focus', function () {
917
  if (transcriptArea.textContent === placeholder) {
918
  transcriptArea.textContent = '';
919
  transcriptArea.style.color = '#333';
920
  }
921
  });
922
 
923
+ transcriptArea.addEventListener('blur', function () {
924
  if (transcriptArea.textContent.trim() === '') {
925
  transcriptArea.style.color = '#999';
926
  transcriptArea.textContent = placeholder;
 
932
  });
933
  </script>
934
  </body>
935
+
936
+ </html>