ginipick commited on
Commit
5cf4b8e
·
verified ·
1 Parent(s): 862ffa9

Delete app-backup.py

Browse files
Files changed (1) hide show
  1. app-backup.py +0 -2182
app-backup.py DELETED
@@ -1,2182 +0,0 @@
1
- import asyncio
2
- import base64
3
- import json
4
- from pathlib import Path
5
- import os
6
- import numpy as np
7
- import openai
8
- from dotenv import load_dotenv
9
- from fastapi import FastAPI, Request
10
- from fastapi.responses import HTMLResponse, StreamingResponse
11
- from fastrtc import (
12
- AdditionalOutputs,
13
- AsyncStreamHandler,
14
- Stream,
15
- get_twilio_turn_credentials,
16
- wait_for_item,
17
- )
18
- from gradio.utils import get_space
19
- from openai.types.beta.realtime import ResponseAudioTranscriptDoneEvent
20
- import httpx
21
- from typing import Optional, List, Dict
22
- import gradio as gr
23
- import io
24
- from scipy import signal
25
- import wave
26
- import aiosqlite
27
- from langdetect import detect, LangDetectException
28
- from datetime import datetime
29
- import uuid
30
-
31
- load_dotenv()
32
-
33
- SAMPLE_RATE = 24000
34
-
35
- # Use Persistent Storage path for Hugging Face Space
36
- # In HF Spaces, persistent storage is at /data
37
- if os.path.exists("/data"):
38
- PERSISTENT_DIR = "/data"
39
- else:
40
- PERSISTENT_DIR = "./data"
41
-
42
- os.makedirs(PERSISTENT_DIR, exist_ok=True)
43
- DB_PATH = os.path.join(PERSISTENT_DIR, "personal_assistant.db")
44
- print(f"Using persistent directory: {PERSISTENT_DIR}")
45
- print(f"Database path: {DB_PATH}")
46
-
47
- # HTML content embedded as a string
48
- HTML_CONTENT = """<!DOCTYPE html>
49
- <html lang="ko">
50
-
51
- <head>
52
- <meta charset="UTF-8">
53
- <meta name="viewport" content="width=device-width, initial-scale=1.0">
54
- <title>Personal AI Assistant</title>
55
- <style>
56
- :root {
57
- --primary-color: #6f42c1;
58
- --secondary-color: #563d7c;
59
- --dark-bg: #121212;
60
- --card-bg: #1e1e1e;
61
- --text-color: #f8f9fa;
62
- --border-color: #333;
63
- --hover-color: #8a5cf6;
64
- --memory-color: #4a9eff;
65
- }
66
- body {
67
- font-family: "SF Pro Display", -apple-system, BlinkMacSystemFont, sans-serif;
68
- background-color: var(--dark-bg);
69
- color: var(--text-color);
70
- margin: 0;
71
- padding: 0;
72
- height: 100vh;
73
- display: flex;
74
- flex-direction: column;
75
- overflow: hidden;
76
- }
77
- .container {
78
- max-width: 1400px;
79
- margin: 0 auto;
80
- padding: 20px;
81
- flex-grow: 1;
82
- display: flex;
83
- flex-direction: column;
84
- width: 100%;
85
- height: 100vh;
86
- box-sizing: border-box;
87
- overflow: hidden;
88
- }
89
- .header {
90
- text-align: center;
91
- padding: 15px 0;
92
- border-bottom: 1px solid var(--border-color);
93
- margin-bottom: 20px;
94
- flex-shrink: 0;
95
- background-color: var(--card-bg);
96
- }
97
- .main-content {
98
- display: flex;
99
- gap: 20px;
100
- flex-grow: 1;
101
- min-height: 0;
102
- overflow: hidden;
103
- }
104
- .sidebar {
105
- width: 350px;
106
- flex-shrink: 0;
107
- display: flex;
108
- flex-direction: column;
109
- gap: 20px;
110
- overflow-y: auto;
111
- max-height: calc(100vh - 120px);
112
- }
113
- .chat-section {
114
- flex-grow: 1;
115
- display: flex;
116
- flex-direction: column;
117
- min-width: 0;
118
- }
119
- .logo {
120
- display: flex;
121
- align-items: center;
122
- justify-content: center;
123
- gap: 10px;
124
- }
125
- .logo h1 {
126
- margin: 0;
127
- background: linear-gradient(135deg, var(--primary-color), #a78bfa);
128
- -webkit-background-clip: text;
129
- background-clip: text;
130
- color: transparent;
131
- font-size: 32px;
132
- letter-spacing: 1px;
133
- }
134
- /* Settings section */
135
- .settings-section {
136
- background-color: var(--card-bg);
137
- border-radius: 12px;
138
- padding: 20px;
139
- border: 1px solid var(--border-color);
140
- overflow-y: auto;
141
- }
142
- .settings-grid {
143
- display: flex;
144
- flex-direction: column;
145
- gap: 15px;
146
- margin-bottom: 15px;
147
- }
148
- .setting-item {
149
- display: flex;
150
- align-items: center;
151
- justify-content: space-between;
152
- gap: 10px;
153
- }
154
- .setting-label {
155
- font-size: 14px;
156
- color: #aaa;
157
- min-width: 60px;
158
- }
159
- /* Toggle switch */
160
- .toggle-switch {
161
- position: relative;
162
- width: 50px;
163
- height: 26px;
164
- background-color: #ccc;
165
- border-radius: 13px;
166
- cursor: pointer;
167
- transition: background-color 0.3s;
168
- }
169
- .toggle-switch.active {
170
- background-color: var(--primary-color);
171
- }
172
- .toggle-slider {
173
- position: absolute;
174
- top: 3px;
175
- left: 3px;
176
- width: 20px;
177
- height: 20px;
178
- background-color: white;
179
- border-radius: 50%;
180
- transition: transform 0.3s;
181
- }
182
- .toggle-switch.active .toggle-slider {
183
- transform: translateX(24px);
184
- }
185
- /* Memory section */
186
- .memory-section {
187
- background-color: var(--card-bg);
188
- border-radius: 12px;
189
- padding: 20px;
190
- border: 1px solid var(--border-color);
191
- max-height: 300px;
192
- overflow-y: auto;
193
- }
194
- .memory-item {
195
- padding: 10px;
196
- margin-bottom: 10px;
197
- background: linear-gradient(135deg, rgba(74, 158, 255, 0.1), rgba(111, 66, 193, 0.1));
198
- border-radius: 6px;
199
- border-left: 3px solid var(--memory-color);
200
- }
201
- .memory-category {
202
- font-size: 12px;
203
- color: var(--memory-color);
204
- font-weight: bold;
205
- text-transform: uppercase;
206
- margin-bottom: 5px;
207
- }
208
- .memory-content {
209
- font-size: 14px;
210
- color: var(--text-color);
211
- }
212
- /* History section */
213
- .history-section {
214
- background-color: var(--card-bg);
215
- border-radius: 12px;
216
- padding: 20px;
217
- border: 1px solid var(--border-color);
218
- max-height: 200px;
219
- overflow-y: auto;
220
- }
221
- .history-item {
222
- padding: 10px;
223
- margin-bottom: 10px;
224
- background-color: var(--dark-bg);
225
- border-radius: 6px;
226
- cursor: pointer;
227
- transition: background-color 0.2s;
228
- }
229
- .history-item:hover {
230
- background-color: var(--hover-color);
231
- }
232
- .history-date {
233
- font-size: 12px;
234
- color: #888;
235
- }
236
- .history-preview {
237
- font-size: 14px;
238
- margin-top: 5px;
239
- overflow: hidden;
240
- text-overflow: ellipsis;
241
- white-space: nowrap;
242
- }
243
- /* Text inputs */
244
- .text-input-section {
245
- margin-top: 15px;
246
- }
247
- input[type="text"], textarea {
248
- width: 100%;
249
- background-color: var(--dark-bg);
250
- color: var(--text-color);
251
- border: 1px solid var(--border-color);
252
- padding: 10px;
253
- border-radius: 6px;
254
- font-size: 14px;
255
- box-sizing: border-box;
256
- margin-top: 5px;
257
- }
258
- input[type="text"]:focus, textarea:focus {
259
- outline: none;
260
- border-color: var(--primary-color);
261
- }
262
- textarea {
263
- resize: vertical;
264
- min-height: 80px;
265
- }
266
- .chat-container {
267
- border-radius: 12px;
268
- background-color: var(--card-bg);
269
- box-shadow: 0 8px 32px rgba(0, 0, 0, 0.2);
270
- padding: 20px;
271
- flex-grow: 1;
272
- display: flex;
273
- flex-direction: column;
274
- border: 1px solid var(--border-color);
275
- overflow: hidden;
276
- min-height: 0;
277
- height: 100%;
278
- }
279
- .chat-messages {
280
- flex-grow: 1;
281
- overflow-y: auto;
282
- padding: 15px;
283
- scrollbar-width: thin;
284
- scrollbar-color: var(--primary-color) var(--card-bg);
285
- min-height: 0;
286
- max-height: calc(100vh - 250px);
287
- }
288
- .chat-messages::-webkit-scrollbar {
289
- width: 6px;
290
- }
291
- .chat-messages::-webkit-scrollbar-thumb {
292
- background-color: var(--primary-color);
293
- border-radius: 6px;
294
- }
295
- .message {
296
- margin-bottom: 15px;
297
- padding: 12px 16px;
298
- border-radius: 8px;
299
- font-size: 15px;
300
- line-height: 1.5;
301
- position: relative;
302
- max-width: 85%;
303
- animation: fade-in 0.3s ease-out;
304
- word-wrap: break-word;
305
- }
306
- @keyframes fade-in {
307
- from {
308
- opacity: 0;
309
- transform: translateY(10px);
310
- }
311
- to {
312
- opacity: 1;
313
- transform: translateY(0);
314
- }
315
- }
316
- .message.user {
317
- background: linear-gradient(135deg, #2c3e50, #34495e);
318
- margin-left: auto;
319
- border-bottom-right-radius: 2px;
320
- }
321
- .message.assistant {
322
- background: linear-gradient(135deg, var(--secondary-color), var(--primary-color));
323
- margin-right: auto;
324
- border-bottom-left-radius: 2px;
325
- }
326
- .message.search-result {
327
- background: linear-gradient(135deg, #1a5a3e, #2e7d32);
328
- font-size: 14px;
329
- padding: 10px;
330
- margin-bottom: 10px;
331
- }
332
- .message.memory-update {
333
- background: linear-gradient(135deg, rgba(74, 158, 255, 0.2), rgba(111, 66, 193, 0.2));
334
- font-size: 13px;
335
- padding: 8px 12px;
336
- margin-bottom: 10px;
337
- border-left: 3px solid var(--memory-color);
338
- }
339
- .language-info {
340
- font-size: 12px;
341
- color: #888;
342
- margin-left: 5px;
343
- }
344
- .controls {
345
- text-align: center;
346
- margin-top: auto;
347
- display: flex;
348
- justify-content: center;
349
- gap: 10px;
350
- flex-shrink: 0;
351
- padding-top: 20px;
352
- }
353
- /* Responsive design */
354
- @media (max-width: 1024px) {
355
- .sidebar {
356
- width: 300px;
357
- }
358
- }
359
- @media (max-width: 768px) {
360
- .main-content {
361
- flex-direction: column;
362
- }
363
- .sidebar {
364
- width: 100%;
365
- margin-bottom: 20px;
366
- }
367
- .chat-section {
368
- height: 400px;
369
- }
370
- }
371
- button {
372
- background: linear-gradient(135deg, var(--primary-color), var(--secondary-color));
373
- color: white;
374
- border: none;
375
- padding: 14px 28px;
376
- font-family: inherit;
377
- font-size: 16px;
378
- cursor: pointer;
379
- transition: all 0.3s;
380
- text-transform: uppercase;
381
- letter-spacing: 1px;
382
- border-radius: 50px;
383
- display: flex;
384
- align-items: center;
385
- justify-content: center;
386
- gap: 10px;
387
- box-shadow: 0 4px 10px rgba(111, 66, 193, 0.3);
388
- }
389
- button:hover {
390
- transform: translateY(-2px);
391
- box-shadow: 0 6px 15px rgba(111, 66, 193, 0.5);
392
- background: linear-gradient(135deg, var(--hover-color), var(--primary-color));
393
- }
394
- button:active {
395
- transform: translateY(1px);
396
- }
397
- #send-button {
398
- background: linear-gradient(135deg, #2ecc71, #27ae60);
399
- padding: 10px 20px;
400
- font-size: 14px;
401
- flex-shrink: 0;
402
- }
403
- #send-button:hover {
404
- background: linear-gradient(135deg, #27ae60, #229954);
405
- }
406
- #end-session-button {
407
- background: linear-gradient(135deg, #4a9eff, #3a7ed8);
408
- padding: 8px 16px;
409
- font-size: 13px;
410
- }
411
- #end-session-button:hover {
412
- background: linear-gradient(135deg, #3a7ed8, #2a5eb8);
413
- }
414
- #audio-output {
415
- display: none;
416
- }
417
- .icon-with-spinner {
418
- display: flex;
419
- align-items: center;
420
- justify-content: center;
421
- gap: 12px;
422
- min-width: 180px;
423
- }
424
- .spinner {
425
- width: 20px;
426
- height: 20px;
427
- border: 2px solid #ffffff;
428
- border-top-color: transparent;
429
- border-radius: 50%;
430
- animation: spin 1s linear infinite;
431
- flex-shrink: 0;
432
- }
433
- @keyframes spin {
434
- to {
435
- transform: rotate(360deg);
436
- }
437
- }
438
- .audio-visualizer {
439
- display: flex;
440
- align-items: center;
441
- justify-content: center;
442
- gap: 5px;
443
- min-width: 80px;
444
- height: 25px;
445
- }
446
- .visualizer-bar {
447
- width: 4px;
448
- height: 100%;
449
- background-color: rgba(255, 255, 255, 0.7);
450
- border-radius: 2px;
451
- transform-origin: bottom;
452
- transform: scaleY(0.1);
453
- transition: transform 0.1s ease;
454
- }
455
- .toast {
456
- position: fixed;
457
- top: 20px;
458
- left: 50%;
459
- transform: translateX(-50%);
460
- padding: 16px 24px;
461
- border-radius: 8px;
462
- font-size: 14px;
463
- z-index: 1000;
464
- display: none;
465
- box-shadow: 0 4px 12px rgba(0, 0, 0, 0.3);
466
- }
467
- .toast.error {
468
- background-color: #f44336;
469
- color: white;
470
- }
471
- .toast.warning {
472
- background-color: #ff9800;
473
- color: white;
474
- }
475
- .toast.success {
476
- background-color: #4caf50;
477
- color: white;
478
- }
479
- .status-indicator {
480
- display: inline-flex;
481
- align-items: center;
482
- margin-top: 10px;
483
- font-size: 14px;
484
- color: #aaa;
485
- }
486
- .status-dot {
487
- width: 8px;
488
- height: 8px;
489
- border-radius: 50%;
490
- margin-right: 8px;
491
- }
492
- .status-dot.connected {
493
- background-color: #4caf50;
494
- }
495
- .status-dot.disconnected {
496
- background-color: #f44336;
497
- }
498
- .status-dot.connecting {
499
- background-color: #ff9800;
500
- animation: pulse 1.5s infinite;
501
- }
502
- @keyframes pulse {
503
- 0% {
504
- opacity: 0.6;
505
- }
506
- 50% {
507
- opacity: 1;
508
- }
509
- 100% {
510
- opacity: 0.6;
511
- }
512
- }
513
- .user-avatar {
514
- width: 40px;
515
- height: 40px;
516
- background: linear-gradient(135deg, var(--primary-color), var(--secondary-color));
517
- border-radius: 50%;
518
- display: flex;
519
- align-items: center;
520
- justify-content: center;
521
- font-size: 20px;
522
- font-weight: bold;
523
- color: white;
524
- }
525
- </style>
526
- </head>
527
-
528
- <body>
529
- <div id="error-toast" class="toast"></div>
530
- <div class="container">
531
- <div class="header">
532
- <div class="logo">
533
- <div class="user-avatar" id="user-avatar">👤</div>
534
- <h1>Personal AI Assistant</h1>
535
- </div>
536
- <div class="status-indicator">
537
- <div id="status-dot" class="status-dot disconnected"></div>
538
- <span id="status-text">연결 대기 중</span>
539
- </div>
540
- </div>
541
-
542
- <div class="main-content">
543
- <div class="sidebar">
544
- <div class="settings-section">
545
- <h3 style="margin: 0 0 15px 0; color: var(--primary-color);">설정</h3>
546
- <div class="settings-grid">
547
- <div class="setting-item">
548
- <span class="setting-label">웹 검색</span>
549
- <div id="search-toggle" class="toggle-switch">
550
- <div class="toggle-slider"></div>
551
- </div>
552
- </div>
553
- </div>
554
- <div class="text-input-section">
555
- <label for="user-name" class="setting-label">사용자 이름:</label>
556
- <input type="text" id="user-name" placeholder="이름을 입력하세요..." />
557
- </div>
558
- </div>
559
-
560
- <div class="memory-section">
561
- <h3 style="margin: 0 0 15px 0; color: var(--memory-color);">기억된 정보</h3>
562
- <div id="memory-list"></div>
563
- </div>
564
-
565
- <div class="history-section">
566
- <h3 style="margin: 0 0 15px 0; color: var(--primary-color);">대화 기록</h3>
567
- <div id="history-list"></div>
568
- </div>
569
-
570
- <div class="controls">
571
- <button id="start-button">대화 시작</button>
572
- <button id="end-session-button" style="display: none;">기억 업데이트</button>
573
- </div>
574
- </div>
575
-
576
- <div class="chat-section">
577
- <div class="chat-container">
578
- <h3 style="margin: 0 0 15px 0; color: var(--primary-color);">대화</h3>
579
- <div class="chat-messages" id="chat-messages"></div>
580
- <div class="text-input-section" style="margin-top: 10px;">
581
- <div style="display: flex; gap: 10px;">
582
- <input type="text" id="text-input" placeholder="텍스트 메시지를 입력하세요..." style="flex-grow: 1;" />
583
- <button id="send-button" style="display: none;">전송</button>
584
- </div>
585
- </div>
586
- </div>
587
- </div>
588
- </div>
589
- </div>
590
- <audio id="audio-output"></audio>
591
-
592
- <script>
593
- let peerConnection;
594
- let webrtc_id;
595
- let webSearchEnabled = false;
596
- let currentSessionId = null;
597
- let userName = localStorage.getItem('userName') || '';
598
- let userMemories = {};
599
- const audioOutput = document.getElementById('audio-output');
600
- const startButton = document.getElementById('start-button');
601
- const endSessionButton = document.getElementById('end-session-button');
602
- const sendButton = document.getElementById('send-button');
603
- const chatMessages = document.getElementById('chat-messages');
604
- const statusDot = document.getElementById('status-dot');
605
- const statusText = document.getElementById('status-text');
606
- const searchToggle = document.getElementById('search-toggle');
607
- const textInput = document.getElementById('text-input');
608
- const historyList = document.getElementById('history-list');
609
- const memoryList = document.getElementById('memory-list');
610
- const userNameInput = document.getElementById('user-name');
611
- const userAvatar = document.getElementById('user-avatar');
612
- let audioLevel = 0;
613
- let animationFrame;
614
- let audioContext, analyser, audioSource;
615
- let dataChannel = null;
616
- let isVoiceActive = false;
617
-
618
- // Initialize user name
619
- userNameInput.value = userName;
620
- if (userName) {
621
- userAvatar.textContent = userName.charAt(0).toUpperCase();
622
- }
623
-
624
- userNameInput.addEventListener('input', () => {
625
- userName = userNameInput.value;
626
- localStorage.setItem('userName', userName);
627
- if (userName) {
628
- userAvatar.textContent = userName.charAt(0).toUpperCase();
629
- } else {
630
- userAvatar.textContent = '👤';
631
- }
632
- });
633
-
634
- // Start new session
635
- async function startNewSession() {
636
- const response = await fetch('/session/new', { method: 'POST' });
637
- const data = await response.json();
638
- currentSessionId = data.session_id;
639
- console.log('New session started:', currentSessionId);
640
- loadHistory();
641
- loadMemories();
642
- }
643
-
644
- // Load memories
645
- async function loadMemories() {
646
- try {
647
- const response = await fetch('/memory/all');
648
- const memories = await response.json();
649
-
650
- userMemories = {};
651
- memoryList.innerHTML = '';
652
-
653
- memories.forEach(memory => {
654
- if (!userMemories[memory.category]) {
655
- userMemories[memory.category] = [];
656
- }
657
- userMemories[memory.category].push(memory.content);
658
-
659
- const item = document.createElement('div');
660
- item.className = 'memory-item';
661
- item.innerHTML = `
662
- <div class="memory-category">${memory.category}</div>
663
- <div class="memory-content">${memory.content}</div>
664
- `;
665
- memoryList.appendChild(item);
666
- });
667
-
668
- console.log('Loaded memories:', userMemories);
669
- } catch (error) {
670
- console.error('Failed to load memories:', error);
671
- }
672
- }
673
-
674
- // End session and update memories
675
- async function endSession() {
676
- if (!currentSessionId) return;
677
-
678
- try {
679
- addMessage('memory-update', '대화 내용을 분석하여 기억을 업데이트하고 있습니다...');
680
-
681
- const response = await fetch('/session/end', {
682
- method: 'POST',
683
- headers: { 'Content-Type': 'application/json' },
684
- body: JSON.stringify({ session_id: currentSessionId })
685
- });
686
-
687
- const result = await response.json();
688
- if (result.status === 'ok') {
689
- showToast('기억이 성공적으로 업데이트되었습니다.', 'success');
690
- loadMemories();
691
- startNewSession();
692
- }
693
- } catch (error) {
694
- console.error('Failed to end session:', error);
695
- showError('기억 업데이트 중 오류가 발생했습니다.');
696
- }
697
- }
698
-
699
- // Load conversation history
700
- async function loadHistory() {
701
- try {
702
- const response = await fetch('/history/recent');
703
- const conversations = await response.json();
704
-
705
- historyList.innerHTML = '';
706
- conversations.forEach(conv => {
707
- const item = document.createElement('div');
708
- item.className = 'history-item';
709
- item.innerHTML = `
710
- <div class="history-date">${new Date(conv.created_at).toLocaleString()}</div>
711
- <div class="history-preview">${conv.summary || '대화 시작'}</div>
712
- `;
713
- item.onclick = () => loadConversation(conv.id);
714
- historyList.appendChild(item);
715
- });
716
- } catch (error) {
717
- console.error('Failed to load history:', error);
718
- }
719
- }
720
-
721
- // Load specific conversation
722
- async function loadConversation(sessionId) {
723
- try {
724
- const response = await fetch(`/history/${sessionId}`);
725
- const messages = await response.json();
726
-
727
- chatMessages.innerHTML = '';
728
- messages.forEach(msg => {
729
- addMessage(msg.role, msg.content, false);
730
- });
731
- } catch (error) {
732
- console.error('Failed to load conversation:', error);
733
- }
734
- }
735
-
736
- // Web search toggle functionality
737
- searchToggle.addEventListener('click', () => {
738
- webSearchEnabled = !webSearchEnabled;
739
- searchToggle.classList.toggle('active', webSearchEnabled);
740
- console.log('Web search enabled:', webSearchEnabled);
741
- });
742
-
743
- // Text input handling
744
- textInput.addEventListener('keypress', (e) => {
745
- if (e.key === 'Enter' && !e.shiftKey) {
746
- e.preventDefault();
747
- sendTextMessage();
748
- }
749
- });
750
-
751
- sendButton.addEventListener('click', sendTextMessage);
752
- endSessionButton.addEventListener('click', endSession);
753
-
754
- async function sendTextMessage() {
755
- const message = textInput.value.trim();
756
- if (!message) return;
757
-
758
- // Check for stop words
759
- const stopWords = ["중단", "그만", "스톱", "stop", "닥쳐", "멈춰", "중지"];
760
- if (stopWords.some(word => message.toLowerCase().includes(word))) {
761
- addMessage('assistant', '대화를 중단합니다.');
762
- return;
763
- }
764
-
765
- // Add user message to chat
766
- addMessage('user', message);
767
- textInput.value = '';
768
-
769
- // Show sending indicator
770
- const typingIndicator = document.createElement('div');
771
- typingIndicator.classList.add('message', 'assistant');
772
- typingIndicator.textContent = '입력 중...';
773
- typingIndicator.id = 'typing-indicator';
774
- chatMessages.appendChild(typingIndicator);
775
- chatMessages.scrollTop = chatMessages.scrollHeight;
776
-
777
- try {
778
- // Send to text chat endpoint
779
- const response = await fetch('/chat/text', {
780
- method: 'POST',
781
- headers: { 'Content-Type': 'application/json' },
782
- body: JSON.stringify({
783
- message: message,
784
- web_search_enabled: webSearchEnabled,
785
- session_id: currentSessionId,
786
- user_name: userName,
787
- memories: userMemories
788
- })
789
- });
790
-
791
- const data = await response.json();
792
-
793
- // Remove typing indicator
794
- const indicator = document.getElementById('typing-indicator');
795
- if (indicator) indicator.remove();
796
-
797
- if (data.error) {
798
- showError(data.error);
799
- } else {
800
- // Add assistant response
801
- let content = data.response;
802
- if (data.detected_language) {
803
- content += ` <span class="language-info">[${data.detected_language}]</span>`;
804
- }
805
- addMessage('assistant', content);
806
- }
807
- } catch (error) {
808
- console.error('Error sending text message:', error);
809
- const indicator = document.getElementById('typing-indicator');
810
- if (indicator) indicator.remove();
811
- showError('메시지 전송 중 오류가 발생했습니다.');
812
- }
813
- }
814
-
815
- function updateStatus(state) {
816
- statusDot.className = 'status-dot ' + state;
817
- if (state === 'connected') {
818
- statusText.textContent = '연결됨';
819
- sendButton.style.display = 'block';
820
- endSessionButton.style.display = 'block';
821
- isVoiceActive = true;
822
- } else if (state === 'connecting') {
823
- statusText.textContent = '연결 중...';
824
- sendButton.style.display = 'none';
825
- endSessionButton.style.display = 'none';
826
- } else {
827
- statusText.textContent = '연결 대기 중';
828
- sendButton.style.display = 'block';
829
- endSessionButton.style.display = 'block';
830
- isVoiceActive = false;
831
- }
832
- }
833
-
834
- function showToast(message, type = 'info') {
835
- const toast = document.getElementById('error-toast');
836
- toast.textContent = message;
837
- toast.className = `toast ${type}`;
838
- toast.style.display = 'block';
839
- setTimeout(() => {
840
- toast.style.display = 'none';
841
- }, 5000);
842
- }
843
-
844
- function showError(message) {
845
- showToast(message, 'error');
846
- }
847
-
848
- function updateButtonState() {
849
- const button = document.getElementById('start-button');
850
- if (peerConnection && (peerConnection.connectionState === 'connecting' || peerConnection.connectionState === 'new')) {
851
- button.innerHTML = `
852
- <div class="icon-with-spinner">
853
- <div class="spinner"></div>
854
- <span>연결 중...</span>
855
- </div>
856
- `;
857
- updateStatus('connecting');
858
- } else if (peerConnection && peerConnection.connectionState === 'connected') {
859
- button.innerHTML = `
860
- <div class="icon-with-spinner">
861
- <div class="audio-visualizer" id="audio-visualizer">
862
- <div class="visualizer-bar"></div>
863
- <div class="visualizer-bar"></div>
864
- <div class="visualizer-bar"></div>
865
- <div class="visualizer-bar"></div>
866
- <div class="visualizer-bar"></div>
867
- </div>
868
- <span>대화 종료</span>
869
- </div>
870
- `;
871
- updateStatus('connected');
872
- } else {
873
- button.innerHTML = '대화 시작';
874
- updateStatus('disconnected');
875
- }
876
- }
877
-
878
- function setupAudioVisualization(stream) {
879
- audioContext = new (window.AudioContext || window.webkitAudioContext)();
880
- analyser = audioContext.createAnalyser();
881
- audioSource = audioContext.createMediaStreamSource(stream);
882
- audioSource.connect(analyser);
883
- analyser.fftSize = 256;
884
- const bufferLength = analyser.frequencyBinCount;
885
- const dataArray = new Uint8Array(bufferLength);
886
-
887
- const visualizerBars = document.querySelectorAll('.visualizer-bar');
888
- const barCount = visualizerBars.length;
889
-
890
- function updateAudioLevel() {
891
- analyser.getByteFrequencyData(dataArray);
892
-
893
- for (let i = 0; i < barCount; i++) {
894
- const start = Math.floor(i * (bufferLength / barCount));
895
- const end = Math.floor((i + 1) * (bufferLength / barCount));
896
-
897
- let sum = 0;
898
- for (let j = start; j < end; j++) {
899
- sum += dataArray[j];
900
- }
901
-
902
- const average = sum / (end - start) / 255;
903
- const scaleY = 0.1 + average * 0.9;
904
- visualizerBars[i].style.transform = `scaleY(${scaleY})`;
905
- }
906
-
907
- animationFrame = requestAnimationFrame(updateAudioLevel);
908
- }
909
-
910
- updateAudioLevel();
911
- }
912
-
913
- async function setupWebRTC() {
914
- const config = __RTC_CONFIGURATION__;
915
- peerConnection = new RTCPeerConnection(config);
916
- const timeoutId = setTimeout(() => {
917
- showToast("연결이 평소보다 오래 걸리고 있습니다. VPN을 사용 중이신가요?", 'warning');
918
- }, 5000);
919
-
920
- try {
921
- const stream = await navigator.mediaDevices.getUserMedia({
922
- audio: true
923
- });
924
- setupAudioVisualization(stream);
925
- stream.getTracks().forEach(track => {
926
- peerConnection.addTrack(track, stream);
927
- });
928
- peerConnection.addEventListener('track', (evt) => {
929
- if (audioOutput.srcObject !== evt.streams[0]) {
930
- audioOutput.srcObject = evt.streams[0];
931
- audioOutput.play();
932
- }
933
- });
934
-
935
- // Create data channel for text messages
936
- dataChannel = peerConnection.createDataChannel('text');
937
- dataChannel.onopen = () => {
938
- console.log('Data channel opened');
939
- };
940
- dataChannel.onmessage = (event) => {
941
- const eventJson = JSON.parse(event.data);
942
- if (eventJson.type === "error") {
943
- showError(eventJson.message);
944
- }
945
- };
946
-
947
- const offer = await peerConnection.createOffer();
948
- await peerConnection.setLocalDescription(offer);
949
- await new Promise((resolve) => {
950
- if (peerConnection.iceGatheringState === "complete") {
951
- resolve();
952
- } else {
953
- const checkState = () => {
954
- if (peerConnection.iceGatheringState === "complete") {
955
- peerConnection.removeEventListener("icegatheringstatechange", checkState);
956
- resolve();
957
- }
958
- };
959
- peerConnection.addEventListener("icegatheringstatechange", checkState);
960
- }
961
- });
962
-
963
- peerConnection.addEventListener('connectionstatechange', () => {
964
- console.log('connectionstatechange', peerConnection.connectionState);
965
- if (peerConnection.connectionState === 'connected') {
966
- clearTimeout(timeoutId);
967
- const toast = document.getElementById('error-toast');
968
- toast.style.display = 'none';
969
- }
970
- updateButtonState();
971
- });
972
-
973
- webrtc_id = Math.random().toString(36).substring(7);
974
-
975
- const response = await fetch('/webrtc/offer', {
976
- method: 'POST',
977
- headers: { 'Content-Type': 'application/json' },
978
- body: JSON.stringify({
979
- sdp: peerConnection.localDescription.sdp,
980
- type: peerConnection.localDescription.type,
981
- webrtc_id: webrtc_id,
982
- web_search_enabled: webSearchEnabled,
983
- session_id: currentSessionId,
984
- user_name: userName,
985
- memories: userMemories
986
- })
987
- });
988
-
989
- const serverResponse = await response.json();
990
- if (serverResponse.status === 'failed') {
991
- showError(serverResponse.meta.error === 'concurrency_limit_reached'
992
- ? `너무 많은 연결입니다. 최대 한도는 ${serverResponse.meta.limit} 입니다.`
993
- : serverResponse.meta.error);
994
- stop();
995
- return;
996
- }
997
-
998
- await peerConnection.setRemoteDescription(serverResponse);
999
- const eventSource = new EventSource('/outputs?webrtc_id=' + webrtc_id);
1000
- eventSource.addEventListener("output", (event) => {
1001
- const eventJson = JSON.parse(event.data);
1002
- let content = eventJson.content;
1003
-
1004
- if (eventJson.detected_language) {
1005
- content += ` <span class="language-info">[${eventJson.detected_language}]</span>`;
1006
- }
1007
- addMessage("assistant", content);
1008
- });
1009
- eventSource.addEventListener("search", (event) => {
1010
- const eventJson = JSON.parse(event.data);
1011
- if (eventJson.query) {
1012
- addMessage("search-result", `웹 검색 중: "${eventJson.query}"`);
1013
- }
1014
- });
1015
- } catch (err) {
1016
- clearTimeout(timeoutId);
1017
- console.error('Error setting up WebRTC:', err);
1018
- showError('연결을 설정하지 못했습니다. 다시 시도해 주세요.');
1019
- stop();
1020
- }
1021
- }
1022
-
1023
- function addMessage(role, content, save = true) {
1024
- const messageDiv = document.createElement('div');
1025
- messageDiv.classList.add('message', role);
1026
-
1027
- if (content.includes('<span')) {
1028
- messageDiv.innerHTML = content;
1029
- } else {
1030
- messageDiv.textContent = content;
1031
- }
1032
- chatMessages.appendChild(messageDiv);
1033
- chatMessages.scrollTop = chatMessages.scrollHeight;
1034
-
1035
- // Save message to database if save flag is true
1036
- if (save && currentSessionId && role !== 'memory-update' && role !== 'search-result') {
1037
- fetch('/message/save', {
1038
- method: 'POST',
1039
- headers: { 'Content-Type': 'application/json' },
1040
- body: JSON.stringify({
1041
- session_id: currentSessionId,
1042
- role: role,
1043
- content: content
1044
- })
1045
- }).catch(error => console.error('Failed to save message:', error));
1046
- }
1047
- }
1048
-
1049
- function stop() {
1050
- console.log('[STOP] Stopping connection...');
1051
-
1052
- // Cancel animation frame first
1053
- if (animationFrame) {
1054
- cancelAnimationFrame(animationFrame);
1055
- animationFrame = null;
1056
- }
1057
-
1058
- // Close audio context
1059
- if (audioContext) {
1060
- audioContext.close();
1061
- audioContext = null;
1062
- analyser = null;
1063
- audioSource = null;
1064
- }
1065
-
1066
- // Close data channel
1067
- if (dataChannel) {
1068
- dataChannel.close();
1069
- dataChannel = null;
1070
- }
1071
-
1072
- // Close peer connection
1073
- if (peerConnection) {
1074
- console.log('[STOP] Current connection state:', peerConnection.connectionState);
1075
-
1076
- // Stop all transceivers
1077
- if (peerConnection.getTransceivers) {
1078
- peerConnection.getTransceivers().forEach(transceiver => {
1079
- if (transceiver.stop) {
1080
- transceiver.stop();
1081
- }
1082
- });
1083
- }
1084
-
1085
- // Stop all senders
1086
- if (peerConnection.getSenders) {
1087
- peerConnection.getSenders().forEach(sender => {
1088
- if (sender.track) {
1089
- sender.track.stop();
1090
- }
1091
- });
1092
- }
1093
-
1094
- // Stop all receivers
1095
- if (peerConnection.getReceivers) {
1096
- peerConnection.getReceivers().forEach(receiver => {
1097
- if (receiver.track) {
1098
- receiver.track.stop();
1099
- }
1100
- });
1101
- }
1102
-
1103
- // Close the connection
1104
- peerConnection.close();
1105
-
1106
- // Clear the reference
1107
- peerConnection = null;
1108
-
1109
- console.log('[STOP] Connection closed');
1110
- }
1111
-
1112
- // Reset audio level
1113
- audioLevel = 0;
1114
- isVoiceActive = false;
1115
-
1116
- // Update UI
1117
- updateButtonState();
1118
-
1119
- // Clear any existing webrtc_id
1120
- if (webrtc_id) {
1121
- console.log('[STOP] Clearing webrtc_id:', webrtc_id);
1122
- webrtc_id = null;
1123
- }
1124
- }
1125
-
1126
- startButton.addEventListener('click', () => {
1127
- console.log('clicked');
1128
- console.log(peerConnection, peerConnection?.connectionState);
1129
- if (!peerConnection || peerConnection.connectionState !== 'connected') {
1130
- setupWebRTC();
1131
- } else {
1132
- console.log('stopping');
1133
- stop();
1134
- }
1135
- });
1136
-
1137
- // Initialize on page load
1138
- window.addEventListener('DOMContentLoaded', () => {
1139
- sendButton.style.display = 'block';
1140
- endSessionButton.style.display = 'block';
1141
- startNewSession();
1142
- loadHistory();
1143
- loadMemories();
1144
- });
1145
- </script>
1146
- </body>
1147
-
1148
- </html>"""
1149
-
1150
-
1151
- class BraveSearchClient:
1152
- """Brave Search API client"""
1153
- def __init__(self, api_key: str):
1154
- self.api_key = api_key
1155
- self.base_url = "https://api.search.brave.com/res/v1/web/search"
1156
-
1157
- async def search(self, query: str, count: int = 10) -> List[Dict]:
1158
- """Perform a web search using Brave Search API"""
1159
- if not self.api_key:
1160
- return []
1161
-
1162
- headers = {
1163
- "Accept": "application/json",
1164
- "X-Subscription-Token": self.api_key
1165
- }
1166
- params = {
1167
- "q": query,
1168
- "count": count,
1169
- "lang": "ko"
1170
- }
1171
-
1172
- async with httpx.AsyncClient() as client:
1173
- try:
1174
- response = await client.get(self.base_url, headers=headers, params=params)
1175
- response.raise_for_status()
1176
- data = response.json()
1177
-
1178
- results = []
1179
- if "web" in data and "results" in data["web"]:
1180
- for result in data["web"]["results"][:count]:
1181
- results.append({
1182
- "title": result.get("title", ""),
1183
- "url": result.get("url", ""),
1184
- "description": result.get("description", "")
1185
- })
1186
- return results
1187
- except Exception as e:
1188
- print(f"Brave Search error: {e}")
1189
- return []
1190
-
1191
-
1192
- # Database helper class
1193
- class PersonalAssistantDB:
1194
- """Database manager for personal assistant"""
1195
-
1196
- @staticmethod
1197
- async def init():
1198
- """Initialize database tables"""
1199
- async with aiosqlite.connect(DB_PATH) as db:
1200
- # Conversations table
1201
- await db.execute("""
1202
- CREATE TABLE IF NOT EXISTS conversations (
1203
- id TEXT PRIMARY KEY,
1204
- created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
1205
- updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
1206
- summary TEXT
1207
- )
1208
- """)
1209
-
1210
- # Messages table
1211
- await db.execute("""
1212
- CREATE TABLE IF NOT EXISTS messages (
1213
- id INTEGER PRIMARY KEY AUTOINCREMENT,
1214
- session_id TEXT NOT NULL,
1215
- role TEXT NOT NULL,
1216
- content TEXT NOT NULL,
1217
- detected_language TEXT,
1218
- timestamp TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
1219
- FOREIGN KEY (session_id) REFERENCES conversations(id)
1220
- )
1221
- """)
1222
-
1223
- # User memories table - stores personal information
1224
- await db.execute("""
1225
- CREATE TABLE IF NOT EXISTS user_memories (
1226
- id INTEGER PRIMARY KEY AUTOINCREMENT,
1227
- category TEXT NOT NULL,
1228
- content TEXT NOT NULL,
1229
- confidence REAL DEFAULT 1.0,
1230
- created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
1231
- updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
1232
- source_session_id TEXT,
1233
- FOREIGN KEY (source_session_id) REFERENCES conversations(id)
1234
- )
1235
- """)
1236
-
1237
- # Create indexes for better performance
1238
- await db.execute("CREATE INDEX IF NOT EXISTS idx_memories_category ON user_memories(category)")
1239
- await db.execute("CREATE INDEX IF NOT EXISTS idx_messages_session ON messages(session_id)")
1240
-
1241
- await db.commit()
1242
-
1243
- @staticmethod
1244
- async def create_session(session_id: str):
1245
- """Create a new conversation session"""
1246
- async with aiosqlite.connect(DB_PATH) as db:
1247
- await db.execute(
1248
- "INSERT INTO conversations (id) VALUES (?)",
1249
- (session_id,)
1250
- )
1251
- await db.commit()
1252
-
1253
- @staticmethod
1254
- async def save_message(session_id: str, role: str, content: str):
1255
- """Save a message to the database"""
1256
- # Check for None or empty content
1257
- if not content:
1258
- print(f"[SAVE_MESSAGE] Empty content for {role} message, skipping")
1259
- return
1260
-
1261
- # Detect language
1262
- detected_language = None
1263
- try:
1264
- if content and len(content) > 10:
1265
- detected_language = detect(content)
1266
- except (LangDetectException, Exception) as e:
1267
- print(f"Language detection error: {e}")
1268
-
1269
- async with aiosqlite.connect(DB_PATH) as db:
1270
- await db.execute(
1271
- """INSERT INTO messages (session_id, role, content, detected_language)
1272
- VALUES (?, ?, ?, ?)""",
1273
- (session_id, role, content, detected_language)
1274
- )
1275
-
1276
- # Update conversation's updated_at timestamp
1277
- await db.execute(
1278
- "UPDATE conversations SET updated_at = CURRENT_TIMESTAMP WHERE id = ?",
1279
- (session_id,)
1280
- )
1281
-
1282
- # Update conversation summary (use first user message as summary)
1283
- if role == "user":
1284
- cursor = await db.execute(
1285
- "SELECT summary FROM conversations WHERE id = ?",
1286
- (session_id,)
1287
- )
1288
- row = await cursor.fetchone()
1289
- if row and not row[0]:
1290
- summary = content[:100] + "..." if len(content) > 100 else content
1291
- await db.execute(
1292
- "UPDATE conversations SET summary = ? WHERE id = ?",
1293
- (summary, session_id)
1294
- )
1295
-
1296
- await db.commit()
1297
-
1298
- @staticmethod
1299
- async def get_recent_conversations(limit: int = 10):
1300
- """Get recent conversations"""
1301
- async with aiosqlite.connect(DB_PATH) as db:
1302
- cursor = await db.execute(
1303
- """SELECT id, created_at, summary
1304
- FROM conversations
1305
- ORDER BY updated_at DESC
1306
- LIMIT ?""",
1307
- (limit,)
1308
- )
1309
- rows = await cursor.fetchall()
1310
- return [
1311
- {
1312
- "id": row[0],
1313
- "created_at": row[1],
1314
- "summary": row[2] or "새 대화"
1315
- }
1316
- for row in rows
1317
- ]
1318
-
1319
- @staticmethod
1320
- async def get_conversation_messages(session_id: str):
1321
- """Get all messages for a conversation"""
1322
- async with aiosqlite.connect(DB_PATH) as db:
1323
- cursor = await db.execute(
1324
- """SELECT role, content, detected_language, timestamp
1325
- FROM messages
1326
- WHERE session_id = ?
1327
- ORDER BY timestamp ASC""",
1328
- (session_id,)
1329
- )
1330
- rows = await cursor.fetchall()
1331
- return [
1332
- {
1333
- "role": row[0],
1334
- "content": row[1],
1335
- "detected_language": row[2],
1336
- "timestamp": row[3]
1337
- }
1338
- for row in rows
1339
- ]
1340
-
1341
- @staticmethod
1342
- async def save_memory(category: str, content: str, session_id: str = None, confidence: float = 1.0):
1343
- """Save or update a user memory"""
1344
- async with aiosqlite.connect(DB_PATH) as db:
1345
- # Check if similar memory exists
1346
- cursor = await db.execute(
1347
- """SELECT id, content FROM user_memories
1348
- WHERE category = ? AND content LIKE ?
1349
- LIMIT 1""",
1350
- (category, f"%{content[:20]}%")
1351
- )
1352
- existing = await cursor.fetchone()
1353
-
1354
- if existing:
1355
- # Update existing memory
1356
- await db.execute(
1357
- """UPDATE user_memories
1358
- SET content = ?, confidence = ?, updated_at = CURRENT_TIMESTAMP,
1359
- source_session_id = ?
1360
- WHERE id = ?""",
1361
- (content, confidence, session_id, existing[0])
1362
- )
1363
- else:
1364
- # Insert new memory
1365
- await db.execute(
1366
- """INSERT INTO user_memories (category, content, confidence, source_session_id)
1367
- VALUES (?, ?, ?, ?)""",
1368
- (category, content, confidence, session_id)
1369
- )
1370
-
1371
- await db.commit()
1372
-
1373
- @staticmethod
1374
- async def get_all_memories():
1375
- """Get all user memories"""
1376
- async with aiosqlite.connect(DB_PATH) as db:
1377
- cursor = await db.execute(
1378
- """SELECT category, content, confidence, updated_at
1379
- FROM user_memories
1380
- ORDER BY category, updated_at DESC"""
1381
- )
1382
- rows = await cursor.fetchall()
1383
- return [
1384
- {
1385
- "category": row[0],
1386
- "content": row[1],
1387
- "confidence": row[2],
1388
- "updated_at": row[3]
1389
- }
1390
- for row in rows
1391
- ]
1392
-
1393
- @staticmethod
1394
- async def extract_and_save_memories(session_id: str):
1395
- """Extract memories from conversation and save them"""
1396
- # Get all messages from the session
1397
- messages = await PersonalAssistantDB.get_conversation_messages(session_id)
1398
-
1399
- if not messages:
1400
- return
1401
-
1402
- # Prepare conversation text for analysis
1403
- conversation_text = "\n".join([
1404
- f"{msg['role']}: {msg['content']}"
1405
- for msg in messages if msg.get('content')
1406
- ])
1407
-
1408
- # Use GPT to extract memories
1409
- client = openai.AsyncOpenAI()
1410
-
1411
- try:
1412
- response = await client.chat.completions.create(
1413
- model="gpt-4.1-mini",
1414
- messages=[
1415
- {
1416
- "role": "system",
1417
- "content": """You are a memory extraction system. Extract personal information from conversations.
1418
-
1419
- Categories to extract:
1420
- - personal_info: 이름, 나이, 성별, 직업, 거주지
1421
- - preferences: 좋아하는 것, 싫어하는 것, 취향
1422
- - important_dates: 생일, 기념일, 중요한 날짜
1423
- - relationships: 가족, 친구, 동료 관계
1424
- - hobbies: 취미, 관심사
1425
- - health: 건강 상태, 알레르기, 의료 정보
1426
- - goals: 목표, 계획, 꿈
1427
- - routines: 일상, 습관, 루틴
1428
- - work: 직장, 업무, 프로젝트
1429
- - education: 학력, 전공, 학습
1430
-
1431
- Return as JSON array with format:
1432
- [
1433
- {
1434
- "category": "category_name",
1435
- "content": "extracted information in Korean",
1436
- "confidence": 0.0-1.0
1437
- }
1438
- ]
1439
-
1440
- Only extract clear, factual information. Do not make assumptions."""
1441
- },
1442
- {
1443
- "role": "user",
1444
- "content": f"Extract memories from this conversation:\n\n{conversation_text}"
1445
- }
1446
- ],
1447
- temperature=0.3,
1448
- max_tokens=2000
1449
- )
1450
-
1451
- # Parse and save memories
1452
- memories_text = response.choices[0].message.content
1453
-
1454
- # Extract JSON from response
1455
- import re
1456
- json_match = re.search(r'\[.*\]', memories_text, re.DOTALL)
1457
- if json_match:
1458
- memories = json.loads(json_match.group())
1459
-
1460
- for memory in memories:
1461
- if memory.get('content') and len(memory['content']) > 5:
1462
- await PersonalAssistantDB.save_memory(
1463
- category=memory.get('category', 'general'),
1464
- content=memory['content'],
1465
- session_id=session_id,
1466
- confidence=memory.get('confidence', 0.8)
1467
- )
1468
-
1469
- print(f"Extracted and saved {len(memories)} memories from session {session_id}")
1470
-
1471
- except Exception as e:
1472
- print(f"Error extracting memories: {e}")
1473
-
1474
-
1475
- # Initialize search client globally
1476
- brave_api_key = os.getenv("BSEARCH_API")
1477
- search_client = BraveSearchClient(brave_api_key) if brave_api_key else None
1478
- print(f"Search client initialized: {search_client is not None}, API key present: {bool(brave_api_key)}")
1479
-
1480
- # Store connection settings
1481
- connection_settings = {}
1482
-
1483
- # Initialize OpenAI client for text chat
1484
- client = openai.AsyncOpenAI()
1485
-
1486
-
1487
- def update_chatbot(chatbot: list[dict], response: ResponseAudioTranscriptDoneEvent):
1488
- chatbot.append({"role": "assistant", "content": response.transcript})
1489
- return chatbot
1490
-
1491
-
1492
- def format_memories_for_prompt(memories: Dict[str, List[str]]) -> str:
1493
- """Format memories for inclusion in system prompt"""
1494
- if not memories:
1495
- return ""
1496
-
1497
- memory_text = "\n\n=== 기억된 정보 ===\n"
1498
- for category, items in memories.items():
1499
- if items and isinstance(items, list):
1500
- memory_text += f"\n[{category}]\n"
1501
- for item in items:
1502
- if item: # Check if item is not None or empty
1503
- memory_text += f"- {item}\n"
1504
-
1505
- return memory_text
1506
-
1507
-
1508
- async def process_text_chat(message: str, web_search_enabled: bool, session_id: str,
1509
- user_name: str = "", memories: Dict = None) -> Dict[str, str]:
1510
- """Process text chat using GPT-4o-mini model"""
1511
- try:
1512
- # Check for empty or None message
1513
- if not message:
1514
- return {"error": "메시지가 비어있습니다."}
1515
-
1516
- # Check for stop words
1517
- stop_words = ["중단", "그만", "스톱", "stop", "닥쳐", "멈춰", "중지"]
1518
- if any(word in message.lower() for word in stop_words):
1519
- return {
1520
- "response": "대화를 중단합니다.",
1521
- "detected_language": "ko"
1522
- }
1523
-
1524
- # Build system prompt with memories
1525
- base_prompt = f"""You are a personal AI assistant for {user_name if user_name else 'the user'}.
1526
- You remember all previous conversations and personal information about the user.
1527
- Be friendly, helpful, and personalized in your responses.
1528
- Always use the information you remember to make conversations more personal and relevant.
1529
- IMPORTANT: Give only ONE response. Do not repeat or give multiple answers."""
1530
-
1531
- # Add memories to prompt
1532
- if memories:
1533
- memory_text = format_memories_for_prompt(memories)
1534
- base_prompt += memory_text
1535
-
1536
- messages = [{"role": "system", "content": base_prompt}]
1537
-
1538
- # Handle web search if enabled
1539
- if web_search_enabled and search_client and message:
1540
- search_keywords = ["날씨", "기온", "비", "눈", "뉴스", "소식", "현재", "최근",
1541
- "오늘", "지금", "가격", "환율", "주가", "weather", "news",
1542
- "current", "today", "price", "2024", "2025"]
1543
-
1544
- should_search = any(keyword in message.lower() for keyword in search_keywords)
1545
-
1546
- if should_search:
1547
- search_results = await search_client.search(message)
1548
- if search_results:
1549
- search_context = "웹 검색 결과:\n\n"
1550
- for i, result in enumerate(search_results[:5], 1):
1551
- search_context += f"{i}. {result['title']}\n{result['description']}\n\n"
1552
-
1553
- messages.append({
1554
- "role": "system",
1555
- "content": "다음 웹 검색 결과를 참고하여 답변하세요:\n\n" + search_context
1556
- })
1557
-
1558
- messages.append({"role": "user", "content": message})
1559
-
1560
- # Call GPT-4o-mini
1561
- response = await client.chat.completions.create(
1562
- model="gpt-4.1-mini",
1563
- messages=messages,
1564
- temperature=0.7,
1565
- max_tokens=2000
1566
- )
1567
-
1568
- response_text = response.choices[0].message.content
1569
-
1570
- # Detect language
1571
- detected_language = None
1572
- try:
1573
- if response_text and len(response_text) > 10:
1574
- detected_language = detect(response_text)
1575
- except:
1576
- pass
1577
-
1578
- # Save messages to database
1579
- if session_id:
1580
- await PersonalAssistantDB.save_message(session_id, "user", message)
1581
- await PersonalAssistantDB.save_message(session_id, "assistant", response_text)
1582
-
1583
- return {
1584
- "response": response_text,
1585
- "detected_language": detected_language
1586
- }
1587
-
1588
- except Exception as e:
1589
- print(f"Error in text chat: {e}")
1590
- return {"error": str(e)}
1591
-
1592
-
1593
- class OpenAIHandler(AsyncStreamHandler):
1594
- def __init__(self, web_search_enabled: bool = False, webrtc_id: str = None,
1595
- session_id: str = None, user_name: str = "", memories: Dict = None) -> None:
1596
- super().__init__(
1597
- expected_layout="mono",
1598
- output_sample_rate=SAMPLE_RATE,
1599
- output_frame_size=480,
1600
- input_sample_rate=SAMPLE_RATE,
1601
- )
1602
- self.connection = None
1603
- self.output_queue = asyncio.Queue()
1604
- self.search_client = search_client
1605
- self.function_call_in_progress = False
1606
- self.current_function_args = ""
1607
- self.current_call_id = None
1608
- self.webrtc_id = webrtc_id
1609
- self.web_search_enabled = web_search_enabled
1610
- self.session_id = session_id
1611
- self.user_name = user_name
1612
- self.memories = memories or {}
1613
- self.is_responding = False # Track if already responding
1614
- self.should_stop = False # Track if conversation should stop
1615
-
1616
- print(f"[INIT] Handler created with web_search={web_search_enabled}, session_id={session_id}, user={user_name}")
1617
-
1618
- def copy(self):
1619
- if connection_settings:
1620
- recent_ids = sorted(connection_settings.keys(),
1621
- key=lambda k: connection_settings[k].get('timestamp', 0),
1622
- reverse=True)
1623
- if recent_ids:
1624
- recent_id = recent_ids[0]
1625
- settings = connection_settings[recent_id]
1626
-
1627
- print(f"[COPY] Copying settings from {recent_id}:")
1628
-
1629
- return OpenAIHandler(
1630
- web_search_enabled=settings.get('web_search_enabled', False),
1631
- webrtc_id=recent_id,
1632
- session_id=settings.get('session_id'),
1633
- user_name=settings.get('user_name', ''),
1634
- memories=settings.get('memories', {})
1635
- )
1636
-
1637
- print(f"[COPY] No settings found, creating default handler")
1638
- return OpenAIHandler(web_search_enabled=False)
1639
-
1640
- async def search_web(self, query: str) -> str:
1641
- """Perform web search and return formatted results"""
1642
- if not self.search_client or not self.web_search_enabled:
1643
- return "웹 검색이 비활성화되어 있습니다."
1644
-
1645
- print(f"Searching web for: {query}")
1646
- results = await self.search_client.search(query)
1647
- if not results:
1648
- return f"'{query}'에 대한 검색 결과를 찾을 수 없습니다."
1649
-
1650
- formatted_results = []
1651
- for i, result in enumerate(results, 1):
1652
- formatted_results.append(
1653
- f"{i}. {result['title']}\n"
1654
- f" URL: {result['url']}\n"
1655
- f" {result['description']}\n"
1656
- )
1657
-
1658
- return f"웹 검색 결과 '{query}':\n\n" + "\n".join(formatted_results)
1659
-
1660
- async def process_text_message(self, message: str):
1661
- """Process text message from user"""
1662
- if self.connection:
1663
- await self.connection.conversation.item.create(
1664
- item={
1665
- "type": "message",
1666
- "role": "user",
1667
- "content": [{"type": "input_text", "text": message}]
1668
- }
1669
- )
1670
- await self.connection.response.create()
1671
-
1672
- async def start_up(self):
1673
- """Connect to realtime API"""
1674
- if connection_settings and self.webrtc_id:
1675
- if self.webrtc_id in connection_settings:
1676
- settings = connection_settings[self.webrtc_id]
1677
- self.web_search_enabled = settings.get('web_search_enabled', False)
1678
- self.session_id = settings.get('session_id')
1679
- self.user_name = settings.get('user_name', '')
1680
- self.memories = settings.get('memories', {})
1681
-
1682
- print(f"[START_UP] Updated settings from storage for {self.webrtc_id}")
1683
-
1684
- self.client = openai.AsyncOpenAI()
1685
-
1686
- print(f"[REALTIME API] Connecting...")
1687
-
1688
- # Build system prompt with memories
1689
- base_instructions = f"""You are a personal AI assistant for {self.user_name if self.user_name else 'the user'}.
1690
- You remember all previous conversations and personal information about the user.
1691
- Be friendly, helpful, and personalized in your responses.
1692
- Always use the information you remember to make conversations more personal and relevant.
1693
- IMPORTANT: Give only ONE response per user input. Do not repeat yourself or give multiple answers."""
1694
-
1695
- # Add memories to prompt
1696
- if self.memories:
1697
- memory_text = format_memories_for_prompt(self.memories)
1698
- base_instructions += memory_text
1699
-
1700
- # Define the web search function
1701
- tools = []
1702
- if self.web_search_enabled and self.search_client:
1703
- tools = [{
1704
- "type": "function",
1705
- "function": {
1706
- "name": "web_search",
1707
- "description": "Search the web for current information. Use this for weather, news, prices, current events, or any time-sensitive topics.",
1708
- "parameters": {
1709
- "type": "object",
1710
- "properties": {
1711
- "query": {
1712
- "type": "string",
1713
- "description": "The search query"
1714
- }
1715
- },
1716
- "required": ["query"]
1717
- }
1718
- }
1719
- }]
1720
-
1721
- search_instructions = (
1722
- "\n\nYou have web search capabilities. "
1723
- "Use web_search for current information like weather, news, prices, etc."
1724
- )
1725
-
1726
- instructions = base_instructions + search_instructions
1727
- else:
1728
- instructions = base_instructions
1729
-
1730
- async with self.client.beta.realtime.connect(
1731
- model="gpt-4o-mini-realtime-preview-2024-12-17"
1732
- ) as conn:
1733
- session_update = {
1734
- "turn_detection": {
1735
- "type": "server_vad",
1736
- "threshold": 0.5,
1737
- "prefix_padding_ms": 300,
1738
- "silence_duration_ms": 200
1739
- },
1740
- "instructions": instructions,
1741
- "tools": tools,
1742
- "tool_choice": "auto" if tools else "none",
1743
- "temperature": 0.7,
1744
- "max_response_output_tokens": 4096,
1745
- "modalities": ["text", "audio"],
1746
- "voice": "alloy"
1747
- }
1748
-
1749
- try:
1750
- await conn.session.update(session=session_update)
1751
- self.connection = conn
1752
- print(f"Connected with tools: {len(tools)} functions")
1753
- print(f"Session update successful")
1754
- except Exception as e:
1755
- print(f"Error updating session: {e}")
1756
- raise
1757
-
1758
- async for event in self.connection:
1759
- # Debug log for all events
1760
- if hasattr(event, 'type'):
1761
- if event.type not in ["response.audio.delta", "response.audio.done"]:
1762
- print(f"[EVENT] Type: {event.type}")
1763
-
1764
- # Handle user input audio transcription
1765
- if event.type == "conversation.item.input_audio_transcription.completed":
1766
- if hasattr(event, 'transcript') and event.transcript:
1767
- user_text = event.transcript.lower()
1768
- stop_words = ["중단", "그만", "스톱", "stop", "닥쳐", "멈춰", "중지"]
1769
-
1770
- if any(word in user_text for word in stop_words):
1771
- print(f"[STOP DETECTED] User said: {event.transcript}")
1772
- self.should_stop = True
1773
- if self.connection:
1774
- try:
1775
- await self.connection.response.cancel()
1776
- except:
1777
- pass
1778
- continue
1779
-
1780
- # Save user message to database
1781
- if self.session_id:
1782
- await PersonalAssistantDB.save_message(self.session_id, "user", event.transcript)
1783
-
1784
- # Handle user transcription for stop detection (alternative event)
1785
- elif event.type == "conversation.item.created":
1786
- if hasattr(event, 'item') and hasattr(event.item, 'role') and event.item.role == "user":
1787
- if hasattr(event.item, 'content') and event.item.content:
1788
- for content_item in event.item.content:
1789
- if hasattr(content_item, 'transcript') and content_item.transcript:
1790
- user_text = content_item.transcript.lower()
1791
- stop_words = ["중단", "그만", "스톱", "stop", "닥쳐", "멈춰", "중지"]
1792
-
1793
- if any(word in user_text for word in stop_words):
1794
- print(f"[STOP DETECTED] User said: {content_item.transcript}")
1795
- self.should_stop = True
1796
- if self.connection:
1797
- try:
1798
- await self.connection.response.cancel()
1799
- except:
1800
- pass
1801
- continue
1802
-
1803
- # Save user message to database
1804
- if self.session_id:
1805
- await PersonalAssistantDB.save_message(self.session_id, "user", content_item.transcript)
1806
-
1807
- elif event.type == "response.audio_transcript.done":
1808
- # Prevent multiple responses
1809
- if self.is_responding:
1810
- print("[DUPLICATE RESPONSE] Skipping duplicate response")
1811
- continue
1812
-
1813
- self.is_responding = True
1814
- print(f"[RESPONSE] Transcript: {event.transcript[:100] if event.transcript else 'None'}...")
1815
-
1816
- # Detect language
1817
- detected_language = None
1818
- try:
1819
- if event.transcript and len(event.transcript) > 10:
1820
- detected_language = detect(event.transcript)
1821
- except Exception as e:
1822
- print(f"Language detection error: {e}")
1823
-
1824
- # Save to database
1825
- if self.session_id and event.transcript:
1826
- await PersonalAssistantDB.save_message(self.session_id, "assistant", event.transcript)
1827
-
1828
- output_data = {
1829
- "event": event,
1830
- "detected_language": detected_language
1831
- }
1832
- await self.output_queue.put(AdditionalOutputs(output_data))
1833
-
1834
- elif event.type == "response.done":
1835
- # Reset responding flag when response is complete
1836
- self.is_responding = False
1837
- self.should_stop = False
1838
- print("[RESPONSE DONE] Response completed")
1839
-
1840
- elif event.type == "response.audio.delta":
1841
- # Check if we should stop
1842
- if self.should_stop:
1843
- continue
1844
-
1845
- if hasattr(event, 'delta'):
1846
- await self.output_queue.put(
1847
- (
1848
- self.output_sample_rate,
1849
- np.frombuffer(
1850
- base64.b64decode(event.delta), dtype=np.int16
1851
- ).reshape(1, -1),
1852
- ),
1853
- )
1854
-
1855
- # Handle errors
1856
- elif event.type == "error":
1857
- print(f"[ERROR] {event}")
1858
- self.is_responding = False
1859
-
1860
- # Handle function calls
1861
- elif event.type == "response.function_call_arguments.start":
1862
- print(f"Function call started")
1863
- self.function_call_in_progress = True
1864
- self.current_function_args = ""
1865
- self.current_call_id = getattr(event, 'call_id', None)
1866
-
1867
- elif event.type == "response.function_call_arguments.delta":
1868
- if self.function_call_in_progress:
1869
- self.current_function_args += event.delta
1870
-
1871
- elif event.type == "response.function_call_arguments.done":
1872
- if self.function_call_in_progress:
1873
- print(f"Function call done, args: {self.current_function_args}")
1874
- try:
1875
- args = json.loads(self.current_function_args)
1876
- query = args.get("query", "")
1877
-
1878
- # Emit search event to client
1879
- await self.output_queue.put(AdditionalOutputs({
1880
- "type": "search",
1881
- "query": query
1882
- }))
1883
-
1884
- # Perform the search
1885
- search_results = await self.search_web(query)
1886
- print(f"Search results length: {len(search_results)}")
1887
-
1888
- # Send function result back to the model
1889
- if self.connection and self.current_call_id:
1890
- await self.connection.conversation.item.create(
1891
- item={
1892
- "type": "function_call_output",
1893
- "call_id": self.current_call_id,
1894
- "output": search_results
1895
- }
1896
- )
1897
- await self.connection.response.create()
1898
-
1899
- except Exception as e:
1900
- print(f"Function call error: {e}")
1901
- finally:
1902
- self.function_call_in_progress = False
1903
- self.current_function_args = ""
1904
- self.current_call_id = None
1905
-
1906
- async def receive(self, frame: tuple[int, np.ndarray]) -> None:
1907
- if not self.connection:
1908
- print(f"[RECEIVE] No connection, skipping")
1909
- return
1910
- try:
1911
- if frame is None or len(frame) < 2:
1912
- print(f"[RECEIVE] Invalid frame")
1913
- return
1914
-
1915
- _, array = frame
1916
- if array is None:
1917
- print(f"[RECEIVE] Null array")
1918
- return
1919
-
1920
- array = array.squeeze()
1921
- audio_message = base64.b64encode(array.tobytes()).decode("utf-8")
1922
- await self.connection.input_audio_buffer.append(audio=audio_message)
1923
- except Exception as e:
1924
- print(f"Error in receive: {e}")
1925
-
1926
- async def emit(self) -> tuple[int, np.ndarray] | AdditionalOutputs | None:
1927
- item = await wait_for_item(self.output_queue)
1928
-
1929
- if isinstance(item, dict) and item.get('type') == 'text_message':
1930
- await self.process_text_message(item['content'])
1931
- return None
1932
-
1933
- return item
1934
-
1935
- async def shutdown(self) -> None:
1936
- print(f"[SHUTDOWN] Called")
1937
-
1938
- if self.connection:
1939
- await self.connection.close()
1940
- self.connection = None
1941
- print("[REALTIME API] Connection closed")
1942
-
1943
-
1944
- # Create initial handler instance
1945
- handler = OpenAIHandler(web_search_enabled=False)
1946
-
1947
- # Create components
1948
- chatbot = gr.Chatbot(type="messages")
1949
-
1950
- # Create stream with handler instance
1951
- stream = Stream(
1952
- handler,
1953
- mode="send-receive",
1954
- modality="audio",
1955
- additional_inputs=[chatbot],
1956
- additional_outputs=[chatbot],
1957
- additional_outputs_handler=update_chatbot,
1958
- rtc_configuration=get_twilio_turn_credentials() if get_space() else None,
1959
- concurrency_limit=5 if get_space() else None,
1960
- time_limit=300 if get_space() else None,
1961
- )
1962
-
1963
- app = FastAPI()
1964
-
1965
- # Mount stream
1966
- stream.mount(app)
1967
-
1968
- # Initialize database on startup
1969
- @app.on_event("startup")
1970
- async def startup_event():
1971
- try:
1972
- await PersonalAssistantDB.init()
1973
- print(f"Database initialized at: {DB_PATH}")
1974
- print(f"Persistent directory: {PERSISTENT_DIR}")
1975
- print(f"DB file exists: {os.path.exists(DB_PATH)}")
1976
-
1977
- # Check if we're in Hugging Face Space
1978
- if os.path.exists("/data"):
1979
- print("Running in Hugging Face Space with persistent storage")
1980
- # List files in persistent directory
1981
- try:
1982
- files = os.listdir(PERSISTENT_DIR)
1983
- print(f"Files in persistent directory: {files}")
1984
- except Exception as e:
1985
- print(f"Error listing files: {e}")
1986
- except Exception as e:
1987
- print(f"Error during startup: {e}")
1988
- # Try to create directory if it doesn't exist
1989
- os.makedirs(PERSISTENT_DIR, exist_ok=True)
1990
- await PersonalAssistantDB.init()
1991
-
1992
- # Intercept offer to capture settings
1993
- @app.post("/webrtc/offer", include_in_schema=False)
1994
- async def custom_offer(request: Request):
1995
- """Intercept offer to capture settings"""
1996
- body = await request.json()
1997
-
1998
- webrtc_id = body.get("webrtc_id")
1999
- web_search_enabled = body.get("web_search_enabled", False)
2000
- session_id = body.get("session_id")
2001
- user_name = body.get("user_name", "")
2002
- memories = body.get("memories", {})
2003
-
2004
- print(f"[OFFER] Received offer with webrtc_id: {webrtc_id}")
2005
- print(f"[OFFER] web_search_enabled: {web_search_enabled}")
2006
- print(f"[OFFER] session_id: {session_id}")
2007
- print(f"[OFFER] user_name: {user_name}")
2008
-
2009
- # Store settings with timestamp
2010
- if webrtc_id:
2011
- connection_settings[webrtc_id] = {
2012
- 'web_search_enabled': web_search_enabled,
2013
- 'session_id': session_id,
2014
- 'user_name': user_name,
2015
- 'memories': memories,
2016
- 'timestamp': asyncio.get_event_loop().time()
2017
- }
2018
-
2019
- print(f"[OFFER] Stored settings for {webrtc_id}")
2020
-
2021
- # Remove our custom route temporarily
2022
- custom_route = None
2023
- for i, route in enumerate(app.routes):
2024
- if hasattr(route, 'path') and route.path == "/webrtc/offer" and route.endpoint == custom_offer:
2025
- custom_route = app.routes.pop(i)
2026
- break
2027
-
2028
- # Forward to stream's offer handler
2029
- print(f"[OFFER] Forwarding to stream.offer()")
2030
- response = await stream.offer(body)
2031
-
2032
- # Re-add our custom route
2033
- if custom_route:
2034
- app.routes.insert(0, custom_route)
2035
-
2036
- print(f"[OFFER] Response status: {response.get('status', 'unknown') if isinstance(response, dict) else 'OK'}")
2037
-
2038
- return response
2039
-
2040
-
2041
- @app.post("/session/new")
2042
- async def create_new_session():
2043
- """Create a new chat session"""
2044
- session_id = str(uuid.uuid4())
2045
- await PersonalAssistantDB.create_session(session_id)
2046
- return {"session_id": session_id}
2047
-
2048
-
2049
- @app.post("/session/end")
2050
- async def end_session(request: Request):
2051
- """End session and extract memories"""
2052
- body = await request.json()
2053
- session_id = body.get("session_id")
2054
-
2055
- if not session_id:
2056
- return {"error": "session_id required"}
2057
-
2058
- # Extract and save memories from the conversation
2059
- await PersonalAssistantDB.extract_and_save_memories(session_id)
2060
-
2061
- return {"status": "ok"}
2062
-
2063
-
2064
- @app.post("/message/save")
2065
- async def save_message(request: Request):
2066
- """Save a message to the database"""
2067
- body = await request.json()
2068
- session_id = body.get("session_id")
2069
- role = body.get("role")
2070
- content = body.get("content")
2071
-
2072
- if not all([session_id, role, content]):
2073
- return {"error": "Missing required fields"}
2074
-
2075
- await PersonalAssistantDB.save_message(session_id, role, content)
2076
- return {"status": "ok"}
2077
-
2078
-
2079
- @app.get("/history/recent")
2080
- async def get_recent_history():
2081
- """Get recent conversation history"""
2082
- conversations = await PersonalAssistantDB.get_recent_conversations()
2083
- return conversations
2084
-
2085
-
2086
- @app.get("/history/{session_id}")
2087
- async def get_conversation(session_id: str):
2088
- """Get messages for a specific conversation"""
2089
- messages = await PersonalAssistantDB.get_conversation_messages(session_id)
2090
- return messages
2091
-
2092
-
2093
- @app.get("/memory/all")
2094
- async def get_all_memories():
2095
- """Get all user memories"""
2096
- memories = await PersonalAssistantDB.get_all_memories()
2097
- return memories
2098
-
2099
-
2100
- @app.post("/chat/text")
2101
- async def chat_text(request: Request):
2102
- """Handle text chat messages using GPT-4o-mini"""
2103
- try:
2104
- body = await request.json()
2105
- message = body.get("message", "")
2106
- web_search_enabled = body.get("web_search_enabled", False)
2107
- session_id = body.get("session_id")
2108
- user_name = body.get("user_name", "")
2109
- memories = body.get("memories", {})
2110
-
2111
- if not message:
2112
- return {"error": "메시지가 비어있습니다."}
2113
-
2114
- # Process text chat
2115
- result = await process_text_chat(message, web_search_enabled, session_id, user_name, memories)
2116
-
2117
- return result
2118
-
2119
- except Exception as e:
2120
- print(f"Error in chat_text endpoint: {e}")
2121
- return {"error": "채팅 처리 중 오류가 발생했습니다."}
2122
-
2123
-
2124
- @app.post("/text_message/{webrtc_id}")
2125
- async def receive_text_message(webrtc_id: str, request: Request):
2126
- """Receive text message from client"""
2127
- body = await request.json()
2128
- message = body.get("content", "")
2129
-
2130
- # Find the handler for this connection
2131
- if webrtc_id in stream.handlers:
2132
- handler = stream.handlers[webrtc_id]
2133
- # Queue the text message for processing
2134
- await handler.output_queue.put({
2135
- 'type': 'text_message',
2136
- 'content': message
2137
- })
2138
-
2139
- return {"status": "ok"}
2140
-
2141
-
2142
- @app.get("/outputs")
2143
- async def outputs(webrtc_id: str):
2144
- """Stream outputs including search events"""
2145
- async def output_stream():
2146
- async for output in stream.output_stream(webrtc_id):
2147
- if hasattr(output, 'args') and output.args:
2148
- # Check if it's a search event
2149
- if isinstance(output.args[0], dict) and output.args[0].get('type') == 'search':
2150
- yield f"event: search\ndata: {json.dumps(output.args[0])}\n\n"
2151
- # Regular transcript event with language info
2152
- elif isinstance(output.args[0], dict) and 'event' in output.args[0]:
2153
- event_data = output.args[0]
2154
- if 'event' in event_data and hasattr(event_data['event'], 'transcript'):
2155
- data = {
2156
- "role": "assistant",
2157
- "content": event_data['event'].transcript,
2158
- "detected_language": event_data.get('detected_language')
2159
- }
2160
- yield f"event: output\ndata: {json.dumps(data)}\n\n"
2161
-
2162
- return StreamingResponse(output_stream(), media_type="text/event-stream")
2163
-
2164
-
2165
- @app.get("/")
2166
- async def index():
2167
- """Serve the HTML page"""
2168
- rtc_config = get_twilio_turn_credentials() if get_space() else None
2169
- html_content = HTML_CONTENT.replace("__RTC_CONFIGURATION__", json.dumps(rtc_config))
2170
- return HTMLResponse(content=html_content)
2171
-
2172
-
2173
- if __name__ == "__main__":
2174
- import uvicorn
2175
-
2176
- mode = os.getenv("MODE")
2177
- if mode == "UI":
2178
- stream.ui.launch(server_port=7860)
2179
- elif mode == "PHONE":
2180
- stream.fastphone(host="0.0.0.0", port=7860)
2181
- else:
2182
- uvicorn.run(app, host="0.0.0.0", port=7860)