Roony05's picture
Upload 3 files
d07fabf verified
Raw History Blame Contribute Delete
14.6 kB
import asyncio
import sys
import time
from typing import Dict, Optional, Tuple, Any
from pathlib import Path
if sys.platform == 'win32':
asyncio.set_event_loop_policy(asyncio.WindowsSelectorEventLoopPolicy())
import torch
import gradio as gr
import numpy as np
# Import backend ML & DSP components
from backend import (
SR,
TARGET_LEN,
run_prediction,
_to_mono_resampled,
DEVICE,
)
def get_empty_prediction_html(message: str = "Chưa có âm thanh được xử lý. Hãy tải lên file hoặc nói vào microphone.") -> str:
return f"""
<div class="prediction-card empty">
<div class="empty-emoji">🎙️</div>
<p class="empty-title">{message}</p>
<p class="empty-subtitle">Đang sẵn sàng phân tích mẫu âm thanh 1 giây của bạn.</p>
</div>
"""
def get_countdown_html(message: str, seconds: float) -> str:
return f"""
<div class="prediction-card empty loading-state">
<div class="loader-spinner"></div>
<p class="empty-title">{message}</p>
<p class="empty-subtitle">Sẵn sàng sau: <span class="countdown-seconds">{seconds:.1f}</span> giây</p>
</div>
"""
def format_prediction_as_html(pred_label: str, pred_conf: float, model_name: str, top_k_results: Dict[str, float]) -> str:
emojis = {
"five": "5️⃣", "zero": "0️⃣", "yes": "✅", "seven": "7️⃣", "no": "❌", "nine": "9️⃣",
"down": "⬇️", "one": "1️⃣", "go": "🚀", "two": "2️⃣", "stop": "🛑", "six": "6️⃣",
"unknown": "❓", "silence": "🔇"
}
emoji = emojis.get(pred_label.lower(), "🎙️")
# Alternative predictions list HTML
alt_html = ""
for label, prob in top_k_results.items():
pct = prob * 100
# Determine bar colors class based on probability
if prob > 0.7:
bar_class = "bar-high"
elif prob > 0.3:
bar_class = "bar-med"
else:
bar_class = "bar-low"
alt_html += f"""
<div class="alt-row">
<span class="alt-label">
<span class="alt-label-emoji">{emojis.get(label.lower(), "🔸")}</span> {label}
</span>
<div class="alt-bar-outer">
<div class="alt-bar-inner {bar_class}" style="width: {pct:.1f}%;"></div>
</div>
<span class="alt-pct">{pct:.1f}%</span>
</div>
"""
conf_pct = pred_conf * 100
if pred_conf > 0.7:
conf_class = "conf-high"
elif pred_conf > 0.3:
conf_class = "conf-medium"
else:
conf_class = "conf-low"
html = f"""
<div class="prediction-card {conf_class}">
<div class="card-header">
<div>
<span class="card-badge">Nhận dạng tốt nhất</span>
<h2 class="card-title">
<span class="card-title-emoji">{emoji}</span>
<span class="card-title-text">{pred_label}</span>
</h2>
</div>
<div class="card-conf-container">
<div class="card-conf-value">{conf_pct:.1f}%</div>
<div class="card-conf-label">Độ tin cậy</div>
</div>
</div>
<div class="card-alts">
<div class="card-alts-header">Xác suất chi tiết (Top-k)</div>
{alt_html}
</div>
<div class="card-footer">
<span>Mô hình: <strong>{model_name}</strong></span>
<span>Tần số lấy mẫu: <strong>{SR} Hz</strong></span>
</div>
</div>
"""
return html
IS_MODEL_WARMED_UP = False
def predict_from_audio(audio_input: Optional[Tuple[int, np.ndarray]], model_name: str, top_k: int = 5):
global IS_MODEL_WARMED_UP
if audio_input is None:
yield get_empty_prediction_html("Chưa nhận được tín hiệu âm thanh.")
return
sample_rate, audio = audio_input
if audio is None or len(audio) == 0:
yield get_empty_prediction_html("File âm thanh trống hoặc không hợp lệ.")
return
if not IS_MODEL_WARMED_UP:
# Show a 1.5 seconds countdown sequence only on the very first click
for remaining in [1.5, 1.0, 0.5]:
yield get_countdown_html("Đang kết nối thiết bị và khởi động mô hình AI...", remaining)
time.sleep(0.5)
# Warmup models globally
try:
from backend import MODELS
dummy_input = torch.zeros((1, 1, 40, 100), device=DEVICE)
for name, m in MODELS.items():
m(dummy_input)
except Exception as e:
print(f"Warmup error: {e}")
IS_MODEL_WARMED_UP = True
pred_label, pred_conf, top_k_results = run_prediction(audio_input, model_name, top_k)
yield format_prediction_as_html(pred_label, pred_conf, model_name, top_k_results)
# ============================================================
# Streaming / near-real-time mode
# ============================================================
def init_state() -> Dict[str, Any]:
return {
"buffer": np.zeros((0,), dtype=np.float32),
"sample_rate": np.array([SR], dtype=np.int32),
"warmup_steps": 0
}
def stream_predict(
audio_chunk: Optional[Tuple[int, np.ndarray]],
state: Optional[Dict[str, Any]],
model_name: str,
top_k: int,
) -> Tuple[Dict[str, Any], str]:
if state is None:
state = init_state()
if audio_chunk is None:
state["warmup_steps"] = 0
return state, get_empty_prediction_html("Đang chờ stream microphone bắt đầu...")
sr, chunk = audio_chunk
chunk = _to_mono_resampled(chunk, sr)
prev = state.get("buffer", np.zeros((0,), dtype=np.float32))
merged = np.concatenate([prev, chunk], axis=0)
if merged.shape[0] > TARGET_LEN:
merged = merged[-TARGET_LEN:]
state["buffer"] = merged
state["sample_rate"] = np.array([SR], dtype=np.int32)
# Implement a 3-step warm-up countdown (1.5s total) to let the microphone stream synchronize
warmup_steps = state.get("warmup_steps", 0)
if warmup_steps < 3:
state["warmup_steps"] = warmup_steps + 1
remaining = (3 - warmup_steps) * 0.5
return state, get_countdown_html("Đang đồng bộ mic và khởi động mô hình AI...", remaining)
if merged.shape[0] < int(0.3 * TARGET_LEN):
return state, get_empty_prediction_html("Đang lắng nghe... cần thêm dữ liệu âm thanh")
pred_label, pred_conf, top_k_results = run_prediction((SR, merged), model_name, top_k)
html_output = format_prediction_as_html(pred_label, pred_conf, model_name, top_k_results)
return state, html_output
# ============================================================
# Load custom CSS from file
# ============================================================
with open(Path(__file__).resolve().parent / "style.css", "r", encoding="utf-8") as f:
CUSTOM_CSS = f.read()
# Custom theme
THEME = gr.themes.Default(
primary_hue="violet",
secondary_hue="indigo",
neutral_hue="slate",
).set(
body_background_fill="radial-gradient(circle at top, #1e1b4b 0%, #0f172a 50%, #020617 100%)",
block_background_fill="rgba(15, 23, 42, 0.45)",
block_border_width="1px",
block_border_color="rgba(255, 255, 255, 0.08)",
block_radius="16px",
button_primary_background_fill="linear-gradient(135deg, #7c3aed, #4f46e5)",
button_primary_text_color="#ffffff",
)
# Custom HTML Header Banner
HEADER_HTML = """
<div id="bg-spotlight"></div>
<div class="shooting-stars-container">
<div class="shooting-star shooting-star-1"></div>
<div class="shooting-star shooting-star-2"></div>
<div class="shooting-star shooting-star-3"></div>
<div class="shooting-star shooting-star-4"></div>
</div>
<img src="x" onerror="if(!window.spotlightInitialized){window.spotlightInitialized=true;document.addEventListener('mousemove',(e)=>{document.documentElement.style.setProperty('--mouse-x',e.clientX+'px');document.documentElement.style.setProperty('--mouse-y',e.clientY+'px');document.querySelectorAll('.glass-panel,.prediction-card').forEach(card=>{const r=card.getBoundingClientRect();card.style.setProperty('--mouse-x',(e.clientX-r.left)+'px');card.style.setProperty('--mouse-y',(e.clientY-r.top)+'px');});});const trans={'Drop Audio Here':'Kéo thả âm thanh vào đây','Drop File Here':'Kéo thả file vào đây','- or -':'- hoặc -','Click to Upload':'Nhấp để tải lên'};const walk=(node)=>{if(node.nodeType===3){const t=node.nodeValue.trim();if(trans[t])node.nodeValue=trans[t];}else{for(let c of node.childNodes)walk(c);}};walk(document.body);new MutationObserver(muts=>{for(let m of muts){for(let n of m.addedNodes)walk(n);}}).observe(document.body,{childList:true,subtree:true});}" style="display:none;" />
<div class="header-banner">
<div class="header-glow-left"></div>
<div class="header-glow-right"></div>
<h1 class="header-title">
<span class="header-emoji">🎙️</span>
<span class="header-text-animated">Speech Commands AI</span>
</h1>
<p class="header-subtitle">Hệ thống nhận dạng từ khóa giọng nói thời gian thực</p>
<div class="header-badges">
<span class="header-badge">🎯 14 Từ Khóa</span>
<span class="header-badge">⚡ Phản Hồi Thời Gian Thực</span>
<span class="header-badge">🤖 CNN & CRNN Best Checkpoint</span>
</div>
</div>
"""
INFO_HTML = """
<div class="info-box">
<strong class="info-box-title">Pipeline xử lý kỹ thuật:</strong>
1. Đầu vào âm thanh (tối đa 1 giây, 16kHz) -> Log-Mel Spectrogram (40 bins, hop 10ms, win 25ms).<br>
2. Các từ khóa hỗ trợ:
<code class="keyword-code">five</code>
<code class="keyword-code">zero</code>
<code class="keyword-code">yes</code>
<code class="keyword-code">seven</code>
<code class="keyword-code">no</code>
<code class="keyword-code">nine</code>
<code class="keyword-code">down</code>
<code class="keyword-code">one</code>
<code class="keyword-code">go</code>
<code class="keyword-code">two</code>
<code class="keyword-code">stop</code>
<code class="keyword-code">six</code>
<code class="keyword-code special">unknown</code>
<code class="keyword-code special">silence</code>.
</div>
"""
FOOTER_HTML = """
<div class="footer-box">
Lưu ý: Chế độ streaming mic hoạt động tối ưu nhất trên trình duyệt hỗ trợ thu âm liên tục. Trễ truyền tải phụ thuộc vào đường truyền mạng và cấu hình phần cứng.
</div>
"""
# Build Gradio Blocks App
with gr.Blocks(title="Speech Commands Realtime Demo") as demo:
# Header Section
gr.HTML(HEADER_HTML)
# Configuration Panel
with gr.Row(elem_classes=["glass-panel"]):
with gr.Column(scale=2):
model_name = gr.Radio(
choices=["Baseline CNN2D", "CRNN + SpecAugment"],
value="CRNN + SpecAugment",
label="Chọn Mô Hình Dự Đoán",
)
with gr.Column(scale=1):
top_k = gr.Slider(
minimum=1,
maximum=10,
value=5,
step=1,
label="Số lượng Top-k hiển thị"
)
# General Information Panel (Pipeline & Keywords)
gr.HTML(INFO_HTML)
# Main Tabs
with gr.Tabs(elem_classes=["tabs"]):
# Tab 1: Single Audio File / Recording
with gr.Tab("Nhận diện Audio (Tải lên / Ghi âm)", elem_id="tab-single"):
with gr.Row():
with gr.Column(scale=1, elem_classes=["glass-panel"]):
gr.Markdown("### 🎙️ Nhập Audio Đầu Vào")
audio = gr.Audio(
sources=["microphone", "upload"],
type="numpy",
label="Ghi âm hoặc tải file âm thanh (độ dài gợi ý ~1 giây)",
elem_classes=["gradio-audio"],
buttons=["download"]
)
predict_btn = gr.Button("🔮 Bắt Đầu Phân Tích", elem_classes=["primary-btn"])
with gr.Column(scale=1):
gr.Markdown("### 📊 Kết Quả Phân Tích")
output_html = gr.HTML(value=get_empty_prediction_html(), elem_id="single-output-html")
predict_btn.click(
fn=predict_from_audio,
inputs=[audio, model_name, top_k],
outputs=[output_html],
)
# Tab 2: Microphone Realtime Streaming
with gr.Tab("Streaming Microphone (Real-time)", elem_id="tab-stream"):
with gr.Row():
with gr.Column(scale=1, elem_classes=["glass-panel"]):
gr.Markdown("### ⚡ Stream Âm Thanh Trực Tiếp")
gr.Markdown(
"Nhấn **Start Recording** trên microphone bên dưới để bắt đầu. "
"Hệ thống sẽ duy trì buffer 1 giây gần nhất và tự động cập nhật kết quả liên tục."
)
stream_audio = gr.Audio(
sources=["microphone"],
type="numpy",
streaming=True,
label="Microphone stream input",
elem_classes=["gradio-audio"],
buttons=[]
)
with gr.Column(scale=1):
gr.Markdown("### 📊 Kết Quả Thời Gian Thực")
stream_output_html = gr.HTML(value=get_empty_prediction_html("Đang chờ stream microphone bắt đầu..."), elem_id="stream-output-html")
stream_state = gr.State(value=init_state())
stream_audio.stream(
fn=stream_predict,
inputs=[stream_audio, stream_state, model_name, top_k],
outputs=[stream_state, stream_output_html],
stream_every=0.5,
)
# Footer Section
gr.HTML(FOOTER_HTML)
if __name__ == "__main__":
demo.launch(server_name="0.0.0.0", server_port=7860, theme=THEME, css=CUSTOM_CSS)