SJLee-0525 commited on
Commit
d26ae30
ยท
1 Parent(s): 0a634ef

[TEST] test41

Browse files
.backend copy.pid ADDED
@@ -0,0 +1 @@
 
 
1
+ 2909266
client/app.py CHANGED
@@ -530,7 +530,7 @@ with demo:
530
  # Hugging Face Spaces ๋ฐ ๋กœ์ปฌ ์‹คํ–‰ ๋ชจ๋‘ ์ง€์›
531
  # Spaces์—์„œ๋Š” demo.launch()๊ฐ€ ์ž๋™ ํ˜ธ์ถœ๋˜์ง€๋งŒ, allowed_paths ์„ค์ •์„ ์œ„ํ•ด ์ง์ ‘ ํ˜ธ์ถœ
532
  server_host = os.getenv("SERVER_HOST", "0.0.0.0")
533
- frontend_port = int(os.getenv("FRONTEND_PORT", 7860))
534
 
535
  # Gradio ์‹คํ–‰ (CSS ์ ์šฉ)
536
  demo.launch(
 
530
  # Hugging Face Spaces ๋ฐ ๋กœ์ปฌ ์‹คํ–‰ ๋ชจ๋‘ ์ง€์›
531
  # Spaces์—์„œ๋Š” demo.launch()๊ฐ€ ์ž๋™ ํ˜ธ์ถœ๋˜์ง€๋งŒ, allowed_paths ์„ค์ •์„ ์œ„ํ•ด ์ง์ ‘ ํ˜ธ์ถœ
532
  server_host = os.getenv("SERVER_HOST", "0.0.0.0")
533
+ frontend_port = int(os.getenv("FRONTEND_PORT"))
534
 
535
  # Gradio ์‹คํ–‰ (CSS ์ ์šฉ)
536
  demo.launch(
client/frontend/app_ui.py CHANGED
@@ -23,6 +23,7 @@ from frontend.components.failure_modal import FailureModalComponent
23
  from frontend.components.success_screen import SuccessScreenComponent
24
  from frontend.components.giveup_screen import GiveUpScreenComponent
25
  from frontend.components.floating_chatbot import FloatingChatbotComponent
 
26
  from frontend.styles.custom_css import get_all_css # Gradio 6: launch()์—์„œ ์‚ฌ์šฉ
27
 
28
  # ๊ฒŒ์ž„ ์ƒํƒœ ๊ด€๋ฆฌ (UUID, ์„ธ์…˜ ๋“ฑ)
@@ -47,6 +48,7 @@ class AppUI:
47
  self.success_screen = SuccessScreenComponent()
48
  self.giveup_screen = GiveUpScreenComponent()
49
  self.floating_chatbot = FloatingChatbotComponent()
 
50
 
51
  # ์ƒํƒœ ๋ณ€์ˆ˜
52
  self.failure_history = None
@@ -134,6 +136,9 @@ class AppUI:
134
  # ============== ํ”Œ๋กœํŒ… AI ์ฑ—๋ด‡ (UUID ๊ณต์œ ) ==============
135
  self.floating_chatbot.render(game_state=self.game_state)
136
 
 
 
 
137
  # ๋ฐ˜ํ™˜ํ•  ์ปดํฌ๋„ŒํŠธ๋“ค
138
  components = {
139
  'demo': demo,
 
23
  from frontend.components.success_screen import SuccessScreenComponent
24
  from frontend.components.giveup_screen import GiveUpScreenComponent
25
  from frontend.components.floating_chatbot import FloatingChatbotComponent
26
+ from frontend.components.welcome_modal import WelcomeModal
27
  from frontend.styles.custom_css import get_all_css # Gradio 6: launch()์—์„œ ์‚ฌ์šฉ
28
 
29
  # ๊ฒŒ์ž„ ์ƒํƒœ ๊ด€๋ฆฌ (UUID, ์„ธ์…˜ ๋“ฑ)
 
48
  self.success_screen = SuccessScreenComponent()
49
  self.giveup_screen = GiveUpScreenComponent()
50
  self.floating_chatbot = FloatingChatbotComponent()
51
+ self.welcome_modal = WelcomeModal()
52
 
53
  # ์ƒํƒœ ๋ณ€์ˆ˜
54
  self.failure_history = None
 
136
  # ============== ํ”Œ๋กœํŒ… AI ์ฑ—๋ด‡ (UUID ๊ณต์œ ) ==============
137
  self.floating_chatbot.render(game_state=self.game_state)
138
 
139
+ # ============== ์›ฐ์ปด ๋ชจ๋‹ฌ (์ฒซ ๋ฐฉ๋ฌธ ์‹œ YouTube ํŠœํ† ๋ฆฌ์–ผ) ==============
140
+ self.welcome_modal.render()
141
+
142
  # ๋ฐ˜ํ™˜ํ•  ์ปดํฌ๋„ŒํŠธ๋“ค
143
  components = {
144
  'demo': demo,
client/frontend/components/floating_chatbot.py CHANGED
@@ -505,8 +505,23 @@ class FloatingChatbotComponent:
505
  Args:
506
  game_state: ์™ธ๋ถ€์—์„œ ์ „๋‹ฌ๋ฐ›์€ gr.BrowserState (UUID ํฌํ•จ)
507
  """
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
508
  # ์ƒํƒœ ๊ด€๋ฆฌ
509
- self.chat_history = gr.State([])
510
  self.game_state_ref = game_state # ์™ธ๋ถ€ game_state ์ฐธ์กฐ ์ €์žฅ
511
 
512
  # ํ† ๊ธ€ ๋ฒ„ํŠผ (Checkbox) - JavaScript๋กœ visibility ์ œ์–ด
@@ -531,9 +546,9 @@ class FloatingChatbotComponent:
531
  min_width=32
532
  )
533
 
534
- # ์ฑ—๋ด‡
535
  chatbot = gr.Chatbot(
536
- value=[],
537
  height=280,
538
  elem_id="floating-chatbot",
539
  show_label=False
 
505
  Args:
506
  game_state: ์™ธ๋ถ€์—์„œ ์ „๋‹ฌ๋ฐ›์€ gr.BrowserState (UUID ํฌํ•จ)
507
  """
508
+ # ์ดˆ๊ธฐ ํ™˜์˜ ๋ฉ”์‹œ์ง€ ์ •์˜
509
+ welcome_message = """Welcome to Audio Semantle! ๐Ÿ‘‹ Get ready for a unique and super fun pronunciation puzzle game where your voice is the key!
510
+
511
+ Here's how it works:
512
+
513
+ 1. **You start completely blind!** You have absolutely NO idea what the mystery word or phrase is. That's part of the fun!
514
+ 2. **Your first move:** Just speak any word or phrase that comes to mind. Seriously, anything!
515
+ 3. **Get instant feedback:** Our AI will listen and give you scores (0-100) for your Pitch, Rhythm, Energy, Pronunciation, and an Overall similarity score to the hidden target.
516
+ 4. **Deduce and discover:** Based on these scores, and with my help offering progressive hints, you'll start narrowing down what the target phrase might be.
517
+ 5. **Unlimited attempts:** This isn't like Wordle! You have as many tries as you need to figure it out. It's all about discovery and getting closer with each attempt.
518
+
519
+ Don't overthink your first try โ€“ it's meant to be a shot in the dark! Just say anything you like, and let's see where that takes us. Ready to give it a go? What's the first thing that comes to your mind?"""
520
+
521
+ self.initial_history = [{"role": "assistant", "content": welcome_message}]
522
+
523
  # ์ƒํƒœ ๊ด€๋ฆฌ
524
+ self.chat_history = gr.State(self.initial_history.copy())
525
  self.game_state_ref = game_state # ์™ธ๋ถ€ game_state ์ฐธ์กฐ ์ €์žฅ
526
 
527
  # ํ† ๊ธ€ ๋ฒ„ํŠผ (Checkbox) - JavaScript๋กœ visibility ์ œ์–ด
 
546
  min_width=32
547
  )
548
 
549
+ # ์ฑ—๋ด‡ (์ดˆ๊ธฐ ํ™˜์˜ ๋ฉ”์‹œ์ง€ ํฌํ•จ)
550
  chatbot = gr.Chatbot(
551
+ value=self.initial_history,
552
  height=280,
553
  elem_id="floating-chatbot",
554
  show_label=False
client/frontend/components/welcome_modal.py ADDED
@@ -0,0 +1,306 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ ์›ฐ์ปด ๋ชจ๋‹ฌ ์ปดํฌ๋„ŒํŠธ - YouTube ํŠœํ† ๋ฆฌ์–ผ ์˜์ƒ ํŒ์—…
3
+ ์ฒซ ๋ฐฉ๋ฌธ ์‹œ ์ž๋™์œผ๋กœ ํ‘œ์‹œ, "๋‹ค์‹œ ๋ณด์ง€ ์•Š๊ธฐ" ๊ธฐ๋Šฅ ์ง€์›
4
+
5
+ localStorage ํ‚ค: 'welcome_modal_dismissed' (๊ฒŒ์ž„ ์ƒํƒœ์™€ ๋…๋ฆฝ์ )
6
+ """
7
+
8
+ import gradio as gr
9
+
10
+
11
+ class WelcomeModal:
12
+ """
13
+ ์›ฐ์ปด ๋ชจ๋‹ฌ - YouTube ํŠœํ† ๋ฆฌ์–ผ ์˜์ƒ ํ‘œ์‹œ
14
+
15
+ - ์ฒซ ๋ฐฉ๋ฌธ ์‹œ ์ž๋™ ํ‘œ์‹œ
16
+ - "๋‹ค์‹œ ๋ณด์ง€ ์•Š๊ธฐ" ํด๋ฆญ ์‹œ localStorage์— ์ €์žฅ
17
+ - ๊ฒŒ์ž„ ์ดˆ๊ธฐํ™”์™€ ๋…๋ฆฝ์ ์œผ๋กœ ์„ค์ • ์œ ์ง€
18
+ """
19
+
20
+ # YouTube ์˜์ƒ ID
21
+ YOUTUBE_VIDEO_ID = "YbCf6x0B3fU"
22
+
23
+ # localStorage ํ‚ค (๊ฒŒ์ž„ ์ƒํƒœ์™€ ๋ณ„๋„)
24
+ STORAGE_KEY = "welcome_modal_dismissed"
25
+
26
+ HTML_TEMPLATE = """
27
+ ${value && value.visible ? `
28
+ <div class="welcome-modal-overlay" data-welcome-overlay="true">
29
+ <div class="welcome-modal-container">
30
+ <div class="welcome-modal-content">
31
+ <h2 class="welcome-modal-title">Welcome to Voice Semantle!</h2>
32
+
33
+ <div class="welcome-modal-video">
34
+ <iframe
35
+ src="https://www.youtube.com/embed/${value.videoId || 'YbCf6x0B3fU'}?rel=0&modestbranding=1"
36
+ title="Voice Semantle Tutorial"
37
+ frameborder="0"
38
+ allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share"
39
+ referrerpolicy="strict-origin-when-cross-origin"
40
+ allowfullscreen>
41
+ </iframe>
42
+ </div>
43
+
44
+ <div class="welcome-modal-actions">
45
+ <button class="welcome-modal-btn welcome-modal-btn-primary" data-welcome-close="true">
46
+ Start Playing
47
+ </button>
48
+ <button class="welcome-modal-btn welcome-modal-btn-secondary" data-welcome-dismiss="true">
49
+ Don't show again
50
+ </button>
51
+ </div>
52
+ </div>
53
+ </div>
54
+ </div>
55
+ ` : ''}
56
+ """
57
+
58
+ CSS_TEMPLATE = """
59
+ /* ์›ฐ์ปด ๋ชจ๋‹ฌ - ์ „์—ญ ์Šคํƒ€์ผ์— ์˜ํ–ฅ ์—†๋„๋ก ์Šค์ฝ”ํ•‘ */
60
+ .welcome-modal-overlay {
61
+ position: fixed !important;
62
+ top: 0 !important;
63
+ left: 0 !important;
64
+ right: 0 !important;
65
+ bottom: 0 !important;
66
+ width: 100vw !important;
67
+ height: 100vh !important;
68
+ background-color: rgba(0, 0, 0, 0.7) !important;
69
+ backdrop-filter: blur(4px);
70
+ z-index: 9999 !important;
71
+ display: flex !important;
72
+ justify-content: center !important;
73
+ align-items: center !important;
74
+ padding: 20px;
75
+ box-sizing: border-box;
76
+ font-family: inherit;
77
+ }
78
+
79
+ .welcome-modal-container {
80
+ width: 100%;
81
+ max-width: 1280px;
82
+ max-height: 90vh;
83
+ overflow-y: auto;
84
+ position: relative;
85
+ animation: welcomeModalFadeIn 0.3s ease-out;
86
+ font-family: inherit;
87
+ }
88
+
89
+ @keyframes welcomeModalFadeIn {
90
+ from {
91
+ opacity: 0;
92
+ transform: scale(0.9) translateY(-20px);
93
+ }
94
+ to {
95
+ opacity: 1;
96
+ transform: scale(1) translateY(0);
97
+ }
98
+ }
99
+
100
+ .welcome-modal-close {
101
+ position: absolute;
102
+ top: 16px;
103
+ right: 16px;
104
+ background: rgba(255, 255, 255, 0.1);
105
+ border: 1px solid rgba(255, 255, 255, 0.2);
106
+ border-radius: 50%;
107
+ width: 40px;
108
+ height: 40px;
109
+ cursor: pointer;
110
+ display: flex;
111
+ align-items: center;
112
+ justify-content: center;
113
+ color: #ffffff;
114
+ transition: all 0.2s ease;
115
+ z-index: 10;
116
+ padding: 0;
117
+ font-family: inherit;
118
+ }
119
+
120
+ .welcome-modal-close:hover {
121
+ background: rgba(255, 255, 255, 0.2);
122
+ transform: scale(1.1);
123
+ }
124
+
125
+ .welcome-modal-content {
126
+ padding: 32px;
127
+ text-align: center;
128
+ font-family: inherit;
129
+ }
130
+
131
+ .welcome-modal-title {
132
+ font-size: 60px;
133
+ font-weight: 700;
134
+ color: #ffffff;
135
+ margin: 0 0 24px 0;
136
+ font-family: inherit;
137
+ }
138
+
139
+ .welcome-modal-subtitle {
140
+ font-size: 16px;
141
+ color: rgba(255, 255, 255, 0.7);
142
+ margin: 0 0 24px 0;
143
+ font-family: inherit;
144
+ }
145
+
146
+ .welcome-modal-video {
147
+ position: relative;
148
+ width: 100%;
149
+ padding-bottom: 56.25%; /* 16:9 aspect ratio */
150
+ border-radius: 16px;
151
+ overflow: hidden;
152
+ background: #000;
153
+ margin-bottom: 24px;
154
+ }
155
+
156
+ .welcome-modal-video iframe {
157
+ position: absolute;
158
+ top: 0;
159
+ left: 0;
160
+ width: 100%;
161
+ height: 100%;
162
+ }
163
+
164
+ .welcome-modal-actions {
165
+ display: flex;
166
+ justify-content: center;
167
+ gap: 12px;
168
+ align-items: center;
169
+ }
170
+
171
+ .welcome-modal-btn {
172
+ padding: 14px 32px;
173
+ border-radius: 20px;
174
+ font-size: 16px;
175
+ font-weight: 600;
176
+ cursor: pointer;
177
+ transition: all 0.2s ease;
178
+ border: none;
179
+ min-width: 200px;
180
+ font-family: inherit;
181
+ }
182
+
183
+ .welcome-modal-btn-primary {
184
+ background: #4db8ff !important;
185
+ color: #ffffff !important;
186
+ }
187
+
188
+ .welcome-modal-btn-primary:hover {
189
+ transform: translateY(-2px);
190
+ }
191
+
192
+ .welcome-modal-btn-secondary {
193
+ background: transparent;
194
+ color: rgba(255, 255, 255, 0.6);
195
+ border: 1px solid rgba(255, 255, 255, 0.2);
196
+ }
197
+
198
+ .welcome-modal-btn-secondary:hover {
199
+ background: rgba(255, 255, 255, 0.1);
200
+ color: rgba(255, 255, 255, 0.9);
201
+ }
202
+
203
+ /* Mobile responsive */
204
+ @media (max-width: 600px) {
205
+ .welcome-modal-container {
206
+ max-width: 100%;
207
+ border-radius: 16px;
208
+ }
209
+
210
+ .welcome-modal-content {
211
+ padding: 24px 16px;
212
+ }
213
+
214
+ .welcome-modal-title {
215
+ font-size: 22px;
216
+ }
217
+
218
+ .welcome-modal-btn {
219
+ width: 100%;
220
+ min-width: unset;
221
+ }
222
+ }
223
+ """
224
+
225
+ JS_ON_LOAD = """
226
+ const STORAGE_KEY = 'welcome_modal_dismissed';
227
+ const videoId = props.value?.videoId || 'YbCf6x0B3fU';
228
+
229
+ // ํŽ˜์ด์ง€ ๋กœ๋“œ ์‹œ localStorage ์ฒดํฌ
230
+ const isDismissed = localStorage.getItem(STORAGE_KEY) === 'true';
231
+
232
+ // "๋‹ค์‹œ ๋ณด์ง€ ์•Š๊ธฐ"๋ฅผ ์„ ํƒํ•˜์ง€ ์•Š์€ ๊ฒฝ์šฐ์—๋งŒ ๋ชจ๋‹ฌ ํ‘œ์‹œ
233
+ if (!isDismissed) {
234
+ // ํฐํŠธ๊ฐ€ ์™„์ „ํžˆ ๋กœ๋“œ๋œ ํ›„ ๋ชจ๋‹ฌ ํ‘œ์‹œ
235
+ document.fonts.ready.then(() => {
236
+ // ํฐํŠธ ๋กœ๋“œ ํ›„ ์•ฝ๊ฐ„์˜ ๋”œ๋ ˆ์ด ์ถ”๊ฐ€ (๋ Œ๋”๋ง ์•ˆ์ •ํ™”)
237
+ setTimeout(() => {
238
+ props.value = { visible: true, videoId: videoId };
239
+ }, 100);
240
+ });
241
+ }
242
+
243
+ // ํด๋ฆญ ์ด๋ฒคํŠธ ํ•ธ๋“ค๋Ÿฌ
244
+ element.addEventListener('click', (e) => {
245
+ // ๋ฐฐ๊ฒฝ ํด๋ฆญ์œผ๋กœ ๋‹ซ๊ธฐ
246
+ if (e.target.dataset.welcomeOverlay === 'true') {
247
+ props.value = { visible: false, videoId: props.value?.videoId };
248
+ }
249
+
250
+ // X ๋ฒ„ํŠผ ๋˜๋Š” "Start Playing" ๋ฒ„ํŠผ ํด๋ฆญ
251
+ if (e.target.closest('[data-welcome-close="true"]')) {
252
+ props.value = { visible: false, videoId: props.value?.videoId };
253
+ }
254
+
255
+ // "Don't show again" ๋ฒ„ํŠผ ํด๋ฆญ
256
+ if (e.target.closest('[data-welcome-dismiss="true"]')) {
257
+ localStorage.setItem(STORAGE_KEY, 'true');
258
+ props.value = { visible: false, videoId: props.value?.videoId };
259
+ }
260
+ });
261
+
262
+ // ESC ํ‚ค๋กœ ๋‹ซ๊ธฐ
263
+ const handleKeydown = (e) => {
264
+ if (e.key === 'Escape' && props.value && props.value.visible) {
265
+ props.value = { visible: false, videoId: props.value?.videoId };
266
+ }
267
+ };
268
+ document.addEventListener('keydown', handleKeydown);
269
+ """
270
+
271
+ def __init__(self, video_id: str = None):
272
+ """
273
+ ์›ฐ์ปด ๋ชจ๋‹ฌ ์ดˆ๊ธฐํ™”
274
+
275
+ Args:
276
+ video_id: YouTube ์˜์ƒ ID (๊ธฐ๋ณธ๊ฐ’: YbCf6x0B3fU)
277
+ """
278
+ self.video_id = video_id or self.YOUTUBE_VIDEO_ID
279
+ self.component = None
280
+
281
+ def render(self) -> gr.HTML:
282
+ """
283
+ ์›ฐ์ปด ๋ชจ๋‹ฌ ์ปดํฌ๋„ŒํŠธ ๋ Œ๋”๋ง
284
+
285
+ Returns:
286
+ gr.HTML: ๋ Œ๋”๋ง๋œ ๋ชจ๋‹ฌ ์ปดํฌ๋„ŒํŠธ
287
+ """
288
+ # ์ดˆ๊ธฐ๊ฐ’์€ visible: False๋กœ ์„ค์ • (JS์—์„œ ๋กœ๋“œ ํ›„ ํ‘œ์‹œ)
289
+ self.component = gr.HTML(
290
+ value={"visible": False, "videoId": self.video_id},
291
+ html_template=self.HTML_TEMPLATE,
292
+ css_template=self.CSS_TEMPLATE,
293
+ js_on_load=self.JS_ON_LOAD,
294
+ elem_id="welcome-modal",
295
+ )
296
+ return self.component
297
+
298
+ @staticmethod
299
+ def show(video_id: str = "YbCf6x0B3fU") -> dict:
300
+ """๋ชจ๋‹ฌ ์—ด๊ธฐ"""
301
+ return {"visible": True, "videoId": video_id}
302
+
303
+ @staticmethod
304
+ def hide() -> dict:
305
+ """๋ชจ๋‹ฌ ๋‹ซ๊ธฐ"""
306
+ return {"visible": False, "videoId": ""}
client/frontend/styles/chatbot_style.py CHANGED
@@ -88,8 +88,8 @@ FLOATING_CHATBOT_CSS = """
88
  position: fixed;
89
  bottom: 104px;
90
  right: 20px;
91
- width: 420px;
92
- height: 640px;
93
  max-width: calc(100vw - 20px);
94
  max-height: calc(100vh - 128px);
95
  gap: 0;
@@ -267,7 +267,7 @@ FLOATING_CHATBOT_CSS = """
267
  }
268
 
269
  /* ๋ชจ๋ฐ”์ผ ๋ฐ˜์‘ํ˜• */
270
- @media (max-width: 480px) {
271
  #chat-container {
272
  width: calc(100vw - 32px);
273
  right: 16px;
 
88
  position: fixed;
89
  bottom: 104px;
90
  right: 20px;
91
+ width: 636px;
92
+ height: 840px;
93
  max-width: calc(100vw - 20px);
94
  max-height: calc(100vh - 128px);
95
  gap: 0;
 
267
  }
268
 
269
  /* ๋ชจ๋ฐ”์ผ ๋ฐ˜์‘ํ˜• */
270
+ @media (max-width: 768px) {
271
  #chat-container {
272
  width: calc(100vw - 32px);
273
  right: 16px;
client/frontend/styles/custom_css.py CHANGED
@@ -38,6 +38,6 @@ def get_all_css():
38
  def get_head_scripts():
39
  """head์— ์‚ฝ์ž…ํ•  JavaScript ๋ฐ˜ํ™˜"""
40
  import os
41
- backend_port = os.getenv("BACKEND_PORT", "8445")
42
  backend_port_script = f"<script>window.BACKEND_PORT = {backend_port};</script>"
43
  return backend_port_script + CELEBRATION_JS + FALLING_ELEMENTS_JS + GAMEOVER_JS
 
38
  def get_head_scripts():
39
  """head์— ์‚ฝ์ž…ํ•  JavaScript ๋ฐ˜ํ™˜"""
40
  import os
41
+ backend_port = os.getenv("BACKEND_PORT")
42
  backend_port_script = f"<script>window.BACKEND_PORT = {backend_port};</script>"
43
  return backend_port_script + CELEBRATION_JS + FALLING_ELEMENTS_JS + GAMEOVER_JS
client/services/analysis_service.py CHANGED
@@ -110,7 +110,8 @@ async def analyze_voice(audio_bytes: bytes, date: str, session_id: str) -> Dict:
110
  add_hint_to_history(session_id, advice)
111
 
112
  # 7. Save guess record to database
113
- save_guess_record(
 
114
  session_id=session_id,
115
  puzzle_number=puzzle["puzzle_number"],
116
  pitch=pitch,
@@ -123,6 +124,10 @@ async def analyze_voice(audio_bytes: bytes, date: str, session_id: str) -> Dict:
123
  is_correct=is_correct,
124
  user_text=user_text,
125
  )
 
 
 
 
126
 
127
  total_time = (time.time() - start_time) * 1000
128
  logger.info(f"โฑ๏ธ TOTAL REQUEST TIME: {total_time:.1f}ms")
 
110
  add_hint_to_history(session_id, advice)
111
 
112
  # 7. Save guess record to database
113
+ logger.info(f"[DB] Attempting to save guess record for session={session_id}, puzzle={puzzle['puzzle_number']}")
114
+ save_result = save_guess_record(
115
  session_id=session_id,
116
  puzzle_number=puzzle["puzzle_number"],
117
  pitch=pitch,
 
124
  is_correct=is_correct,
125
  user_text=user_text,
126
  )
127
+ if save_result:
128
+ logger.info(f"[DB] โœ… Successfully saved guess record")
129
+ else:
130
+ logger.error(f"[DB] โŒ Failed to save guess record!")
131
 
132
  total_time = (time.time() - start_time) * 1000
133
  logger.info(f"โฑ๏ธ TOTAL REQUEST TIME: {total_time:.1f}ms")
client/services/database.py CHANGED
@@ -125,10 +125,10 @@ def save_guess_record(
125
  """
126
  INSERT INTO guess_records
127
  (session_id, puzzle_number, pitch, rhythm, energy, pronunciation,
128
- transcript, overall, advice, is_correct, guess_timestamp, user_text)
129
  VALUES
130
  (:session_id, :puzzle_number, :pitch, :rhythm, :energy, :pronunciation,
131
- :transcript, :overall, :advice, :is_correct, :guess_timestamp, :user_text)
132
  """
133
  )
134
 
@@ -147,7 +147,6 @@ def save_guess_record(
147
  "advice": advice,
148
  "is_correct": is_correct,
149
  "guess_timestamp": guess_timestamp,
150
- "user_text": user_text,
151
  },
152
  )
153
  connection.commit()
@@ -159,6 +158,9 @@ def save_guess_record(
159
 
160
  except Exception as e:
161
  logger.error(f"Failed to save guess record: {e}")
 
 
 
162
  return False
163
 
164
 
 
125
  """
126
  INSERT INTO guess_records
127
  (session_id, puzzle_number, pitch, rhythm, energy, pronunciation,
128
+ transcript, overall, advice, is_correct, guess_timestamp)
129
  VALUES
130
  (:session_id, :puzzle_number, :pitch, :rhythm, :energy, :pronunciation,
131
+ :transcript, :overall, :advice, :is_correct, :guess_timestamp)
132
  """
133
  )
134
 
 
147
  "advice": advice,
148
  "is_correct": is_correct,
149
  "guess_timestamp": guess_timestamp,
 
150
  },
151
  )
152
  connection.commit()
 
158
 
159
  except Exception as e:
160
  logger.error(f"Failed to save guess record: {e}")
161
+ import traceback
162
+ traceback.print_exc()
163
+ print(f"[DB ERROR] save_guess_record failed: {e}")
164
  return False
165
 
166