|
|
""" |
|
|
์คํจ ๋ชจ๋ฌ ์ปดํฌ๋ํธ (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: ๋ชจ๋ฌ ๋ซํ ๋ ํธ์ถํ ์ฝ๋ฐฑ (๋ฏธ์ฌ์ฉ) |
|
|
""" |
|
|
|
|
|
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" |
|
|
|
|
|
|
|
|
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> |
|
|
""" |
|
|
|
|
|
|
|
|
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> |
|
|
""" |
|
|
|
|
|
|
|
|
|
|
|
radar_svg = render_radar_chart(metrics, size=280) if metrics else "" |
|
|
|
|
|
|
|
|
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> |
|
|
""" |
|
|
|
|
|
|
|
|
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() |
|
|
|