Spaces:
Runtime error
Runtime error
| import gradio as gr | |
| from pathlib import Path | |
| from ui.handlers import handle_verify, handle_upload, handle_reset, handle_audio_change | |
| # Đọc CSS và JS từ file thật | |
| # Bỏ @import trong CSS vì Gradio không cho phép | |
| _CSS = (Path(__file__).parent / "styles.css").read_text() | |
| _JS = (Path(__file__).parent / "recorder.js").read_text() | |
| _RECORDER_HTML = """ | |
| <div id="lumi-recorder-ui"> | |
| <button id="lumi-mic-btn" title="Nhấn để ghi âm">🎙️</button> | |
| <div id="lumi-rec-label">Nhấn để bắt đầu ghi âm</div> | |
| <canvas id="lumi-canvas" width="520" height="72"></canvas> | |
| <div id="lumi-proc-bar"> | |
| <div class="plabel"><span class="pdot">●</span> Đang xử lý âm thanh...</div> | |
| <div class="ptrack"><div class="pfill"></div></div> | |
| </div> | |
| <div id="lumi-audio-playback"> | |
| <div class="play-label">🎧 Nghe lại bản ghi:</div> | |
| <audio id="lumi-audio-el" controls></audio> | |
| </div> | |
| </div> | |
| """ | |
| def build_ui() -> gr.Blocks: | |
| with gr.Blocks(theme=gr.themes.Base(), css=_CSS, title="Lumi Voice Collector") as demo: | |
| # --- Font + permissions (thay thế @import trong CSS) --- | |
| gr.HTML(""" | |
| <link rel="preconnect" href="https://fonts.googleapis.com"> | |
| <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin> | |
| <link href="https://fonts.googleapis.com/css2?family=Be+Vietnam+Pro:wght@300;400;500;600;700&display=swap" rel="stylesheet"> | |
| <meta http-equiv="Permissions-Policy" content="microphone=*"> | |
| """) | |
| # --- Header --- | |
| gr.HTML(""" | |
| <div class="lumi-header"> | |
| <div class="lumi-logo-ring">🎙️</div> | |
| <h1>Thu thập giọng nói "Lumi ơi"</h1> | |
| <p>Giúp huấn luyện wake word cho trợ lý Lumi</p> | |
| </div> | |
| """) | |
| # --- Steps --- | |
| gr.HTML(""" | |
| <div class="lumi-steps"> | |
| <div class="lumi-step"> | |
| <div class="lumi-step-num">1</div> | |
| <div class="lumi-step-lbl">Ghi âm</div> | |
| </div> | |
| <div class="lumi-step-line"></div> | |
| <div class="lumi-step"> | |
| <div class="lumi-step-num">2</div> | |
| <div class="lumi-step-lbl">Kiểm tra</div> | |
| </div> | |
| <div class="lumi-step-line"></div> | |
| <div class="lumi-step"> | |
| <div class="lumi-step-num">3</div> | |
| <div class="lumi-step-lbl">Gửi lên</div> | |
| </div> | |
| </div> | |
| """) | |
| # --- Instruction --- | |
| gr.HTML(""" | |
| <div class="lumi-instruction"> | |
| <div class="ititle">📋 Hướng dẫn</div> | |
| <p> | |
| <span class="hi">1.</span> Nhấn nút mic để bắt đầu ghi âm<br> | |
| <span class="hi">2.</span> Nói rõ <span class="hw">"Lumi ơi"</span> vào micro<br> | |
| <span class="hi">3.</span> Nhấn lại để dừng — đợi thanh xử lý biến mất<br> | |
| <span class="hi">4.</span> Nhấn <b>Kiểm tra</b> → đạt ≥ 0.8 → nhấn <b>Gửi</b> | |
| </p> | |
| </div> | |
| """) | |
| # --- Recorder card --- | |
| with gr.Group(elem_classes="lumi-card"): | |
| gr.HTML(_RECORDER_HTML) | |
| gr.HTML(f"<script>{_JS}</script>") | |
| # --- Hidden Gradio audio --- | |
| with gr.Group(elem_classes="hidden-audio-wrap"): | |
| audio_inst = gr.Audio( | |
| sources=["upload"], | |
| type="filepath", | |
| label="hidden", | |
| visible=False | |
| ) | |
| # --- Status --- | |
| status_txt = gr.Label( | |
| value='🎙️ Sẵn sàng — Nhấn mic và đọc "Lumi ơi"', | |
| elem_classes="lumi-status", | |
| show_label=False | |
| ) | |
| # --- Buttons --- | |
| with gr.Row(elem_classes="lumi-btn-row"): | |
| btn_check = gr.Button("🔍 Kiểm tra mẫu", variant="secondary", elem_classes="lb-check") | |
| btn_reset = gr.Button("🗑 Ghi lại", variant="stop", elem_classes="lb-reset") | |
| btn_send = gr.Button( | |
| "🚀 Gửi lên hệ thống", | |
| variant="primary", | |
| interactive=False, | |
| elem_classes="lb-send" | |
| ) | |
| # --- State --- | |
| score_state = gr.State(0.0) | |
| # --- Event bindings --- | |
| btn_check.click( | |
| fn=handle_verify, | |
| inputs=audio_inst, | |
| outputs=[status_txt, btn_send, score_state] | |
| ) | |
| btn_send.click( | |
| fn=handle_upload, | |
| inputs=[audio_inst, score_state], | |
| outputs=[status_txt, btn_send, audio_inst] | |
| ) | |
| btn_reset.click( | |
| fn=handle_reset, | |
| outputs=[audio_inst, status_txt, btn_send, score_state] | |
| ) | |
| audio_inst.change( | |
| fn=handle_audio_change, | |
| outputs=btn_send | |
| ) | |
| return demo |