SJLee-0525
[CHORE] test9
9f30ef0
"""
์‹คํŒจ ๋ชจ๋‹ฌ ์ปดํฌ๋„ŒํŠธ (Gradio 6 ํ˜ธํ™˜ ๋ฒ„์ „)
์ปค์Šคํ…€ Modal ํด๋ž˜์Šค ์‚ฌ์šฉ
๐Ÿ‘จโ€๐Ÿ’ป ๋‹ด๋‹น: ๊ฐœ๋ฐœ์ž B
"""
import gradio as gr
from frontend.components.custom_modal import Modal
from frontend.renderers import render_radar_chart
class FailureModalComponent:
"""์‹คํŒจ ๋ชจ๋‹ฌ ์ปดํฌ๋„ŒํŠธ - custom_modal ์‚ฌ์šฉ"""
def __init__(self):
self.modal = None
self.close_btn = None
def render(self):
"""
์‹คํŒจ ๋ชจ๋‹ฌ UI ๋ Œ๋”๋ง
Returns:
gr.HTML: modal_component
"""
self.modal = Modal(
visible=False,
content="",
elem_id="failure-modal"
)
modal_component = self.modal.render()
return modal_component
def setup_events(self, on_close=None):
"""
์ด๋ฒคํŠธ ๋ฐ”์ธ๋”ฉ (ํ˜„์žฌ๋Š” JS์—์„œ ์ฒ˜๋ฆฌ)
Args:
on_close: ๋ชจ๋‹ฌ ๋‹ซํž ๋•Œ ํ˜ธ์ถœํ•  ์ฝœ๋ฐฑ (๋ฏธ์‚ฌ์šฉ)
"""
# ๋ชจ๋‹ฌ ๋‹ซ๊ธฐ๋Š” JS์—์„œ ์ž๋™ ์ฒ˜๋ฆฌ๋จ
pass
@staticmethod
def create_modal_content(
recognized_text: str,
score: int,
hint: str,
audio_path: str = None,
metrics: dict = None,
user_text: str = None
) -> str:
"""
๋ชจ๋‹ฌ ๋‚ด์šฉ HTML ์ƒ์„ฑ (์˜ค๋””์˜ค ์žฌ์ƒ + ์˜ค๊ฐ ๊ทธ๋ž˜ํ”„ + ์ ์ˆ˜ํ‘œ + ์กฐ์–ธ)
Args:
recognized_text: ์ธ์‹๋œ ํ…์ŠคํŠธ
score: ์ ์ˆ˜
hint: ํžŒํŠธ ๋ฉ”์‹œ์ง€
audio_path: ์ œ์ถœํ•œ ์˜ค๋””์˜ค ํŒŒ์ผ ๊ฒฝ๋กœ
metrics: ๋ฉ”ํŠธ๋ฆญ ๋ฐ์ดํ„ฐ {pronunciation, tone, pitch, rhythm, energy}
user_text: ์‚ฌ์šฉ์ž๊ฐ€ ๋งํ•œ ํ…์ŠคํŠธ (STT ๊ฒฐ๊ณผ)
Returns:
str: HTML ๋ฌธ์ž์—ด
"""
# ๊ธฐ๋ณธ ๋ฉ”ํŠธ๋ฆญ
if metrics is None:
metrics = {}
# ์ ์ˆ˜์— ๋”ฐ๋ฅธ ์ƒ‰์ƒ ๊ฒฐ์ • ํ•จ์ˆ˜
def get_color(val):
return "#e8a054" if val < 85 else "#4db8ff"
# ์‚ฌ์šฉ์ž๊ฐ€ ๋งํ•œ ํ…์ŠคํŠธ (STT ๊ฒฐ๊ณผ) HTML
user_text_html = ""
if user_text:
user_text_html = f"""
<div class="fm-user-text-section">
<div class="fm-section-title">You said</div>
<div class="fm-user-text">"{user_text}"</div>
</div>
"""
# ์˜ค๋””์˜ค ํ”Œ๋ ˆ์ด์–ด HTML
audio_html = ""
if audio_path:
audio_html = f"""
<div class="fm-audio-section">
{user_text_html}
<audio controls style="width: 100%; height: 36px; margin-top: 16px;" preload="metadata">
<source src="/gradio_api/file={audio_path}" type="audio/wav">
<source src="/gradio_api/file={audio_path}" type="audio/mpeg">
์˜ค๋””์˜ค๋ฅผ ์žฌ์ƒํ•  ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค.
</audio>
</div>
"""
# ๋ ˆ์ด๋” ์ฐจํŠธ SVG
radar_svg = render_radar_chart(metrics, size=280) if metrics else ""
# ์ ์ˆ˜ํ‘œ HTML
score_table_html = ""
if metrics:
score_table_html = f"""
<div class="fm-score-grid">
<div style="text-align: center;">
<div class="fm-score-label">Pronunciation</div>
<div style="font-size: 0.95rem; font-weight: 700; color: {get_color(metrics.get('pronunciation', 0))};">{metrics.get('pronunciation', '-')}</div>
</div>
<div style="text-align: center;">
<div class="fm-score-label">Pitch</div>
<div style="font-size: 0.95rem; font-weight: 700; color: {get_color(metrics.get('pitch', 0))};">{metrics.get('pitch', '-')}</div>
</div>
<div style="text-align: center;">
<div class="fm-score-label">Line Accuracy</div>
<div style="font-size: 0.95rem; font-weight: 700; color: {get_color(metrics.get('tone', 0))};">{metrics.get('tone', '-')}</div>
</div>
<div style="text-align: center;">
<div class="fm-score-label">Rhythm</div>
<div style="font-size: 0.95rem; font-weight: 700; color: {get_color(metrics.get('rhythm', 0))};">{metrics.get('rhythm', '-')}</div>
</div>
<div style="text-align: center;">
<div class="fm-score-label">Energy</div>
<div style="font-size: 0.95rem; font-weight: 700; color: {get_color(metrics.get('energy', 0))};">{metrics.get('energy', '-')}</div>
</div>
</div>
"""
# ๊ทธ๋ž˜ํ”„ + ์ ์ˆ˜ํ‘œ ์˜์—ญ
chart_section = ""
if metrics:
chart_section = f"""
<div class="fm-chart-section">
{radar_svg}
{score_table_html}
</div>
"""
# API์—์„œ ๋ฐ›์€ advice ๋˜๋Š” ๊ธฐ๋ณธ ๋ฉ”์‹œ์ง€
advice_text = hint if hint else "๋ฐœ์Œ์˜ ์ •ํ™•๋„๋ฅผ ๋†’์ด๊ธฐ ์œ„ํ•ด ๊ฐ ์Œ์ ˆ์„ ๋˜๋ฐ•๋˜๋ฐ• ๋ฐœ์Œํ•ด ๋ณด์„ธ์š”. ํŠนํžˆ ๋ฐ›์นจ ๋ฐœ์Œ์— ์ฃผ์˜ํ•˜์‹œ๊ณ , ๋ง์˜ ์†๋„๋ฅผ ์กฐ๊ธˆ ์ฒœ์ฒœํžˆ ํ•˜๋ฉด ์ธ์‹๋ฅ ์ด ํ–ฅ์ƒ๋ฉ๋‹ˆ๋‹ค."
advice_html = f"""
<div class="fm-advice-section">
<div class="fm-advice-title">Advice</div>
<div class="fm-advice-text">
{advice_text}
</div>
</div>
"""
backup_recognized_text = """
<div style="background: #f8f9fa; border-radius: 8px; padding: 8px;">
<div style="color: #666; font-size: 0.85em; margin-bottom: 4px;">์ธ์‹๋œ ํ…์ŠคํŠธ</div>
<div style="font-size: 1.05em; color: #333;">{recognized_text}</div>
</div>
"""
return f"""
<style>
/* ์˜ค๋””์˜ค ์„น์…˜ */
.fm-audio-section {{
padding: 12px;
background: #f8f9fa;
border-radius: 8px;
}}
.fm-section-title {{
color: #666;
font-size: 1em;
margin-bottom: 12px;
font-weight: 600;
}}
/* ์ฐจํŠธ ์„น์…˜ */
.fm-chart-section {{
background: #f0f7fc;
border-radius: 12px;
padding: 16px;
display: flex;
flex-direction: column;
align-items: center;
}}
.fm-score-grid {{
display: grid;
grid-template-columns: repeat(5, 1fr);
gap: 4px;
width: 100%;
margin-top: 8px;
}}
.fm-score-label {{
font-size: 0.65rem;
color: #666;
}}
.fm-user-text {{
color: #5c3d1e;
font-size: 1.25em;
font-style: italic;
line-height: 1.5;
font-weight: 500;
text-align: center;
}}
/* ์กฐ์–ธ ์„น์…˜ */
.fm-advice-section {{
padding: 14px;
background: linear-gradient(135deg, #e0f2fe 0%, #bae6fd 100%);
border-radius: 8px;
}}
.fm-advice-title {{
color: #666;
font-size: 1em;
margin-bottom: 12px;
font-weight: 600;
}}
.fm-advice-text {{
color: #075985;
font-size: 0.9em;
line-height: 1.5;
}}
/* ๋‹ซ๊ธฐ ๋ฒ„ํŠผ */
.failure-modal-close-btn {{
font-family: 'Lilita One' !important;
text-align: center !important;
background: linear-gradient(135deg, #e8a054 0%, #f29430 100%) !important;
color: white !important;
border: none !important;
padding: 8px 0px !important;
font-size: 22px !important;
font-weight: 700 !important;
text-transform: uppercase !important;
letter-spacing: 1px !important;
border-radius: 25px !important;
cursor: pointer !important;
transition: all 0.3s ease !important;
width: 100% !important;
min-width: 320px !important;
max-width: 800px !important;
margin: 0 auto !important;
display: block !important;
}}
.failure-modal-close-btn:hover {{
background: linear-gradient(135deg, #f29430 0%, #e8a054 100%) !important;
transform: translateY(-2px) !important;
}}
/* ========== ๋‹คํฌ๋ชจ๋“œ ========== */
.modal-dark .fm-audio-section {{
background: #2d2d2d !important;
}}
.modal-dark .fm-section-title {{
color: #a0a0a0 !important;
}}
.modal-dark .fm-user-text {{
color: #ffba70 !important;
}}
.modal-dark .fm-chart-section {{
background: #2d2d2d !important;
}}
.modal-dark .fm-score-label {{
color: #a0a0a0 !important;
}}
.modal-dark .fm-advice-section {{
background: linear-gradient(135deg, #1e3a4c 0%, #1a3040 100%) !important;
}}
.modal-dark .fm-advice-title {{
color: #7dd3fc !important;
}}
.modal-dark .fm-advice-text {{
color: #bae6fd !important;
}}
</style>
<div style="text-align: start;">
<div style="display: flex; align-items: center; justify-content: center; height: fit-content; padding-left: 4px; margin-bottom: 20px;">
<h2 style="
font-family: 'Lilita One', 'Bangers', Impact, sans-serif;
text-align: center;
color: #dc3545;
margin: 0;
font-size: 64px;
color: #e8a054;
letter-spacing: 1.5px;
-webkit-text-stroke: 1.5px #8b5a2b;
text-shadow:
2px 2px 0 #8b5a2b,
2px 2px 0 #5c3d1e,
2.5px 2.5px 0 #5c3d1e,
0 0 20px rgba(232, 160, 84, 0.5);
">
Try Again!
</h2>
</div>
<div style="display: flex; flex-direction: column; gap: 12px; margin-bottom: 20px;">
{audio_html}
{advice_html}
{chart_section}
</div>
<div style="display: flex; justify-content: center;">
<button class="failure-modal-close-btn" data-modal-close="true">
Close
</button>
</div>
</div>
"""
@staticmethod
def show(content: str):
"""๋ชจ๋‹ฌ ์—ด๊ธฐ"""
return Modal.show(content)
@staticmethod
def hide():
"""๋ชจ๋‹ฌ ๋‹ซ๊ธฐ"""
return Modal.hide()