Spaces:
Sleeping
Sleeping
Download app.py from Roony05/DL_KeySpotting_Project: direct link, hf CLI and curl.
- Browser
- Download file 14.6 kB
-
https://huggingface.co/spaces/Roony05/DL_KeySpotting_Project/resolve/main/app.py
- Command line
-
hf download hf://spaces/Roony05/DL_KeySpotting_Project/app.py
-
curl -L -o app.py https://huggingface.co/spaces/Roony05/DL_KeySpotting_Project/resolve/main/app.py
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) | |