Spaces:
Running
Running
Update app.py
Browse files
app.py
CHANGED
@@ -88,22 +88,46 @@ HTML_CONTENT = """<!DOCTYPE html>
|
|
88 |
overflow: hidden;
|
89 |
}
|
90 |
.container {
|
91 |
-
max-width:
|
92 |
margin: 0 auto;
|
93 |
padding: 20px;
|
94 |
flex-grow: 1;
|
95 |
display: flex;
|
96 |
flex-direction: column;
|
97 |
width: 100%;
|
98 |
-
height:
|
99 |
box-sizing: border-box;
|
|
|
100 |
}
|
101 |
.header {
|
102 |
text-align: center;
|
103 |
-
padding:
|
104 |
border-bottom: 1px solid var(--border-color);
|
105 |
margin-bottom: 20px;
|
106 |
flex-shrink: 0;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
107 |
}
|
108 |
.logo {
|
109 |
display: flex;
|
@@ -125,18 +149,19 @@ HTML_CONTENT = """<!DOCTYPE html>
|
|
125 |
background-color: var(--card-bg);
|
126 |
border-radius: 12px;
|
127 |
padding: 20px;
|
128 |
-
margin-bottom: 20px;
|
129 |
border: 1px solid var(--border-color);
|
|
|
|
|
130 |
}
|
131 |
.settings-grid {
|
132 |
-
display:
|
133 |
-
|
134 |
gap: 15px;
|
135 |
margin-bottom: 15px;
|
136 |
}
|
137 |
.interpretation-section {
|
138 |
display: flex;
|
139 |
-
|
140 |
gap: 15px;
|
141 |
padding: 15px;
|
142 |
background-color: var(--dark-bg);
|
@@ -151,12 +176,13 @@ HTML_CONTENT = """<!DOCTYPE html>
|
|
151 |
.setting-item {
|
152 |
display: flex;
|
153 |
align-items: center;
|
|
|
154 |
gap: 10px;
|
155 |
}
|
156 |
.setting-label {
|
157 |
font-size: 14px;
|
158 |
color: #aaa;
|
159 |
-
min-width:
|
160 |
}
|
161 |
/* Toggle switch */
|
162 |
.toggle-switch {
|
@@ -193,7 +219,8 @@ HTML_CONTENT = """<!DOCTYPE html>
|
|
193 |
border-radius: 6px;
|
194 |
font-size: 14px;
|
195 |
cursor: pointer;
|
196 |
-
min-width:
|
|
|
197 |
}
|
198 |
select:focus {
|
199 |
outline: none;
|
@@ -233,14 +260,16 @@ HTML_CONTENT = """<!DOCTYPE html>
|
|
233 |
border: 1px solid var(--border-color);
|
234 |
overflow: hidden;
|
235 |
min-height: 0;
|
|
|
236 |
}
|
237 |
.chat-messages {
|
238 |
flex-grow: 1;
|
239 |
overflow-y: auto;
|
240 |
-
padding:
|
241 |
scrollbar-width: thin;
|
242 |
scrollbar-color: var(--primary-color) var(--card-bg);
|
243 |
min-height: 0;
|
|
|
244 |
}
|
245 |
.chat-messages::-webkit-scrollbar {
|
246 |
width: 6px;
|
@@ -250,14 +279,15 @@ HTML_CONTENT = """<!DOCTYPE html>
|
|
250 |
border-radius: 6px;
|
251 |
}
|
252 |
.message {
|
253 |
-
margin-bottom:
|
254 |
-
padding:
|
255 |
border-radius: 8px;
|
256 |
-
font-size:
|
257 |
-
line-height: 1.
|
258 |
position: relative;
|
259 |
-
max-width:
|
260 |
animation: fade-in 0.3s ease-out;
|
|
|
261 |
}
|
262 |
@keyframes fade-in {
|
263 |
from {
|
@@ -287,11 +317,30 @@ HTML_CONTENT = """<!DOCTYPE html>
|
|
287 |
}
|
288 |
.controls {
|
289 |
text-align: center;
|
290 |
-
margin-top:
|
291 |
display: flex;
|
292 |
justify-content: center;
|
293 |
gap: 10px;
|
294 |
flex-shrink: 0;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
295 |
}
|
296 |
button {
|
297 |
background: linear-gradient(135deg, var(--primary-color), var(--secondary-color));
|
@@ -323,6 +372,7 @@ HTML_CONTENT = """<!DOCTYPE html>
|
|
323 |
background: linear-gradient(135deg, #2ecc71, #27ae60);
|
324 |
padding: 10px 20px;
|
325 |
font-size: 14px;
|
|
|
326 |
}
|
327 |
#send-button:hover {
|
328 |
background: linear-gradient(135deg, #27ae60, #229954);
|
@@ -477,112 +527,123 @@ HTML_CONTENT = """<!DOCTYPE html>
|
|
477 |
</div>
|
478 |
</div>
|
479 |
|
480 |
-
<div class="
|
481 |
-
<div class="
|
482 |
-
<div class="
|
483 |
-
<
|
484 |
-
<div
|
485 |
-
<div class="
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
486 |
</div>
|
487 |
</div>
|
488 |
-
|
489 |
-
|
490 |
-
<
|
491 |
-
<option value="">비활성화</option>
|
492 |
-
<option value="ko">한국어 (Korean)</option>
|
493 |
-
<option value="en">English</option>
|
494 |
-
<option value="es">Español (Spanish)</option>
|
495 |
-
<option value="fr">Français (French)</option>
|
496 |
-
<option value="de">Deutsch (German)</option>
|
497 |
-
<option value="it">Italiano (Italian)</option>
|
498 |
-
<option value="pt">Português (Portuguese)</option>
|
499 |
-
<option value="ru">Русский (Russian)</option>
|
500 |
-
<option value="ja">日本語 (Japanese)</option>
|
501 |
-
<option value="zh">中文 (Chinese)</option>
|
502 |
-
<option value="ar">العربية (Arabic)</option>
|
503 |
-
<option value="hi">हिन्दी (Hindi)</option>
|
504 |
-
<option value="nl">Nederlands (Dutch)</option>
|
505 |
-
<option value="pl">Polski (Polish)</option>
|
506 |
-
<option value="tr">Türkçe (Turkish)</option>
|
507 |
-
<option value="vi">Tiếng Việt (Vietnamese)</option>
|
508 |
-
<option value="th">ไทย (Thai)</option>
|
509 |
-
<option value="id">Bahasa Indonesia</option>
|
510 |
-
<option value="sv">Svenska (Swedish)</option>
|
511 |
-
<option value="da">Dansk (Danish)</option>
|
512 |
-
<option value="no">Norsk (Norwegian)</option>
|
513 |
-
<option value="fi">Suomi (Finnish)</option>
|
514 |
-
<option value="he">עברית (Hebrew)</option>
|
515 |
-
<option value="uk">Українська (Ukrainian)</option>
|
516 |
-
<option value="cs">Čeština (Czech)</option>
|
517 |
-
<option value="el">Ελληνικά (Greek)</option>
|
518 |
-
<option value="ro">Română (Romanian)</option>
|
519 |
-
<option value="hu">Magyar (Hungarian)</option>
|
520 |
-
<option value="ms">Bahasa Melayu (Malay)</option>
|
521 |
-
</select>
|
522 |
</div>
|
523 |
</div>
|
524 |
-
|
525 |
-
|
526 |
-
|
527 |
-
<
|
528 |
-
|
|
|
|
|
|
|
|
|
|
|
529 |
</div>
|
530 |
</div>
|
531 |
-
<div class="setting-item" id="interpretation-language-container" style="display: none;">
|
532 |
-
<span class="setting-label">통역 언어</span>
|
533 |
-
<select id="interpretation-language-select">
|
534 |
-
<option value="">언어 선택</option>
|
535 |
-
<option value="ko">한국어 (Korean)</option>
|
536 |
-
<option value="en">English</option>
|
537 |
-
<option value="es">Español (Spanish)</option>
|
538 |
-
<option value="fr">Français (French)</option>
|
539 |
-
<option value="de">Deutsch (German)</option>
|
540 |
-
<option value="it">Italiano (Italian)</option>
|
541 |
-
<option value="pt">Português (Portuguese)</option>
|
542 |
-
<option value="ru">Русский (Russian)</option>
|
543 |
-
<option value="ja">日本語 (Japanese)</option>
|
544 |
-
<option value="zh">中文 (Chinese)</option>
|
545 |
-
<option value="ar">العربية (Arabic)</option>
|
546 |
-
<option value="hi">हिन्दी (Hindi)</option>
|
547 |
-
<option value="nl">Nederlands (Dutch)</option>
|
548 |
-
<option value="pl">Polski (Polish)</option>
|
549 |
-
<option value="tr">Türkçe (Turkish)</option>
|
550 |
-
<option value="vi">Tiếng Việt (Vietnamese)</option>
|
551 |
-
<option value="th">ไทย (Thai)</option>
|
552 |
-
<option value="id">Bahasa Indonesia</option>
|
553 |
-
<option value="sv">Svenska (Swedish)</option>
|
554 |
-
<option value="da">Dansk (Danish)</option>
|
555 |
-
<option value="no">Norsk (Norwegian)</option>
|
556 |
-
<option value="fi">Suomi (Finnish)</option>
|
557 |
-
<option value="he">עברית (Hebrew)</option>
|
558 |
-
<option value="uk">Українська (Ukrainian)</option>
|
559 |
-
<option value="cs">Čeština (Czech)</option>
|
560 |
-
<option value="el">Ελληνικά (Greek)</option>
|
561 |
-
<option value="ro">Română (Romanian)</option>
|
562 |
-
<option value="hu">Magyar (Hungarian)</option>
|
563 |
-
<option value="ms">Bahasa Melayu (Malay)</option>
|
564 |
-
</select>
|
565 |
-
</div>
|
566 |
-
</div>
|
567 |
-
<div class="interpretation-info" id="interpretation-info" style="display: none;">
|
568 |
-
통역 모드: 입력한 음성이 선택한 언어로 자동 통역됩니다.
|
569 |
-
</div>
|
570 |
-
<div class="text-input-section">
|
571 |
-
<label for="system-prompt" class="setting-label">시스템 프롬프트:</label>
|
572 |
-
<textarea id="system-prompt" placeholder="AI 어시스턴트의 성격, 역할, 행동 방식을 정의하세요...">You are a helpful assistant. Respond in a friendly and professional manner.</textarea>
|
573 |
-
</div>
|
574 |
-
</div>
|
575 |
-
|
576 |
-
<div class="chat-container">
|
577 |
-
<div class="chat-messages" id="chat-messages"></div>
|
578 |
-
<div class="text-input-section" style="margin-top: 10px;">
|
579 |
-
<input type="text" id="text-input" placeholder="텍스트 메시지를 입력하세요..." />
|
580 |
</div>
|
581 |
</div>
|
582 |
-
<div class="controls">
|
583 |
-
<button id="start-button">대화 시작</button>
|
584 |
-
<button id="send-button" style="display: none;">전송</button>
|
585 |
-
</div>
|
586 |
</div>
|
587 |
<audio id="audio-output"></audio>
|
588 |
|
@@ -629,24 +690,35 @@ HTML_CONTENT = """<!DOCTYPE html>
|
|
629 |
|
630 |
// Interpretation mode toggle
|
631 |
interpretationToggle.addEventListener('click', () => {
|
632 |
-
|
633 |
-
|
634 |
-
|
635 |
-
|
636 |
-
|
637 |
-
|
638 |
-
|
639 |
-
|
640 |
-
|
641 |
-
|
642 |
-
|
643 |
-
webSearchEnabled = false;
|
644 |
-
searchToggle.style.opacity = '0.5';
|
645 |
-
searchToggle.style.pointerEvents = 'none';
|
646 |
} else {
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
647 |
languageSelect.disabled = false;
|
648 |
searchToggle.style.opacity = '1';
|
649 |
searchToggle.style.pointerEvents = 'auto';
|
|
|
|
|
|
|
|
|
|
|
650 |
}
|
651 |
|
652 |
console.log('Interpretation mode:', interpretationMode);
|
@@ -656,6 +728,26 @@ HTML_CONTENT = """<!DOCTYPE html>
|
|
656 |
interpretationLanguageSelect.addEventListener('change', () => {
|
657 |
interpretationLanguage = interpretationLanguageSelect.value;
|
658 |
console.log('Interpretation language:', interpretationLanguage);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
659 |
});
|
660 |
|
661 |
// System prompt update
|
@@ -736,14 +828,18 @@ HTML_CONTENT = """<!DOCTYPE html>
|
|
736 |
statusDot.className = 'status-dot ' + state;
|
737 |
if (state === 'connected') {
|
738 |
statusText.textContent = '연결됨';
|
739 |
-
|
|
|
|
|
740 |
isVoiceActive = true;
|
741 |
} else if (state === 'connecting') {
|
742 |
statusText.textContent = '연결 중...';
|
743 |
sendButton.style.display = 'none';
|
744 |
} else {
|
745 |
statusText.textContent = '연결 대기 중';
|
746 |
-
|
|
|
|
|
747 |
isVoiceActive = false;
|
748 |
}
|
749 |
}
|
@@ -979,6 +1075,11 @@ HTML_CONTENT = """<!DOCTYPE html>
|
|
979 |
stop();
|
980 |
}
|
981 |
});
|
|
|
|
|
|
|
|
|
|
|
982 |
</script>
|
983 |
</body>
|
984 |
|
@@ -1195,12 +1296,32 @@ class OpenAIHandler(AsyncStreamHandler):
|
|
1195 |
return ""
|
1196 |
|
1197 |
target_language_name = SUPPORTED_LANGUAGES.get(self.interpretation_language, self.interpretation_language)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1198 |
return (
|
1199 |
-
f"
|
1200 |
-
f"You
|
1201 |
-
f"
|
1202 |
-
f"
|
1203 |
-
f"
|
|
|
|
|
|
|
|
|
|
|
|
|
1204 |
)
|
1205 |
|
1206 |
def get_translation_instructions(self):
|
@@ -1234,9 +1355,11 @@ class OpenAIHandler(AsyncStreamHandler):
|
|
1234 |
print(f"start_up: Updated settings from storage - webrtc_id={self.webrtc_id}, "
|
1235 |
f"web_search_enabled={self.web_search_enabled}, target_language={self.target_language}, "
|
1236 |
f"interpretation_mode={self.interpretation_mode}")
|
|
|
1237 |
|
1238 |
print(f"Starting up handler with web_search_enabled={self.web_search_enabled}, "
|
1239 |
-
f"target_language={self.target_language}, interpretation_mode={self.interpretation_mode}"
|
|
|
1240 |
self.client = openai.AsyncOpenAI()
|
1241 |
|
1242 |
# Define the web search function
|
@@ -1246,10 +1369,17 @@ class OpenAIHandler(AsyncStreamHandler):
|
|
1246 |
# Check if in interpretation mode
|
1247 |
if self.interpretation_mode:
|
1248 |
# In interpretation mode, override all instructions
|
|
|
|
|
|
|
|
|
|
|
|
|
1249 |
interpretation_instructions = self.get_interpretation_instructions()
|
1250 |
-
instructions = interpretation_instructions
|
1251 |
# No tools in interpretation mode
|
1252 |
tools = []
|
|
|
1253 |
else:
|
1254 |
# Normal mode - add translation instructions if language is selected
|
1255 |
translation_instructions = self.get_translation_instructions()
|
@@ -1296,7 +1426,12 @@ class OpenAIHandler(AsyncStreamHandler):
|
|
1296 |
) as conn:
|
1297 |
# Update session with tools
|
1298 |
session_update = {
|
1299 |
-
"turn_detection": {
|
|
|
|
|
|
|
|
|
|
|
1300 |
"instructions": instructions,
|
1301 |
"tools": tools,
|
1302 |
"tool_choice": "auto" if tools else "none"
|
@@ -1305,21 +1440,27 @@ class OpenAIHandler(AsyncStreamHandler):
|
|
1305 |
# Add voice setting based on interpretation or translation language
|
1306 |
voice_language = self.interpretation_language if self.interpretation_mode else self.target_language
|
1307 |
if voice_language:
|
1308 |
-
#
|
1309 |
-
|
1310 |
-
|
1311 |
-
|
1312 |
-
|
1313 |
-
|
1314 |
-
"
|
1315 |
-
|
1316 |
-
|
1317 |
-
|
1318 |
-
|
|
|
|
|
|
|
|
|
|
|
1319 |
|
1320 |
await conn.session.update(session=session_update)
|
1321 |
self.connection = conn
|
1322 |
-
print(f"Connected with tools: {len(tools)} functions, voice: {session_update.get('voice', 'default')}"
|
|
|
1323 |
|
1324 |
async for event in self.connection:
|
1325 |
# Debug logging for function calls
|
@@ -1346,18 +1487,18 @@ class OpenAIHandler(AsyncStreamHandler):
|
|
1346 |
),
|
1347 |
)
|
1348 |
|
1349 |
-
# Handle function calls
|
1350 |
-
elif event.type == "response.function_call_arguments.start":
|
1351 |
print(f"Function call started")
|
1352 |
self.function_call_in_progress = True
|
1353 |
self.current_function_args = ""
|
1354 |
self.current_call_id = getattr(event, 'call_id', None)
|
1355 |
|
1356 |
-
elif event.type == "response.function_call_arguments.delta":
|
1357 |
if self.function_call_in_progress:
|
1358 |
self.current_function_args += event.delta
|
1359 |
|
1360 |
-
elif event.type == "response.function_call_arguments.done":
|
1361 |
if self.function_call_in_progress:
|
1362 |
print(f"Function call done, args: {self.current_function_args}")
|
1363 |
try:
|
@@ -1421,7 +1562,7 @@ class OpenAIHandler(AsyncStreamHandler):
|
|
1421 |
|
1422 |
|
1423 |
# Create initial handler instance
|
1424 |
-
handler = OpenAIHandler(web_search_enabled=False)
|
1425 |
|
1426 |
# Create components
|
1427 |
chatbot = gr.Chatbot(type="messages")
|
|
|
88 |
overflow: hidden;
|
89 |
}
|
90 |
.container {
|
91 |
+
max-width: 1400px;
|
92 |
margin: 0 auto;
|
93 |
padding: 20px;
|
94 |
flex-grow: 1;
|
95 |
display: flex;
|
96 |
flex-direction: column;
|
97 |
width: 100%;
|
98 |
+
height: 100vh;
|
99 |
box-sizing: border-box;
|
100 |
+
overflow: hidden;
|
101 |
}
|
102 |
.header {
|
103 |
text-align: center;
|
104 |
+
padding: 15px 0;
|
105 |
border-bottom: 1px solid var(--border-color);
|
106 |
margin-bottom: 20px;
|
107 |
flex-shrink: 0;
|
108 |
+
background-color: var(--card-bg);
|
109 |
+
}
|
110 |
+
.main-content {
|
111 |
+
display: flex;
|
112 |
+
gap: 20px;
|
113 |
+
flex-grow: 1;
|
114 |
+
min-height: 0;
|
115 |
+
overflow: hidden;
|
116 |
+
}
|
117 |
+
.sidebar {
|
118 |
+
width: 350px;
|
119 |
+
flex-shrink: 0;
|
120 |
+
display: flex;
|
121 |
+
flex-direction: column;
|
122 |
+
gap: 20px;
|
123 |
+
overflow-y: auto;
|
124 |
+
max-height: calc(100vh - 120px);
|
125 |
+
}
|
126 |
+
.chat-section {
|
127 |
+
flex-grow: 1;
|
128 |
+
display: flex;
|
129 |
+
flex-direction: column;
|
130 |
+
min-width: 0;
|
131 |
}
|
132 |
.logo {
|
133 |
display: flex;
|
|
|
149 |
background-color: var(--card-bg);
|
150 |
border-radius: 12px;
|
151 |
padding: 20px;
|
|
|
152 |
border: 1px solid var(--border-color);
|
153 |
+
overflow-y: auto;
|
154 |
+
flex-grow: 1;
|
155 |
}
|
156 |
.settings-grid {
|
157 |
+
display: flex;
|
158 |
+
flex-direction: column;
|
159 |
gap: 15px;
|
160 |
margin-bottom: 15px;
|
161 |
}
|
162 |
.interpretation-section {
|
163 |
display: flex;
|
164 |
+
flex-direction: column;
|
165 |
gap: 15px;
|
166 |
padding: 15px;
|
167 |
background-color: var(--dark-bg);
|
|
|
176 |
.setting-item {
|
177 |
display: flex;
|
178 |
align-items: center;
|
179 |
+
justify-content: space-between;
|
180 |
gap: 10px;
|
181 |
}
|
182 |
.setting-label {
|
183 |
font-size: 14px;
|
184 |
color: #aaa;
|
185 |
+
min-width: 60px;
|
186 |
}
|
187 |
/* Toggle switch */
|
188 |
.toggle-switch {
|
|
|
219 |
border-radius: 6px;
|
220 |
font-size: 14px;
|
221 |
cursor: pointer;
|
222 |
+
min-width: 120px;
|
223 |
+
max-width: 200px;
|
224 |
}
|
225 |
select:focus {
|
226 |
outline: none;
|
|
|
260 |
border: 1px solid var(--border-color);
|
261 |
overflow: hidden;
|
262 |
min-height: 0;
|
263 |
+
height: 100%;
|
264 |
}
|
265 |
.chat-messages {
|
266 |
flex-grow: 1;
|
267 |
overflow-y: auto;
|
268 |
+
padding: 15px;
|
269 |
scrollbar-width: thin;
|
270 |
scrollbar-color: var(--primary-color) var(--card-bg);
|
271 |
min-height: 0;
|
272 |
+
max-height: calc(100vh - 250px);
|
273 |
}
|
274 |
.chat-messages::-webkit-scrollbar {
|
275 |
width: 6px;
|
|
|
279 |
border-radius: 6px;
|
280 |
}
|
281 |
.message {
|
282 |
+
margin-bottom: 15px;
|
283 |
+
padding: 12px 16px;
|
284 |
border-radius: 8px;
|
285 |
+
font-size: 15px;
|
286 |
+
line-height: 1.5;
|
287 |
position: relative;
|
288 |
+
max-width: 85%;
|
289 |
animation: fade-in 0.3s ease-out;
|
290 |
+
word-wrap: break-word;
|
291 |
}
|
292 |
@keyframes fade-in {
|
293 |
from {
|
|
|
317 |
}
|
318 |
.controls {
|
319 |
text-align: center;
|
320 |
+
margin-top: auto;
|
321 |
display: flex;
|
322 |
justify-content: center;
|
323 |
gap: 10px;
|
324 |
flex-shrink: 0;
|
325 |
+
padding-top: 20px;
|
326 |
+
}
|
327 |
+
/* Responsive design */
|
328 |
+
@media (max-width: 1024px) {
|
329 |
+
.sidebar {
|
330 |
+
width: 300px;
|
331 |
+
}
|
332 |
+
}
|
333 |
+
@media (max-width: 768px) {
|
334 |
+
.main-content {
|
335 |
+
flex-direction: column;
|
336 |
+
}
|
337 |
+
.sidebar {
|
338 |
+
width: 100%;
|
339 |
+
margin-bottom: 20px;
|
340 |
+
}
|
341 |
+
.chat-section {
|
342 |
+
height: 400px;
|
343 |
+
}
|
344 |
}
|
345 |
button {
|
346 |
background: linear-gradient(135deg, var(--primary-color), var(--secondary-color));
|
|
|
372 |
background: linear-gradient(135deg, #2ecc71, #27ae60);
|
373 |
padding: 10px 20px;
|
374 |
font-size: 14px;
|
375 |
+
flex-shrink: 0;
|
376 |
}
|
377 |
#send-button:hover {
|
378 |
background: linear-gradient(135deg, #27ae60, #229954);
|
|
|
527 |
</div>
|
528 |
</div>
|
529 |
|
530 |
+
<div class="main-content">
|
531 |
+
<div class="sidebar">
|
532 |
+
<div class="settings-section">
|
533 |
+
<h3 style="margin: 0 0 15px 0; color: var(--primary-color);">설정</h3>
|
534 |
+
<div class="settings-grid">
|
535 |
+
<div class="setting-item">
|
536 |
+
<span class="setting-label">웹 검색</span>
|
537 |
+
<div id="search-toggle" class="toggle-switch">
|
538 |
+
<div class="toggle-slider"></div>
|
539 |
+
</div>
|
540 |
+
</div>
|
541 |
+
<div class="setting-item">
|
542 |
+
<span class="setting-label">자동 번역</span>
|
543 |
+
<select id="language-select">
|
544 |
+
<option value="">비활성화</option>
|
545 |
+
<option value="ko">한국어 (Korean)</option>
|
546 |
+
<option value="en">English</option>
|
547 |
+
<option value="es">Español (Spanish)</option>
|
548 |
+
<option value="fr">Français (French)</option>
|
549 |
+
<option value="de">Deutsch (German)</option>
|
550 |
+
<option value="it">Italiano (Italian)</option>
|
551 |
+
<option value="pt">Português (Portuguese)</option>
|
552 |
+
<option value="ru">Русский (Russian)</option>
|
553 |
+
<option value="ja">日本語 (Japanese)</option>
|
554 |
+
<option value="zh">中文 (Chinese)</option>
|
555 |
+
<option value="ar">العربية (Arabic)</option>
|
556 |
+
<option value="hi">हिन्दी (Hindi)</option>
|
557 |
+
<option value="nl">Nederlands (Dutch)</option>
|
558 |
+
<option value="pl">Polski (Polish)</option>
|
559 |
+
<option value="tr">Türkçe (Turkish)</option>
|
560 |
+
<option value="vi">Tiếng Việt (Vietnamese)</option>
|
561 |
+
<option value="th">ไทย (Thai)</option>
|
562 |
+
<option value="id">Bahasa Indonesia</option>
|
563 |
+
<option value="sv">Svenska (Swedish)</option>
|
564 |
+
<option value="da">Dansk (Danish)</option>
|
565 |
+
<option value="no">Norsk (Norwegian)</option>
|
566 |
+
<option value="fi">Suomi (Finnish)</option>
|
567 |
+
<option value="he">עברית (Hebrew)</option>
|
568 |
+
<option value="uk">Українська (Ukrainian)</option>
|
569 |
+
<option value="cs">Čeština (Czech)</option>
|
570 |
+
<option value="el">Ελληνικά (Greek)</option>
|
571 |
+
<option value="ro">Română (Romanian)</option>
|
572 |
+
<option value="hu">Magyar (Hungarian)</option>
|
573 |
+
<option value="ms">Bahasa Melayu (Malay)</option>
|
574 |
+
</select>
|
575 |
+
</div>
|
576 |
+
</div>
|
577 |
+
<div class="interpretation-section">
|
578 |
+
<div class="setting-item">
|
579 |
+
<span class="setting-label">자동 통역</span>
|
580 |
+
<div id="interpretation-toggle" class="toggle-switch">
|
581 |
+
<div class="toggle-slider"></div>
|
582 |
+
</div>
|
583 |
+
</div>
|
584 |
+
<div class="setting-item" id="interpretation-language-container" style="display: none;">
|
585 |
+
<span class="setting-label">통역 언어</span>
|
586 |
+
<select id="interpretation-language-select">
|
587 |
+
<option value="">언어 선택</option>
|
588 |
+
<option value="ko">한국어 (Korean)</option>
|
589 |
+
<option value="en">English</option>
|
590 |
+
<option value="es">Español (Spanish)</option>
|
591 |
+
<option value="fr">Français (French)</option>
|
592 |
+
<option value="de">Deutsch (German)</option>
|
593 |
+
<option value="it">Italiano (Italian)</option>
|
594 |
+
<option value="pt">Português (Portuguese)</option>
|
595 |
+
<option value="ru">Русский (Russian)</option>
|
596 |
+
<option value="ja">日本語 (Japanese)</option>
|
597 |
+
<option value="zh">中文 (Chinese)</option>
|
598 |
+
<option value="ar">العربية (Arabic)</option>
|
599 |
+
<option value="hi">हिन्दी (Hindi)</option>
|
600 |
+
<option value="nl">Nederlands (Dutch)</option>
|
601 |
+
<option value="pl">Polski (Polish)</option>
|
602 |
+
<option value="tr">Türkçe (Turkish)</option>
|
603 |
+
<option value="vi">Tiếng Việt (Vietnamese)</option>
|
604 |
+
<option value="th">ไทย (Thai)</option>
|
605 |
+
<option value="id">Bahasa Indonesia</option>
|
606 |
+
<option value="sv">Svenska (Swedish)</option>
|
607 |
+
<option value="da">Dansk (Danish)</option>
|
608 |
+
<option value="no">Norsk (Norwegian)</option>
|
609 |
+
<option value="fi">Suomi (Finnish)</option>
|
610 |
+
<option value="he">עברית (Hebrew)</option>
|
611 |
+
<option value="uk">Українська (Ukrainian)</option>
|
612 |
+
<option value="cs">Čeština (Czech)</option>
|
613 |
+
<option value="el">Ελληνικά (Greek)</option>
|
614 |
+
<option value="ro">Română (Romanian)</option>
|
615 |
+
<option value="hu">Magyar (Hungarian)</option>
|
616 |
+
<option value="ms">Bahasa Melayu (Malay)</option>
|
617 |
+
</select>
|
618 |
+
</div>
|
619 |
+
</div>
|
620 |
+
<div class="interpretation-info" id="interpretation-info" style="display: none;">
|
621 |
+
통역 모드: 입력한 음성이 선택한 언어로 자동 통역됩니다.
|
622 |
+
</div>
|
623 |
+
<div class="text-input-section">
|
624 |
+
<label for="system-prompt" class="setting-label">시스템 프롬프트:</label>
|
625 |
+
<textarea id="system-prompt" placeholder="AI 어시스턴트의 성격, 역할, 행동 방식을 정의하세요...">You are a helpful assistant. Respond in a friendly and professional manner.</textarea>
|
626 |
</div>
|
627 |
</div>
|
628 |
+
|
629 |
+
<div class="controls">
|
630 |
+
<button id="start-button">대화 시작</button>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
631 |
</div>
|
632 |
</div>
|
633 |
+
|
634 |
+
<div class="chat-section">
|
635 |
+
<div class="chat-container">
|
636 |
+
<h3 style="margin: 0 0 15px 0; color: var(--primary-color);">대화</h3>
|
637 |
+
<div class="chat-messages" id="chat-messages"></div>
|
638 |
+
<div class="text-input-section" style="margin-top: 10px;">
|
639 |
+
<div style="display: flex; gap: 10px;">
|
640 |
+
<input type="text" id="text-input" placeholder="텍스트 메시지를 입력하세요..." style="flex-grow: 1;" />
|
641 |
+
<button id="send-button" style="display: none;">전송</button>
|
642 |
+
</div>
|
643 |
</div>
|
644 |
</div>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
645 |
</div>
|
646 |
</div>
|
|
|
|
|
|
|
|
|
647 |
</div>
|
648 |
<audio id="audio-output"></audio>
|
649 |
|
|
|
690 |
|
691 |
// Interpretation mode toggle
|
692 |
interpretationToggle.addEventListener('click', () => {
|
693 |
+
if (!interpretationMode) {
|
694 |
+
// Turning ON interpretation mode
|
695 |
+
interpretationLanguageContainer.style.display = 'flex';
|
696 |
+
interpretationInfo.style.display = 'block';
|
697 |
+
|
698 |
+
// Show language selector first
|
699 |
+
showError('통역 언어를 선택해주세요.');
|
700 |
+
interpretationToggle.classList.remove('active');
|
701 |
+
|
702 |
+
// Don't actually enable interpretation mode until language is selected
|
703 |
+
return;
|
|
|
|
|
|
|
704 |
} else {
|
705 |
+
// Turning OFF interpretation mode
|
706 |
+
interpretationMode = false;
|
707 |
+
interpretationToggle.classList.remove('active');
|
708 |
+
interpretationLanguageContainer.style.display = 'none';
|
709 |
+
interpretationInfo.style.display = 'none';
|
710 |
+
interpretationLanguage = '';
|
711 |
+
interpretationLanguageSelect.value = '';
|
712 |
+
|
713 |
+
// Re-enable other features
|
714 |
languageSelect.disabled = false;
|
715 |
searchToggle.style.opacity = '1';
|
716 |
searchToggle.style.pointerEvents = 'auto';
|
717 |
+
textInput.disabled = false;
|
718 |
+
textInput.placeholder = '텍스트 메시지를 입력하세요...';
|
719 |
+
sendButton.style.display = 'block';
|
720 |
+
|
721 |
+
console.log('Interpretation mode disabled');
|
722 |
}
|
723 |
|
724 |
console.log('Interpretation mode:', interpretationMode);
|
|
|
728 |
interpretationLanguageSelect.addEventListener('change', () => {
|
729 |
interpretationLanguage = interpretationLanguageSelect.value;
|
730 |
console.log('Interpretation language:', interpretationLanguage);
|
731 |
+
|
732 |
+
if (interpretationLanguage && !interpretationMode) {
|
733 |
+
// Now actually enable interpretation mode
|
734 |
+
interpretationMode = true;
|
735 |
+
interpretationToggle.classList.add('active');
|
736 |
+
|
737 |
+
// Disable other features
|
738 |
+
languageSelect.value = '';
|
739 |
+
selectedLanguage = '';
|
740 |
+
languageSelect.disabled = true;
|
741 |
+
searchToggle.classList.remove('active');
|
742 |
+
webSearchEnabled = false;
|
743 |
+
searchToggle.style.opacity = '0.5';
|
744 |
+
searchToggle.style.pointerEvents = 'none';
|
745 |
+
textInput.disabled = true;
|
746 |
+
textInput.placeholder = '통역 모드에서는 텍스트 입력이 지원되지 않습니다';
|
747 |
+
sendButton.style.display = 'none';
|
748 |
+
|
749 |
+
console.log('Interpretation mode enabled with language:', interpretationLanguage);
|
750 |
+
}
|
751 |
});
|
752 |
|
753 |
// System prompt update
|
|
|
828 |
statusDot.className = 'status-dot ' + state;
|
829 |
if (state === 'connected') {
|
830 |
statusText.textContent = '연결됨';
|
831 |
+
if (!interpretationMode) {
|
832 |
+
sendButton.style.display = 'block';
|
833 |
+
}
|
834 |
isVoiceActive = true;
|
835 |
} else if (state === 'connecting') {
|
836 |
statusText.textContent = '연결 중...';
|
837 |
sendButton.style.display = 'none';
|
838 |
} else {
|
839 |
statusText.textContent = '연결 대기 중';
|
840 |
+
if (!interpretationMode) {
|
841 |
+
sendButton.style.display = 'block'; // Show send button even when disconnected for text chat
|
842 |
+
}
|
843 |
isVoiceActive = false;
|
844 |
}
|
845 |
}
|
|
|
1075 |
stop();
|
1076 |
}
|
1077 |
});
|
1078 |
+
|
1079 |
+
// Initialize send button visibility on page load
|
1080 |
+
window.addEventListener('DOMContentLoaded', () => {
|
1081 |
+
sendButton.style.display = 'block';
|
1082 |
+
});
|
1083 |
</script>
|
1084 |
</body>
|
1085 |
|
|
|
1296 |
return ""
|
1297 |
|
1298 |
target_language_name = SUPPORTED_LANGUAGES.get(self.interpretation_language, self.interpretation_language)
|
1299 |
+
target_code = self.interpretation_language
|
1300 |
+
|
1301 |
+
# Language-specific examples
|
1302 |
+
examples = {
|
1303 |
+
"en": "Hello, the weather is nice today",
|
1304 |
+
"es": "Hola, el clima está agradable hoy",
|
1305 |
+
"fr": "Bonjour, il fait beau aujourd'hui",
|
1306 |
+
"de": "Hallo, das Wetter ist heute schön",
|
1307 |
+
"ja": "こんにちは、今日はいい天気ですね",
|
1308 |
+
"zh": "你好,今天天气很好"
|
1309 |
+
}
|
1310 |
+
|
1311 |
+
example_translation = examples.get(target_code, "Hello, the weather is nice today")
|
1312 |
+
|
1313 |
return (
|
1314 |
+
f"INTERPRETATION MODE - CRITICAL RULES:\n\n"
|
1315 |
+
f"1. You are ONLY a translator to {target_language_name} (language code: {target_code}).\n"
|
1316 |
+
f"2. NEVER respond in any other language.\n"
|
1317 |
+
f"3. NEVER generate conversation or additional content.\n"
|
1318 |
+
f"4. ONLY translate what the user says.\n"
|
1319 |
+
f"5. STOP immediately after translating.\n\n"
|
1320 |
+
f"Example:\n"
|
1321 |
+
f"If user says: '안녕하세요, 오늘 날씨가 좋네요'\n"
|
1322 |
+
f"You MUST respond ONLY: '{example_translation}'\n\n"
|
1323 |
+
f"DO NOT say anything else. DO NOT continue talking.\n"
|
1324 |
+
f"Your output language MUST be {target_language_name} ONLY."
|
1325 |
)
|
1326 |
|
1327 |
def get_translation_instructions(self):
|
|
|
1355 |
print(f"start_up: Updated settings from storage - webrtc_id={self.webrtc_id}, "
|
1356 |
f"web_search_enabled={self.web_search_enabled}, target_language={self.target_language}, "
|
1357 |
f"interpretation_mode={self.interpretation_mode}")
|
1358 |
+
print(f"Handler interpretation settings: mode={self.interpretation_mode}, language={self.interpretation_language}")
|
1359 |
|
1360 |
print(f"Starting up handler with web_search_enabled={self.web_search_enabled}, "
|
1361 |
+
f"target_language={self.target_language}, interpretation_mode={self.interpretation_mode}, "
|
1362 |
+
f"interpretation_language={self.interpretation_language}")
|
1363 |
self.client = openai.AsyncOpenAI()
|
1364 |
|
1365 |
# Define the web search function
|
|
|
1369 |
# Check if in interpretation mode
|
1370 |
if self.interpretation_mode:
|
1371 |
# In interpretation mode, override all instructions
|
1372 |
+
base_instructions = (
|
1373 |
+
f"You are a professional interpreter. Your ONLY task is to translate what the user says "
|
1374 |
+
f"into {SUPPORTED_LANGUAGES.get(self.interpretation_language, self.interpretation_language)}. "
|
1375 |
+
f"Do not add any commentary, do not continue the conversation, do not generate new content. "
|
1376 |
+
f"Simply translate what was said and stop."
|
1377 |
+
)
|
1378 |
interpretation_instructions = self.get_interpretation_instructions()
|
1379 |
+
instructions = base_instructions + "\n\n" + interpretation_instructions
|
1380 |
# No tools in interpretation mode
|
1381 |
tools = []
|
1382 |
+
print(f"Interpretation mode active - target language: {self.interpretation_language}")
|
1383 |
else:
|
1384 |
# Normal mode - add translation instructions if language is selected
|
1385 |
translation_instructions = self.get_translation_instructions()
|
|
|
1426 |
) as conn:
|
1427 |
# Update session with tools
|
1428 |
session_update = {
|
1429 |
+
"turn_detection": {
|
1430 |
+
"type": "server_vad",
|
1431 |
+
"threshold": 0.5,
|
1432 |
+
"prefix_padding_ms": 300,
|
1433 |
+
"silence_duration_ms": 500 if self.interpretation_mode else 700
|
1434 |
+
},
|
1435 |
"instructions": instructions,
|
1436 |
"tools": tools,
|
1437 |
"tool_choice": "auto" if tools else "none"
|
|
|
1440 |
# Add voice setting based on interpretation or translation language
|
1441 |
voice_language = self.interpretation_language if self.interpretation_mode else self.target_language
|
1442 |
if voice_language:
|
1443 |
+
# Use only alloy voice to avoid language confusion
|
1444 |
+
# The model will handle the language based on instructions
|
1445 |
+
session_update["voice"] = "alloy"
|
1446 |
+
|
1447 |
+
# For interpretation mode, explicitly set the output language
|
1448 |
+
if self.interpretation_mode:
|
1449 |
+
session_update["output_audio_format"] = "pcm16"
|
1450 |
+
|
1451 |
+
print(f"Voice set to: alloy for language: {voice_language}")
|
1452 |
+
|
1453 |
+
# For interpretation mode, ensure proper language settings
|
1454 |
+
if self.interpretation_mode and self.interpretation_language:
|
1455 |
+
session_update["modalities"] = ["text", "audio"]
|
1456 |
+
session_update["temperature"] = 0.3 # Lower temperature for more accurate translation
|
1457 |
+
session_update["max_response_output_tokens"] = 500 # Limit output to prevent long generations
|
1458 |
+
print(f"Interpretation session config: voice={session_update.get('voice')}, lang={self.interpretation_language}")
|
1459 |
|
1460 |
await conn.session.update(session=session_update)
|
1461 |
self.connection = conn
|
1462 |
+
print(f"Connected with tools: {len(tools)} functions, voice: {session_update.get('voice', 'default')}, "
|
1463 |
+
f"interpretation_mode: {self.interpretation_mode}, language: {self.interpretation_language if self.interpretation_mode else self.target_language}")
|
1464 |
|
1465 |
async for event in self.connection:
|
1466 |
# Debug logging for function calls
|
|
|
1487 |
),
|
1488 |
)
|
1489 |
|
1490 |
+
# Handle function calls (only in non-interpretation mode)
|
1491 |
+
elif event.type == "response.function_call_arguments.start" and not self.interpretation_mode:
|
1492 |
print(f"Function call started")
|
1493 |
self.function_call_in_progress = True
|
1494 |
self.current_function_args = ""
|
1495 |
self.current_call_id = getattr(event, 'call_id', None)
|
1496 |
|
1497 |
+
elif event.type == "response.function_call_arguments.delta" and not self.interpretation_mode:
|
1498 |
if self.function_call_in_progress:
|
1499 |
self.current_function_args += event.delta
|
1500 |
|
1501 |
+
elif event.type == "response.function_call_arguments.done" and not self.interpretation_mode:
|
1502 |
if self.function_call_in_progress:
|
1503 |
print(f"Function call done, args: {self.current_function_args}")
|
1504 |
try:
|
|
|
1562 |
|
1563 |
|
1564 |
# Create initial handler instance
|
1565 |
+
handler = OpenAIHandler(web_search_enabled=False, interpretation_mode=False)
|
1566 |
|
1567 |
# Create components
|
1568 |
chatbot = gr.Chatbot(type="messages")
|