usmanyousaf commited on
Commit
d98e7f6
·
verified ·
1 Parent(s): f230d54

Upload 3 files

Browse files
Files changed (3) hide show
  1. app.py +124 -0
  2. templates/chose voice page.html +304 -0
  3. templates/index.html +514 -0
app.py ADDED
@@ -0,0 +1,124 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+ import time
3
+ import uuid
4
+ from flask import Flask, request, render_template, jsonify, send_from_directory, session
5
+ from dotenv import load_dotenv
6
+ from groq import Groq
7
+ from deepgram import DeepgramClient, SpeakOptions
8
+
9
+ # Load environment variables
10
+ load_dotenv()
11
+
12
+ # Set up the Groq client
13
+ os.environ["GROQ_API_KEY"] = "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"
14
+ client = Groq(api_key=os.environ["GROQ_API_KEY"])
15
+
16
+ # Set up Deepgram client
17
+ deepgram = DeepgramClient("f93a923d27690bf29e66d045d5143c7bee1c76e3")
18
+
19
+ # Flask app
20
+ app = Flask(__name__)
21
+ app.secret_key = os.urandom(24)
22
+
23
+ # Store conversation history
24
+ conversation_history = []
25
+
26
+ # Synthesize therapist response to speech
27
+ def synthesize_audio(text, model="aura-asteria-en"):
28
+ try:
29
+ options = SpeakOptions(model=model)
30
+ audio_folder = "static/audio"
31
+ if not os.path.exists(audio_folder):
32
+ os.makedirs(audio_folder)
33
+
34
+ # Generate a unique filename using timestamp and UUID
35
+ unique_filename = f"therapist_response_{int(time.time())}_{uuid.uuid4().hex}.mp3"
36
+ filename = os.path.join(audio_folder, unique_filename)
37
+
38
+ options = SpeakOptions(
39
+ model=model
40
+ )
41
+ # Synthesize the response and save it to the file
42
+ deepgram.speak.v("1").save(filename, {"text": text}, options)
43
+ return filename
44
+ except Exception as e:
45
+ raise ValueError(f"Speech synthesis failed: {str(e)}")
46
+
47
+ @app.route('/final_audio/<path:filename>')
48
+ def serve_audio(filename):
49
+ return send_from_directory('final_audio', filename)
50
+
51
+ @app.route('/')
52
+ def choose_voice():
53
+ return render_template('chose voice page.html')
54
+
55
+ @app.route('/start-chat')
56
+ def start_chat():
57
+ selected_voice = request.args.get('voice', 'aura-asteria-en')
58
+ session['selected_voice'] = selected_voice
59
+ return render_template('index.html')
60
+
61
+
62
+ @app.route('/process', methods=['POST'])
63
+ def process_audio():
64
+ global conversation_history
65
+
66
+ # Step 1: Accept audio input
67
+ audio_data = request.files.get('audio_data') # Retrieve audio blob from client
68
+ if not audio_data:
69
+ return jsonify({'error': 'No audio file uploaded'}), 400
70
+
71
+ try:
72
+ # Step 2: Transcribe the audio using Groq Whisper
73
+ transcription = client.audio.transcriptions.create(
74
+ file=('recording.wav', audio_data.read()),
75
+ model="whisper-large-v3",
76
+ prompt="Transcribe the audio accurately.",
77
+ response_format="text"
78
+ )
79
+ user_input = transcription.strip()
80
+ if not user_input:
81
+ return jsonify({'error': 'No valid transcription from audio'}), 400
82
+
83
+ # Append user input to conversation history
84
+ conversation_history.append({"role": "user", "content": user_input})
85
+
86
+ # Step 3: Generate therapist response
87
+ fixed_prompt = [
88
+ {"role": "system", "content": """
89
+ You are an AI therapist named Virtual Therapist, designed to provide conversational support and mental health guidance in a clear, concise, and professional manner. Your responses should:
90
+ 1. Be short and to the point.
91
+ 2. Maintain a professional tone.
92
+ 3. Encourage open dialogue.
93
+ 4. Provide solutions or suggestions where appropriate.
94
+ 5. Stay respectful and non-judgmental.
95
+ 6. Avoid lengthy explanations.
96
+ """}
97
+ ]
98
+
99
+ conversation_history_with_prompt = fixed_prompt + conversation_history
100
+
101
+ response = client.chat.completions.create(
102
+ messages=conversation_history_with_prompt,
103
+ model="llama3-8b-8192"
104
+ )
105
+ assistant_reply = response.choices[0].message.content
106
+
107
+ # Append assistant reply to conversation history
108
+ conversation_history.append({"role": "assistant", "content": assistant_reply})
109
+
110
+ # Step 4: Synthesize therapist response to speech
111
+ audio_file = synthesize_audio(assistant_reply)
112
+ audio_url = f"{request.url_root}static/audio/{os.path.basename(audio_file)}"
113
+
114
+ return jsonify({
115
+ 'transcription': user_input,
116
+ 'response': assistant_reply,
117
+ 'audioUrl': audio_url
118
+ })
119
+
120
+ except Exception as e:
121
+ return jsonify({'error': str(e)}), 500
122
+
123
+ if __name__ == '__main__':
124
+ app.run(debug=True)
templates/chose voice page.html ADDED
@@ -0,0 +1,304 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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">
6
+ <title>Choose a Voice</title>
7
+ <style>
8
+ body {
9
+ font-family: Arial, sans-serif;
10
+ background-color: #1d1d1d;
11
+ color: white;
12
+ text-align: center;
13
+ display: flex;
14
+ flex-direction: column;
15
+ justify-content: center;
16
+ align-items: center;
17
+ height: 100vh;
18
+ margin: 0;
19
+ overflow: hidden;
20
+ }
21
+ h1 {
22
+ font-size: 2rem;
23
+ color: #ffffff;
24
+ margin-bottom: 40px;
25
+ }
26
+ .voice-container {
27
+ display: flex;
28
+ flex-direction: column;
29
+ align-items: center;
30
+ gap: 30px;
31
+ }
32
+ .voice-circle {
33
+ width: 200px;
34
+ height: 200px;
35
+ border-radius: 50%;
36
+ background: radial-gradient(circle, rgba(255,255,255,0.1), rgba(0,0,0,0.5));
37
+ display: flex;
38
+ justify-content: center;
39
+ align-items: center;
40
+ font-size: 1.2rem;
41
+ color: #ffffff;
42
+ position: relative;
43
+ animation: float 5s infinite ease-in-out;
44
+ }
45
+ .voice-circle:before {
46
+ content: "";
47
+ position: absolute;
48
+ top: -10px;
49
+ right: -10px;
50
+ bottom: -10px;
51
+ left: -10px;
52
+ background: radial-gradient(circle, rgba(255,255,255,0.2), transparent);
53
+ border-radius: 50%;
54
+ animation: rotate 10s infinite linear;
55
+ }
56
+ .voice-circle.playing:before {
57
+ animation: none;
58
+ }
59
+ .voice-circle.playing:after {
60
+ content: "";
61
+ position: absolute;
62
+ width: 0;
63
+ height: 0;
64
+ border-radius: 50%;
65
+ background: rgba(255, 255, 255, 0.3);
66
+ box-shadow: 0 0 20px rgba(255, 255, 255, 0.5), 0 0 40px rgba(255, 255, 255, 0.3);
67
+ animation: pulse 1.5s infinite ease-out;
68
+ }
69
+ @keyframes pulse {
70
+ 0% {
71
+ width: 0;
72
+ height: 0;
73
+ opacity: 0.5;
74
+ }
75
+ 100% {
76
+ width: 350px;
77
+ height: 350px;
78
+ opacity: 0;
79
+ }
80
+ }
81
+ @keyframes float {
82
+ 0%, 100% {
83
+ transform: translateY(0);
84
+ }
85
+ 50% {
86
+ transform: translateY(-20px);
87
+ }
88
+ }
89
+ @keyframes rotate {
90
+ 0% {
91
+ transform: rotate(0deg);
92
+ }
93
+ 100% {
94
+ transform: rotate(360deg);
95
+ }
96
+ }
97
+ .voice-slider {
98
+ display: flex;
99
+ justify-content: center;
100
+ align-items: center;
101
+ gap: 10px;
102
+ margin-top: 20px;
103
+ color: #aaaaaa;
104
+ }
105
+ .voice-item {
106
+ text-align: center;
107
+ opacity: 0.3;
108
+ transition: opacity 0.3s;
109
+ flex: 1;
110
+ white-space: nowrap;
111
+ }
112
+ .voice-item.active {
113
+ opacity: 1;
114
+ color: white;
115
+ }
116
+ .voice-item strong {
117
+ display: inline;
118
+ font-size: 1.2rem;
119
+ }
120
+ .voice-item span {
121
+ display: inline;
122
+ margin-left: 10px;
123
+ font-size: 0.9rem;
124
+ }
125
+ .voice-slider button {
126
+ background: none;
127
+ border: none;
128
+ font-size: 2rem;
129
+ cursor: pointer;
130
+ color: #ffffff;
131
+ transition: color 0.3s;
132
+ }
133
+ .voice-slider button.disabled {
134
+ color: #555555;
135
+ cursor: default;
136
+ }
137
+ .voice-controls {
138
+ margin-top: 50px;
139
+ display: flex;
140
+ flex-direction: column;
141
+ gap: 10px;
142
+ }
143
+ button {
144
+ padding: 10px 20px;
145
+ font-size: 1rem;
146
+ border: none;
147
+ border-radius: 30px;
148
+ cursor: pointer;
149
+ transition: background-color 0.3s;
150
+ font-weight: bold;
151
+ }
152
+ button:hover {
153
+ background-color: #3b3838;
154
+ }
155
+ .start-chat {
156
+ background-color: #ffffff;
157
+ color: #1d1d1d;
158
+ padding: 15px 40px;
159
+ border-radius: 50px;
160
+ font-size: 1rem;
161
+ }
162
+ .cancel {
163
+ background-color: #252323;
164
+ color: #ffffff;
165
+ padding: 15px 40px;
166
+ border-radius: 50px;
167
+ font-size: 1rem;
168
+ }
169
+ button:hover {
170
+ background-color: #3b3838;
171
+ color: rgb(255, 255, 255);
172
+ }
173
+ </style>
174
+ </head>
175
+ <body>
176
+ <h1>Choose a Voice</h1>
177
+ <div class="voice-container">
178
+ <div class="voice-circle" id="voice-circle">
179
+ <div id="voice-description" style="font-size: 1.rem; color: #ffffff;"></div>
180
+ </div>
181
+ <div class="voice-slider">
182
+ <div class="voice-item" id="prev-voice"></div>
183
+ <button id="prev-button" onclick="prevVoice()">&#8249;</button>
184
+ <div class="voice-item active" id="current-voice"></div>
185
+ <button id="next-button" onclick="nextVoice()">&#8250;</button>
186
+ <div class="voice-item" id="next-voice"></div>
187
+ </div>
188
+ </div>
189
+ <div class="voice-controls">
190
+ <button class="start-chat" onclick="startChat()">Start new chat</button>
191
+ <button class="cancel" onclick="cancel()">Cancel</button>
192
+ </div>
193
+
194
+ <script>
195
+ const voices = [
196
+ { id: "aura-luna-en", displayName: "Luna", description: "English (US) - Female", file: "Luna - English (US) - Female.wav" },
197
+ { id: "aura-athena-en", displayName: "Athena", description: "English (UK) - Female", file: "Athena - English (UK) - Female.wav" },
198
+ { id: "aura-angus-en", displayName: "Angus", description: "English (Ireland) - Male", file: "Angus - English (Ireland) - Male.wav" },
199
+ { id: "aura-stella-en", displayName: "Stella", description: "English (US) - Female", file: "Stella - English (US) - Female.wav" },
200
+ { id: "aura-zeus-en", displayName: "Zeus", description: "English (US) - Male", file: "Zeus - English (US) - Male.wav" },
201
+ { id: "aura-orion-en", displayName: "Orion", description: "English (US) - Male", file: "Orion - English (US) - Male.mp3" },
202
+ { id: "aura-hera-en", displayName: "Hera", description: "English (US) - Female", file: "Hera - English (US) - Female.wav" },
203
+ { id: "aura-arcas-en", displayName: "Arcas", description: "English (US) - Male", file: "Arcas - English (US) - Male.mp3" },
204
+ { id: "aura-perseus-en", displayName: "Perseus", description: "English (US) - Male", file: "Perseus - English (US) - Male.wav" },
205
+ { id: "aura-helios-en", displayName: "Helios", description: "English (UK) - Male", file: "Helios - English (UK) - Male.wav" },
206
+ { id: "aura-orpheus-en", displayName: "Orpheus", description: "English (US) - Male", file: "Orpheus - English (US) - Male.wav" }
207
+ ];
208
+
209
+
210
+ let currentVoiceIndex = 5;
211
+ let audioElement = new Audio();
212
+ let playTimeout = null;
213
+
214
+ function updateVoiceDisplay() {
215
+ const prevVoice = document.getElementById("prev-voice");
216
+ const currentVoice = document.getElementById("current-voice");
217
+ const nextVoice = document.getElementById("next-voice");
218
+ const voiceDescription = document.getElementById("voice-description");
219
+ const prevButton = document.getElementById("prev-button");
220
+ const nextButton = document.getElementById("next-button");
221
+
222
+ if (currentVoiceIndex > 0) {
223
+ prevVoice.innerHTML = `<strong>${voices[currentVoiceIndex - 1].displayName}</strong><br><span>${voices[currentVoiceIndex - 1].description}</span>`;
224
+ prevVoice.style.opacity = "0.3";
225
+ } else {
226
+ prevVoice.innerHTML = "";
227
+ prevVoice.style.opacity = "0";
228
+ }
229
+
230
+ currentVoice.innerHTML = `<strong>${voices[currentVoiceIndex].displayName}</strong><br><span>${voices[currentVoiceIndex].description}</span>`;
231
+ currentVoice.classList.add("active");
232
+
233
+ if (currentVoiceIndex < voices.length - 1) {
234
+ nextVoice.innerHTML = `<strong>${voices[currentVoiceIndex + 1].displayName}</strong><br><span>${voices[currentVoiceIndex + 1].description}</span>`;
235
+ nextVoice.style.opacity = "0.3";
236
+ } else {
237
+ nextVoice.innerHTML = "";
238
+ nextVoice.style.opacity = "0";
239
+ }
240
+
241
+ const descParts = voices[currentVoiceIndex].description.split('-');
242
+ const gender = descParts[descParts.length - 1].trim();
243
+ voiceDescription.textContent = gender;
244
+
245
+ prevButton.classList.toggle("disabled", currentVoiceIndex === 0);
246
+ nextButton.classList.toggle("disabled", currentVoiceIndex === voices.length - 1);
247
+
248
+ // Schedule audio playback for the current voice after 1-2 seconds
249
+ setTimeout(playSelectedVoiceAudio, 1500);
250
+ }
251
+
252
+
253
+ function prevVoice() {
254
+ if (currentVoiceIndex > 0) {
255
+ currentVoiceIndex--;
256
+ updateVoiceDisplay();
257
+ }
258
+ }
259
+
260
+ function nextVoice() {
261
+ if (currentVoiceIndex < voices.length - 1) {
262
+ currentVoiceIndex++;
263
+ updateVoiceDisplay();
264
+ }
265
+ }
266
+
267
+ function startChat() {
268
+ const voice = voices[currentVoiceIndex].id;
269
+ window.location.href = '/start-chat?voice=' + encodeURIComponent(voice);
270
+ }
271
+
272
+ function cancel() {
273
+ alert("Chat canceled");
274
+ }
275
+
276
+ function scheduleAudioPlayback() {
277
+ if (playTimeout) {
278
+ clearTimeout(playTimeout);
279
+ }
280
+
281
+ audioElement.pause();
282
+ audioElement.currentTime = 0;
283
+
284
+ playTimeout = setTimeout(playSelectedVoiceAudio, 1000);
285
+ }
286
+
287
+ function playSelectedVoiceAudio() {
288
+ const voiceCircle = document.getElementById("voice-circle");
289
+ const voice = voices[currentVoiceIndex];
290
+ audioElement.src = `../final_audio/${voice.file}`;
291
+ voiceCircle.classList.add("playing");
292
+ audioElement.play().catch(err => {
293
+ console.error("Audio playback failed:", err);
294
+ });
295
+
296
+ audioElement.onended = () => {
297
+ voiceCircle.classList.remove("playing");
298
+ };
299
+ }
300
+
301
+ updateVoiceDisplay();
302
+ </script>
303
+ </body>
304
+ </html>
templates/index.html ADDED
@@ -0,0 +1,514 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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">
6
+ <title>Virtual Therapist</title>
7
+
8
+ <style>
9
+ /* ----- Styles from index.html ----- */
10
+ body {
11
+ font-family: Arial, sans-serif;
12
+ background-color: #1d1d1d;
13
+ color: white;
14
+ text-align: center;
15
+ display: flex;
16
+ flex-direction: column;
17
+ justify-content: center;
18
+ align-items: center;
19
+ height: 100vh;
20
+ margin: 0;
21
+ overflow: hidden;
22
+ padding: 0 20px; /* Ensure text is centered with proper spacing */
23
+ }
24
+ h1 {
25
+ font-size: 2rem;
26
+ color: #ffffff;
27
+ margin-bottom: 40px;
28
+ }
29
+ h6 {
30
+ font-size: 0.7rem;
31
+ color: #ffffff;
32
+ margin-bottom: 90px;
33
+ }
34
+ .circle-container {
35
+ width: 200px;
36
+ height: 200px;
37
+ border-radius: 50%;
38
+ background: radial-gradient(circle, rgba(255,255,255,0.1), rgba(0,0,0,0.5));
39
+ display: flex;
40
+ justify-content: center;
41
+ align-items: center;
42
+ font-size: 1.5rem;
43
+ color: #ffffff;
44
+ position: relative;
45
+ animation: float 5s infinite ease-in-out;
46
+ cursor: pointer;
47
+ }
48
+ .circle-container.disabled {
49
+ pointer-events: none; /* Disable clicks */
50
+ animation: none; /* Stop floating animation */
51
+ }
52
+ .circle-container:before {
53
+ content: "";
54
+ position: absolute;
55
+ top: -10px;
56
+ right: -10px;
57
+ bottom: -10px;
58
+ left: -10px;
59
+ background: radial-gradient(circle, rgba(255,255,255,0.2), transparent);
60
+ border-radius: 50%;
61
+ animation: rotate 10s infinite linear;
62
+ }
63
+ @keyframes float {
64
+ 0%, 100% {
65
+ transform: translateY(0);
66
+ }
67
+ 50% {
68
+ transform: translateY(-20px);
69
+ }
70
+ }
71
+ @keyframes rotate {
72
+ 0% {
73
+ transform: rotate(0deg);
74
+ }
75
+ 100% {
76
+ transform: rotate(360deg);
77
+ }
78
+ }
79
+ .status-text {
80
+ display: flex;
81
+ flex-direction: column;
82
+ align-items: center;
83
+ font-size: 1.2rem;
84
+ color: #ffffff;
85
+ }
86
+ .status-text .emoji {
87
+ font-size: 2rem;
88
+ margin-bottom: 5px;
89
+ }
90
+ /* Response container styles for multi-line text */
91
+ .response-container {
92
+ margin-top: 20px;
93
+ font-family: 'Anonymous Pro', monospace;
94
+ font-size: 0.9rem;
95
+ color: #ffffff;
96
+ text-align: center;
97
+ white-space: normal;
98
+ overflow: hidden;
99
+ width: 80%;
100
+ max-width: 600px; /* Limit the width of the text block */
101
+ line-height: 1.5;
102
+ display: flex;
103
+ flex-direction: column;
104
+ align-items: center;
105
+ }
106
+ .response-line {
107
+ opacity: 0;
108
+ animation: fadeIn 1s ease forwards;
109
+ }
110
+ @keyframes fadeIn {
111
+ from { opacity: 0; }
112
+ to { opacity: 1; }
113
+ }
114
+ </style>
115
+
116
+ <!-- Additional code from buttons.html -->
117
+ <link href="https://fonts.googleapis.com/icon?family=Material+Icons" rel="stylesheet">
118
+
119
+ <style>
120
+ /* ----- Styles from buttons.html (exactly as provided) ----- */
121
+ body {
122
+ background-color: #222;
123
+ display: flex;
124
+ justify-content: center;
125
+ align-items: flex-end;
126
+ height: 90vh;
127
+ margin: 109;
128
+ padding-bottom: 40px;
129
+ position: relative;
130
+ }
131
+ .button-container {
132
+ position: absolute;
133
+ bottom: 50px;
134
+ left: 50%;
135
+ transform: translateX(-50%);
136
+ display: flex;
137
+ flex-direction: row;
138
+ gap: 20px; /* Space between buttons */
139
+ align-items: center;
140
+ justify-content: center;
141
+ }
142
+
143
+ .button {
144
+ width: 60px;
145
+ height: 60px;
146
+ border-radius: 50%;
147
+ display: flex;
148
+ justify-content: center;
149
+ align-items: center;
150
+ cursor: pointer;
151
+ border: 2px solid #fff;
152
+ position: relative;
153
+ transition: background-color 0.3s ease, transform 0.2s ease;
154
+ }
155
+ .button:hover {
156
+ transform: scale(1.1);
157
+ }
158
+ .mute-button:hover {
159
+ background-color: #ffffff; /* Default background color for mute button */
160
+ }
161
+ .mute-button {
162
+ background-color: #333; /* Default background color for mute button */
163
+ }
164
+ .close-button {
165
+ background-color: #333; /* Default background color for close button */
166
+ }
167
+ .close-button:hover {
168
+ background-color: #fff; /* Close button turns white on hover */
169
+ }
170
+ .close-button:hover .mute-icon {
171
+ color: #000; /* Icon color turns black on hover */
172
+ }
173
+ .close-button:hover::after {
174
+ font-size: 12px;
175
+ font-style: bold;
176
+ position: absolute;
177
+ color: #000000;
178
+ }
179
+ .button:hover .mute-icon {
180
+ color: black; /* Icon color changes to black on hover */
181
+ }
182
+ .mute-icon {
183
+ font-size: 36px;
184
+ color: white;
185
+ }
186
+ .muted {
187
+ background-color: #ff0000; /* Light red background when muted */
188
+ }
189
+ .unmuted {
190
+ background-color: #333; /* Default background when unmuted */
191
+ }
192
+ .muted .mute-icon {
193
+ color: black; /* Black mic icon when muted with a line */
194
+ text-decoration: line-through;
195
+ }
196
+ .muted .button {
197
+ background-color: lightcoral; /* Background turns light red when muted */
198
+ }
199
+ .mute-text {
200
+ display: none; /* Mute text is hidden by default */
201
+ }
202
+ .button:hover .mute-text {
203
+ display: block; /* Mute User text is visible on hover */
204
+ font-size: 13px;
205
+ font-weight: bold;
206
+ white-space: nowrap;
207
+ color: white;
208
+ position: absolute;
209
+ top: -30px;
210
+ }
211
+ .mute-text-unmuted {
212
+ display: none; /* Mute User text hidden when unmuted */
213
+ }
214
+ .button.muted .mute-text-unmuted {
215
+ display: none; /* Mute User text is hidden when muted */
216
+ }
217
+ .button.muted .mute-text {
218
+ display: block; /* Unmute text is visible when muted */
219
+ font-size: 13px;
220
+ font-weight: bold;
221
+ white-space: nowrap;
222
+ color: white;
223
+ position: absolute;
224
+ top: -30px;
225
+ }
226
+
227
+ .snowflake {
228
+ position: absolute;
229
+ top: 10px;
230
+ right: 10px;
231
+ color: rgb(160, 227, 246);; /* Snowflake icon color */
232
+ font-size: 36px;
233
+ width: 50px;
234
+ height: 50px;
235
+ display: flex;
236
+ justify-content: center;
237
+ align-items: center;
238
+ animation: snowflakeAnimation 3s infinite;
239
+ cursor: pointer;
240
+ transition: transform 0.3s ease;
241
+ }
242
+
243
+ .snowflake:hover {
244
+ transform: rotate(45deg);
245
+ animation: snowflakeHoverAnimation 0.5s forwards;
246
+ color: rgb(255, 255, 255);
247
+ }
248
+
249
+ @keyframes snowflakeAnimation {
250
+ 0% {
251
+ transform: rotate(0deg);
252
+ }
253
+ 50% {
254
+ transform: rotate(180deg);
255
+ }
256
+ 100% {
257
+ transform: rotate(360deg);
258
+ }
259
+ }
260
+
261
+ @keyframes snowflakeHoverAnimation {
262
+ 0% {
263
+ transform: rotate(45deg);
264
+ }
265
+ 100% {
266
+ transform: scale(1.2);
267
+ }
268
+ }
269
+ .snowflake:hover .snowflake-text {
270
+ display: block;
271
+ color: rgb(255, 255, 255); /* Text color white */
272
+ font-size: 12px; /* Smaller font size */
273
+ font-family: 'Arial', sans-serif; /* Clean font */
274
+ text-align: center;
275
+ line-height: 1.2;
276
+ white-space: nowrap;
277
+ position: absolute; /* Absolute positioning relative to the snowflake */
278
+ top: 100%; /* Positions the text below the snowflake */
279
+ left: 50%; /* Centers the text horizontally */
280
+ transform: translateX(-50%); /* Ensures proper centering */
281
+ margin-top: 10px; /* Additional space between the snowflake and text */
282
+ }
283
+
284
+ .snowflake-text {
285
+ display: none;
286
+ }
287
+
288
+ /* Add a class to remove pointer cursor when muted */
289
+ .muted-disabled {
290
+ cursor: default !important;
291
+ }
292
+ #start-text {
293
+ color: rgb(255, 255, 255);
294
+ text-align: center;
295
+ font-size: 24px;
296
+ font-weight: bold;
297
+ animation: blink 0.5s step-start 2; /* On-off effect (2 cycles, on-off-on) */
298
+ }
299
+
300
+ #start-text.hidden {
301
+ display: none; /* Completely hides the element */
302
+ }
303
+
304
+ @keyframes blink {
305
+ 50% {
306
+ opacity: 0; /* Off state */
307
+ }
308
+ }
309
+
310
+ </style>
311
+ </head>
312
+ <body>
313
+ <!-- Add a wrapper to center main content -->
314
+ <div class="content-wrapper">
315
+ <!-- Content from index.html -->
316
+ <h1>Talk to Your Virtual Therapist</h1>
317
+ <div class="circle-container" id="status-circle" onclick="toggleRecording()">
318
+ <div class="status-text" id="status-text">
319
+ <span class="emoji" id="emoji">🎙️</span>
320
+ Start
321
+ </div>
322
+ </div>
323
+ <div class="response-container" id="response-container"></div>
324
+ <h4 id="start-text">Hit start and let’s talk</h4>
325
+
326
+ <!-- Button container from buttons.html -->
327
+ <div class="button-container">
328
+ <div id="muteButton" class="button mute-button unmuted">
329
+ <span class="mute-icon"><i class="material-icons">mic</i></span>
330
+ <span class="mute-text">Mute User</span>
331
+ </div>
332
+ <div id="closeButton" class="button close-button">
333
+ <span class="mute-icon">&#x2715;</span>
334
+ <span class="mute-text">Terminate</span>
335
+ </div>
336
+ </div>
337
+ </div>
338
+
339
+ <!-- Snowflake from buttons.html -->
340
+ <div class="snowflake" onclick="window.history.back();">
341
+ &#10052;
342
+ <div class="snowflake-text">
343
+ Choose<br>voice
344
+ </div>
345
+ </div>
346
+
347
+ <script>
348
+ // ----- Script from index.html -----
349
+ const statusText = document.getElementById('status-text');
350
+ const emoji = document.getElementById('emoji');
351
+ const responseContainer = document.getElementById('response-container');
352
+ const statusCircle = document.getElementById('status-circle');
353
+
354
+ // Hide the text after 2 blinks (1 second total)
355
+ const startText = document.getElementById('start-text');
356
+
357
+ setTimeout(() => {
358
+ startText.classList.add('hidden'); // Hide the element completely
359
+ }, 5500); // 2 blinks = 1 second (0.5s per blink * 2)
360
+
361
+
362
+ let isRecording = false;
363
+ let mediaRecorder;
364
+ let audioChunks = [];
365
+
366
+ // Reference to mute state
367
+ let isMuted = false;
368
+
369
+ function toggleRecording() {
370
+ // If muted, do nothing
371
+ if (isMuted) {
372
+ return;
373
+ }
374
+
375
+ if (isRecording) {
376
+ stopRecording();
377
+ } else {
378
+ startRecording();
379
+ }
380
+ }
381
+
382
+ function startRecording() {
383
+ isRecording = true;
384
+ statusText.innerHTML = '<span class="emoji">✋</span> Stop';
385
+ responseContainer.innerHTML = ''; // Clear response
386
+ statusCircle.classList.remove('disabled'); // Enable circle click during recording
387
+
388
+ navigator.mediaDevices.getUserMedia({ audio: true }).then((stream) => {
389
+ mediaRecorder = new MediaRecorder(stream);
390
+ audioChunks = [];
391
+ mediaRecorder.ondataavailable = function(event) {
392
+ audioChunks.push(event.data);
393
+ };
394
+ mediaRecorder.onstop = async function() {
395
+ const audioBlob = new Blob(audioChunks, { type: 'audio/wav' });
396
+ const formData = new FormData();
397
+ formData.append('audio_data', audioBlob, 'recording.wav');
398
+
399
+ statusText.innerHTML = '<span class="emoji">⏳</span> Processing';
400
+ statusCircle.classList.add('disabled'); // Disable click while processing
401
+
402
+ const response = await fetch('/process', {
403
+ method: 'POST',
404
+ body: formData
405
+ });
406
+
407
+ const result = await response.json();
408
+
409
+ if (result.error) {
410
+ statusText.innerHTML = `<span class="emoji">❌</span> Error`;
411
+ responseContainer.textContent = `Error: ${result.error}`;
412
+ } else {
413
+ statusText.innerHTML = `<span class="emoji">💬</span> Response`;
414
+ displayResponse(result.response);
415
+ const audio = new Audio(result.audioUrl);
416
+ audio.play();
417
+ audio.onended = () => {
418
+ resetCircle();
419
+ };
420
+ }
421
+ };
422
+ mediaRecorder.start();
423
+ });
424
+ }
425
+
426
+ function stopRecording() {
427
+ isRecording = false;
428
+ statusText.innerHTML = '<span class="emoji">⏳</span> Processing';
429
+ mediaRecorder.stop();
430
+ statusCircle.classList.add('disabled'); // Disable circle click during processing
431
+ }
432
+
433
+ function resetCircle() {
434
+ isRecording = false;
435
+ statusText.innerHTML = '<span class="emoji">🎙️</span> Start';
436
+ statusCircle.classList.remove('disabled'); // Enable circle click after response
437
+ }
438
+
439
+ function displayResponse(responseText) {
440
+ responseContainer.innerHTML = ''; // Clear existing text
441
+ const lines = responseText.split('. '); // Split response into sentences
442
+ lines.forEach((line, index) => {
443
+ const lineElement = document.createElement('div');
444
+ lineElement.textContent = line.trim();
445
+ lineElement.className = 'response-line';
446
+ lineElement.style.animationDelay = `${index}s`;
447
+ responseContainer.appendChild(lineElement);
448
+ });
449
+ }
450
+
451
+ // ----- Script from buttons.html -----
452
+ const muteButton = document.getElementById('muteButton');
453
+ const closeButton = document.getElementById('closeButton');
454
+
455
+ muteButton.addEventListener('click', () => {
456
+ isMuted = !isMuted;
457
+
458
+ // Add/remove class to circle-container to show no pointer when muted
459
+ if (isMuted) {
460
+ muteButton.classList.remove('unmuted');
461
+ muteButton.classList.add('muted');
462
+ muteButton.innerHTML = `
463
+ <span class="mute-icon">
464
+ <i class="material-icons" style="color:black">mic_off</i>
465
+ </span>
466
+ <span class="mute-text">Unmute</span> <!-- Show "Unmute" when muted -->
467
+ `;
468
+ statusCircle.classList.add('muted-disabled');
469
+ } else {
470
+ muteButton.classList.remove('muted');
471
+ muteButton.classList.add('unmuted');
472
+ muteButton.innerHTML = `
473
+ <span class="mute-icon">
474
+ <i class="material-icons">mic</i>
475
+ </span>
476
+ <span class="mute-text">Mute User</span> <!-- Show "Mute User" when unmuted -->
477
+ `;
478
+ statusCircle.classList.remove('muted-disabled');
479
+ }
480
+ });
481
+
482
+ closeButton.addEventListener('click', () => {
483
+ location.reload();
484
+ });
485
+ </script>
486
+
487
+ <!-- Override styles at the end to center the main content while keeping snowflake top-right -->
488
+ <style>
489
+ body {
490
+ background-color: #1d1d1d !important;
491
+ display: flex !important;
492
+ flex-direction: column !important;
493
+ justify-content: center !important;
494
+ align-items: center !important;
495
+ height: 100vh !important;
496
+ margin: 0 !important;
497
+ overflow: hidden !important;
498
+ padding: 0 20px !important;
499
+ text-align: center !important;
500
+ }
501
+ .content-wrapper {
502
+ display: flex;
503
+ flex-direction: column;
504
+ align-items: center;
505
+ justify-content: center;
506
+ }
507
+ .snowflake {
508
+ position: absolute !important;
509
+ top: 10px !important;
510
+ right: 10px !important;
511
+ }
512
+ </style>
513
+ </body>
514
+ </html>