|
|
""" |
|
|
์คํจ ๊ธฐ๋ก ํ์ ์ปดํฌ๋ํธ |
|
|
์๋ ๊ธฐ๋ก์ HTML๋ก ๋ ๋๋ง |
|
|
|
|
|
๐จโ๐ป ๋ด๋น: ๊ฐ๋ฐ์ B |
|
|
""" |
|
|
|
|
|
import random |
|
|
import gradio as gr |
|
|
from frontend.renderers.radar_chart import render_radar_chart |
|
|
|
|
|
|
|
|
class HistoryDisplayComponent: |
|
|
"""์คํจ ๊ธฐ๋ก ํ์ ์ปดํฌ๋ํธ""" |
|
|
|
|
|
def __init__(self): |
|
|
self.history_html = None |
|
|
self.give_up_trigger_btn = None |
|
|
|
|
|
def render(self): |
|
|
""" |
|
|
์คํจ ๊ธฐ๋ก UI ๋ ๋๋ง |
|
|
๊ธฐ๋ก์ด ์์ผ๋ฉด ์๋ฌด๊ฒ๋ ํ์ํ์ง ์์ |
|
|
|
|
|
Returns: |
|
|
gr.HTML: history_html ์ปดํฌ๋ํธ |
|
|
""" |
|
|
self.history_html = gr.HTML(value="") |
|
|
|
|
|
|
|
|
self.give_up_trigger_btn = gr.Button( |
|
|
value="Give Up Trigger", |
|
|
visible=True, |
|
|
elem_id="hidden-give-up-btn", |
|
|
elem_classes=["hidden-trigger"] |
|
|
) |
|
|
return self.history_html, self.give_up_trigger_btn |
|
|
|
|
|
@staticmethod |
|
|
def render_empty_history() -> str: |
|
|
""" |
|
|
๋น ๊ธฐ๋ก ํ๋ฉด ๋ ๋๋ง - ๋น ์ํ UI ํ์ |
|
|
|
|
|
Returns: |
|
|
str: HTML ๋ฌธ์์ด |
|
|
""" |
|
|
widget_id = f"hist{random.randint(1000, 9999)}" |
|
|
|
|
|
return f""" |
|
|
<style> |
|
|
.give-up-btn {{ |
|
|
display: flex !important; |
|
|
align-items: center !important; |
|
|
justify-content: center !important; |
|
|
width: 76px !important; |
|
|
height: 26px !important; |
|
|
margin: 0 !important; |
|
|
padding: 0 !important; |
|
|
background: linear-gradient(135deg, #ef4444 0%, #dc2626 100%) !important; |
|
|
color: white !important; |
|
|
border: none !important; |
|
|
border-radius: 12px !important; |
|
|
font-size: 0.8rem !important; |
|
|
font-weight: 600 !important; |
|
|
cursor: pointer !important; |
|
|
transition: all 0.2s ease !important; |
|
|
}} |
|
|
.give-up-btn:hover {{ |
|
|
scale: 1.05 !important; |
|
|
transition: all 0.2s ease !important; |
|
|
}} |
|
|
.hidden-trigger {{ |
|
|
display: none !important; |
|
|
}} |
|
|
/* ๋คํฌ๋ชจ๋ ์ง์ */ |
|
|
.dark #{widget_id} {{ |
|
|
background: var(--dark-surface, #1a1a1b) !important; |
|
|
border-color: var(--dark-border, #3a3a3c) !important; |
|
|
}} |
|
|
.dark #{widget_id} .history-header {{ |
|
|
background: var(--dark-surface-light, #2d2d2d) !important; |
|
|
border-color: var(--dark-border, #3a3a3c) !important; |
|
|
}} |
|
|
.dark #{widget_id} .history-header .history-title {{ |
|
|
color: var(--dark-text, #ffffff) !important; |
|
|
}} |
|
|
.dark #{widget_id} .empty-message {{ |
|
|
color: var(--dark-text-secondary, #818384) !important; |
|
|
}} |
|
|
</style> |
|
|
|
|
|
<div id="{widget_id}" class="history-widget" style=' |
|
|
margin-top: 24px; |
|
|
background: #ffffff; |
|
|
border-radius: 16px; |
|
|
border: 1px solid #c5dae8; |
|
|
overflow: hidden; |
|
|
box-shadow: 0 4px 12px rgba(77, 184, 255, 0.1); |
|
|
'> |
|
|
<!-- ํค๋ --> |
|
|
<div class="history-header" style=' |
|
|
padding: 12px 16px; |
|
|
background: #f0f7fc; |
|
|
border-bottom: 1px solid #c5dae8; |
|
|
display: flex; |
|
|
justify-content: space-between; |
|
|
align-items: center; |
|
|
'> |
|
|
<span class="history-title" style='font-weight: 600; color: #2c3e50;'>History</span> |
|
|
|
|
|
<div style='display: flex; align-items: center; gap: 8px;'> |
|
|
<button id="give-up-btn" class="give-up-btn" onclick="document.getElementById('hidden-give-up-btn').click()">GIVE UP</button> |
|
|
<span style=' |
|
|
display: flex; |
|
|
align-items: center; |
|
|
justify-content: center; |
|
|
width: 32px; |
|
|
height: 26px; |
|
|
background: linear-gradient(135deg, #4db8ff 0%, #5bc0eb 100%); |
|
|
color: white; |
|
|
border-radius: 12px; |
|
|
font-size: 0.8rem; |
|
|
font-weight: 600; |
|
|
'>0</span> |
|
|
</div> |
|
|
</div> |
|
|
|
|
|
<!-- ๋น ์ํ ๋ฉ์์ง --> |
|
|
<div class="empty-message" style=' |
|
|
padding: 48px 24px; |
|
|
text-align: center; |
|
|
color: #9ca3af; |
|
|
font-size: 0.95rem; |
|
|
'> |
|
|
No attempts yet. Record your voice to start! |
|
|
</div> |
|
|
</div> |
|
|
""" |
|
|
|
|
|
@staticmethod |
|
|
def render_failure_history(history: list) -> str: |
|
|
""" |
|
|
์คํจ ๊ธฐ๋ก ๋ฆฌ์คํธ ๋ ๋๋ง - ์ข์ฐ ๋ถํ ๋ ์ด์์ (์ข: ๊ธฐ๋ก ๋ชฉ๋ก, ์ฐ: ์ค๊ฐ ๊ทธ๋ํ) |
|
|
CSS ๊ธฐ๋ฐ ํ ๊ธ (radio button hidden) |
|
|
|
|
|
Args: |
|
|
history (list): ์คํจ ๊ธฐ๋ก ๋ฆฌ์คํธ |
|
|
[{"timestamp": str, "text": str, "audio": str, "metrics": dict}, ...] |
|
|
|
|
|
Returns: |
|
|
str: HTML ๋ฌธ์์ด |
|
|
""" |
|
|
if not history: |
|
|
return HistoryDisplayComponent.render_empty_history() |
|
|
|
|
|
|
|
|
widget_id = f"hist{random.randint(1000, 9999)}" |
|
|
|
|
|
|
|
|
reversed_history = list(reversed(history)) |
|
|
|
|
|
|
|
|
html = f""" |
|
|
<style> |
|
|
#{widget_id} input[type="radio"] {{ |
|
|
display: none; |
|
|
}} |
|
|
#{widget_id} .graph-panel {{ |
|
|
display: none; |
|
|
}} |
|
|
#{widget_id} .content-area {{ |
|
|
flex-direction: row; |
|
|
}} |
|
|
#{widget_id} .history-list {{ |
|
|
border-right: 1px solid #e5e7eb; |
|
|
}} |
|
|
#{widget_id} .graph-area {{ |
|
|
flex: 0 0 360px; |
|
|
}} |
|
|
/* ์ปจํ
์ด๋๊ฐ 720px ๋ฏธ๋ง์ผ ๋ ์ํ ๋ฐฐ์น (๊ทธ๋ํ ์) */ |
|
|
@container (max-width: 719px) {{ |
|
|
#{widget_id} .content-area {{ |
|
|
flex-direction: column-reverse; |
|
|
}} |
|
|
#{widget_id} .history-list {{ |
|
|
border-right: none; |
|
|
border-top: 1px solid #e5e7eb; |
|
|
max-height: none; |
|
|
height: auto; |
|
|
overflow-y: visible; |
|
|
}} |
|
|
#{widget_id} .graph-area {{ |
|
|
flex: none; |
|
|
width: 100%; |
|
|
}} |
|
|
}} |
|
|
|
|
|
.give-up-btn {{ |
|
|
display: flex !important; |
|
|
align-items: center !important; |
|
|
justify-content: center !important; |
|
|
width: 76px !important; |
|
|
height: 26px !important; |
|
|
margin: 0 !important; |
|
|
padding: 0 !important; |
|
|
background: linear-gradient(135deg, #ef4444 0%, #dc2626 100%) !important; |
|
|
color: white !important; |
|
|
border: none !important; |
|
|
border-radius: 12px !important; |
|
|
font-size: 0.8rem !important; |
|
|
font-weight: 600 !important; |
|
|
cursor: pointer !important; |
|
|
transition: all 0.2s ease !important; |
|
|
}} |
|
|
|
|
|
.give-up-btn:hover {{ |
|
|
scale: 1.05 !important; |
|
|
transition: all 0.2s ease !important; |
|
|
}} |
|
|
""" |
|
|
|
|
|
|
|
|
for idx in range(len(reversed_history)): |
|
|
html += f""" |
|
|
#{widget_id} #select-{widget_id}-{idx}:checked ~ .content-area .graph-panel-{idx} {{ |
|
|
display: flex; |
|
|
}} |
|
|
#{widget_id} #select-{widget_id}-{idx}:checked ~ .content-area .item-{idx} {{ |
|
|
background: #e3f5ff; |
|
|
}} |
|
|
#{widget_id} #select-{widget_id}-{idx}:checked ~ .content-area .item-{idx} .badge {{ |
|
|
background: #4db8ff; |
|
|
}} |
|
|
""" |
|
|
|
|
|
html += f""" |
|
|
/* ๋คํฌ๋ชจ๋ ์ง์ */ |
|
|
.dark #{widget_id} {{ |
|
|
background: var(--dark-surface, #1a1a1b) !important; |
|
|
border-color: var(--dark-border, #3a3a3c) !important; |
|
|
}} |
|
|
.dark #{widget_id} .history-header {{ |
|
|
background: var(--dark-surface-light, #2d2d2d) !important; |
|
|
border-color: var(--dark-border, #3a3a3c) !important; |
|
|
}} |
|
|
.dark #{widget_id} .history-header .history-title {{ |
|
|
color: var(--dark-text, #ffffff) !important; |
|
|
}} |
|
|
.dark #{widget_id} .history-list {{ |
|
|
border-color: var(--dark-border, #3a3a3c) !important; |
|
|
}} |
|
|
.dark #{widget_id} .history-item {{ |
|
|
border-color: var(--dark-border, #3a3a3c) !important; |
|
|
}} |
|
|
.dark #{widget_id} .history-item .item-text {{ |
|
|
color: var(--dark-text, #ffffff) !important; |
|
|
}} |
|
|
.dark #{widget_id} .history-item .item-time {{ |
|
|
color: var(--dark-text-secondary, #818384) !important; |
|
|
}} |
|
|
.dark #{widget_id} .history-item .item-user-text {{ |
|
|
color: var(--dark-text-secondary, #818384) !important; |
|
|
}} |
|
|
.dark #{widget_id} .graph-area {{ |
|
|
background: var(--dark-surface-light, #2d2d2d) !important; |
|
|
}} |
|
|
.dark #{widget_id} .metric-label {{ |
|
|
color: var(--dark-text-secondary, #818384) !important; |
|
|
}} |
|
|
.dark #{widget_id} .graph-advice {{ |
|
|
background: linear-gradient(135deg, #1e3a4c 0%, #1a3040 100%) !important; |
|
|
color: #bae6fd !important; |
|
|
}} |
|
|
.dark #{widget_id} #select-{widget_id}-0:checked ~ .content-area .item-0, |
|
|
.dark #{widget_id} #select-{widget_id}-1:checked ~ .content-area .item-1, |
|
|
.dark #{widget_id} #select-{widget_id}-2:checked ~ .content-area .item-2, |
|
|
.dark #{widget_id} #select-{widget_id}-3:checked ~ .content-area .item-3, |
|
|
.dark #{widget_id} #select-{widget_id}-4:checked ~ .content-area .item-4 {{ |
|
|
background: rgba(77, 184, 255, 0.2) !important; |
|
|
}} |
|
|
|
|
|
/* ์จ๊ฒจ์ง ํธ๋ฆฌ๊ฑฐ ๋ฒํผ */ |
|
|
.hidden-trigger {{ |
|
|
display: none !important; |
|
|
}} |
|
|
</style> |
|
|
|
|
|
<div id="{widget_id}" class="history-widget" style=' |
|
|
margin-top: 24px; |
|
|
background: #ffffff; |
|
|
border-radius: 16px; |
|
|
border: 1px solid #c5dae8; |
|
|
overflow: hidden; |
|
|
container-type: inline-size; |
|
|
box-shadow: 0 4px 12px rgba(77, 184, 255, 0.1); |
|
|
'> |
|
|
<!-- ํค๋ --> |
|
|
<div class="history-header" style=' |
|
|
padding: 12px 16px; |
|
|
background: #f0f7fc; |
|
|
border-bottom: 1px solid #c5dae8; |
|
|
display: flex; |
|
|
justify-content: space-between; |
|
|
align-items: center; |
|
|
'> |
|
|
<span class="history-title" style='font-weight: 600; color: #2c3e50;'>History</span> |
|
|
|
|
|
<div style='display: flex; align-items: center; gap: 8px;'> |
|
|
<button id="give-up-btn" class="give-up-btn" onclick="document.getElementById('hidden-give-up-btn').click()">GIVE UP</button> |
|
|
<span style=' |
|
|
display: flex; |
|
|
align-items: center; |
|
|
justify-content: center; |
|
|
width: 32px; |
|
|
height: 26px; |
|
|
background: linear-gradient(135deg, #4db8ff 0%, #5bc0eb 100%); |
|
|
color: white; |
|
|
border-radius: 12px; |
|
|
font-size: 0.8rem; |
|
|
font-weight: 600; |
|
|
'>{len(history)}</span> |
|
|
</div> |
|
|
</div> |
|
|
""" |
|
|
|
|
|
|
|
|
for idx in range(len(reversed_history)): |
|
|
checked = "checked" if idx == 0 else "" |
|
|
html += f'<input type="radio" name="select-{widget_id}" id="select-{widget_id}-{idx}" {checked}>' |
|
|
|
|
|
html += f""" |
|
|
<!-- ์ข์ฐ ๋ถํ ์ปจํ
์ธ (๋ฐ์ํ) --> |
|
|
<div class="content-area" style=' |
|
|
display: flex; |
|
|
'> |
|
|
<!-- ์ข์ธก: ๊ธฐ๋ก ๋ฆฌ์คํธ --> |
|
|
<div class="history-list" style=' |
|
|
flex: 1; |
|
|
min-width: 0; |
|
|
max-height: 560px; |
|
|
overflow-y: auto; |
|
|
'> |
|
|
""" |
|
|
|
|
|
|
|
|
for idx, entry in enumerate(reversed_history): |
|
|
|
|
|
metrics = entry.get('metrics', {}) |
|
|
overall_score = metrics.get('overall', 0) |
|
|
score = f"{overall_score:.1f}" if isinstance(overall_score, float) else str(overall_score) |
|
|
|
|
|
|
|
|
user_text = entry.get('user_text', '') |
|
|
display_text = f'{user_text}' if user_text else '-' |
|
|
|
|
|
html += f""" |
|
|
<label for="select-{widget_id}-{idx}" class="history-item item-{idx}" style=' |
|
|
display: flex; |
|
|
align-items: center; |
|
|
padding: 16px 14px; |
|
|
border-bottom: 1px solid #f0f0f0; |
|
|
cursor: pointer; |
|
|
gap: 12px; |
|
|
transition: background 0.15s; |
|
|
'> |
|
|
<div class="badge" style=' |
|
|
min-width: 48px; |
|
|
width: 48px; |
|
|
max-width: 48px; |
|
|
height: 28px; |
|
|
background: #8ba4bd; |
|
|
color: white; |
|
|
border-radius: 12px; |
|
|
display: flex; |
|
|
align-items: center; |
|
|
justify-content: center; |
|
|
font-size: 0.9rem; |
|
|
font-weight: 700; |
|
|
flex-shrink: 0; |
|
|
'>{score}</div> |
|
|
<div class="item-user-text" style=' |
|
|
flex: 1; |
|
|
min-width: 0; |
|
|
font-size: 0.9rem; |
|
|
font-weight: 700; |
|
|
color: #27272A; |
|
|
line-height: 1.4; |
|
|
overflow: hidden; |
|
|
display: -webkit-box; |
|
|
-webkit-box-orient: vertical; |
|
|
-webkit-line-clamp: 1; |
|
|
text-overflow: ellipsis; |
|
|
white-space: nowrap; |
|
|
|
|
|
'>{display_text}</div> |
|
|
</label> |
|
|
""" |
|
|
|
|
|
html += f""" |
|
|
</div> |
|
|
|
|
|
<!-- ์ฐ์ธก: ์ค๊ฐ ๊ทธ๋ํ ์์ญ (์ํ ๋ฐฐ์น ์ ์๋ก) --> |
|
|
<div class="graph-area" style=' |
|
|
min-height: 560px; |
|
|
background: #f0f7fc; |
|
|
position: relative; |
|
|
'> |
|
|
""" |
|
|
|
|
|
|
|
|
def get_score_color(val): |
|
|
return "#e8a054" if val < 85 else "#4db8ff" |
|
|
|
|
|
|
|
|
for idx, entry in enumerate(reversed_history): |
|
|
metrics = entry.get('metrics', {}) |
|
|
radar_svg = render_radar_chart(metrics, size=280) if metrics else "" |
|
|
|
|
|
html += f""" |
|
|
<div class="graph-panel graph-panel-{idx}" style=' |
|
|
position: absolute; |
|
|
top: 0; |
|
|
left: 0; |
|
|
right: 0; |
|
|
bottom: 0; |
|
|
flex-direction: column; |
|
|
align-items: center; |
|
|
justify-content: start; |
|
|
gap: 12px; |
|
|
padding: 12px; |
|
|
'> |
|
|
<div style='display: flex; flex-direction: column; width: 100%; max-width: 420px; gap: 12px;'> |
|
|
<!-- ๋ ์ด๋ ์ฐจํธ --> |
|
|
<div style='display: flex; justify-content: center; flex: 1; align-items: center;'> |
|
|
{radar_svg} |
|
|
</div> |
|
|
|
|
|
<!-- ๋ฉํธ๋ฆญ ์์น --> |
|
|
<div style=' |
|
|
display: grid; |
|
|
grid-template-columns: repeat(5, 1fr); |
|
|
gap: 4px; |
|
|
width: 100%; |
|
|
'> |
|
|
<div style='text-align: center;'> |
|
|
<div class="metric-label" style='font-size: 0.6rem; color: #666;'>Pronunciation</div> |
|
|
<div style='font-size: 1rem; font-weight: 700; color: {get_score_color(metrics.get("pronunciation", 0))};'>{metrics.get('pronunciation', '-')}</div> |
|
|
</div> |
|
|
<div style='text-align: center;'> |
|
|
<div class="metric-label" style='font-size: 0.6rem; color: #666;'>Pitch</div> |
|
|
<div style='font-size: 1rem; font-weight: 700; color: {get_score_color(metrics.get("pitch", 0))};'>{metrics.get('pitch', '-')}</div> |
|
|
</div> |
|
|
<div style='text-align: center;'> |
|
|
<div class="metric-label" style='font-size: 0.6rem; color: #666;'>Line Accuracy</div> |
|
|
<div style='font-size: 1rem; font-weight: 700; color: {get_score_color(metrics.get("tone", 0))};'>{metrics.get('tone', '-')}</div> |
|
|
</div> |
|
|
<div style='text-align: center;'> |
|
|
<div class="metric-label" style='font-size: 0.6rem; color: #666;'>Rhythm</div> |
|
|
<div style='font-size: 1rem; font-weight: 700; color: {get_score_color(metrics.get("rhythm", 0))};'>{metrics.get('rhythm', '-')}</div> |
|
|
</div> |
|
|
<div style='text-align: center;'> |
|
|
<div class="metric-label" style='font-size: 0.6rem; color: #666;'>Energy</div> |
|
|
<div style='font-size: 1rem; font-weight: 700; color: {get_score_color(metrics.get("energy", 0))};'>{metrics.get('energy', '-')}</div> |
|
|
</div> |
|
|
</div> |
|
|
</div> |
|
|
|
|
|
<!-- ์ค๋์ค ํ๋ ์ด์ด --> |
|
|
<audio controls style='width: 100%; height: 32px;' preload="metadata"> |
|
|
<source src='/gradio_api/file={entry.get("audio", "")}' type='audio/wav'> |
|
|
</audio> |
|
|
|
|
|
<!-- Advice --> |
|
|
<div class="graph-advice" style=' |
|
|
flex: 1; |
|
|
width: 100%; |
|
|
padding: 10px 12px; |
|
|
background: #e3f5ff; |
|
|
border-radius: 8px; |
|
|
font-size: 0.85rem; |
|
|
color: #075985; |
|
|
line-height: 1.5; |
|
|
'>{entry.get('advice', '')}</div> |
|
|
</div> |
|
|
""" |
|
|
|
|
|
html += """ |
|
|
</div> |
|
|
</div> |
|
|
</div> |
|
|
""" |
|
|
|
|
|
return html |
|
|
|