nemabruh404 commited on
Commit
e407164
·
verified ·
1 Parent(s): 4c05ad8

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +860 -45
app.py CHANGED
@@ -1,14 +1,3 @@
1
- # from core.model_loader import start_loading
2
- # from ui.layout import build_ui
3
-
4
- # # Kick off model loading ngay khi app start
5
- # # hoàn toàn non-blocking, chạy trong background thread
6
- # start_loading()
7
-
8
- # demo = build_ui()
9
-
10
- # if __name__ == "__main__":
11
- # demo.launch()
12
  import gradio as gr
13
  import os, uuid, librosa, threading, time, tempfile, wave, io, csv
14
  import numpy as np
@@ -169,19 +158,19 @@ CSS = """
169
  *, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
170
 
171
  :root {
172
- --bg: #080b10;
173
- --surface: #0e1219;
174
- --surface2: #141920;
175
- --border: #1d2535;
176
- --border2: #252f42;
177
- --accent: #4f8ef7;
178
- --accent2: #7eb8ff;
179
- --success: #34d399;
180
- --danger: #f87171;
181
- --warn: #fbbf24;
182
- --text: #e8edf5;
183
- --text2: #8896b0;
184
- --text3: #4a566e;
185
  --mono: 'DM Mono', monospace;
186
  }
187
 
@@ -206,6 +195,11 @@ body, .gradio-container {
206
  display: flex;
207
  align-items: center;
208
  gap: 16px;
 
 
 
 
 
209
  }
210
 
211
  .lumi-icon-wrap {
@@ -222,7 +216,7 @@ body, .gradio-container {
222
  .lumi-icon-wrap::before {
223
  content: '';
224
  position: absolute; inset: 0;
225
- background: radial-gradient(circle at 30% 30%, rgba(79,142,247,0.15), transparent 70%);
226
  }
227
 
228
  .lumi-header-text h1 {
@@ -311,16 +305,16 @@ body, .gradio-container {
311
  font-family: var(--mono);
312
  font-size: 10px;
313
  color: var(--accent);
314
- background: rgba(79,142,247,0.1);
315
- border: 1px solid rgba(79,142,247,0.2);
316
  border-radius: 4px;
317
  padding: 1px 5px;
318
  flex-shrink: 0;
319
  }
320
  .lumi-keyword {
321
- color: var(--text);
322
  font-weight: 600;
323
- background: rgba(255,255,255,0.05);
324
  border-radius: 4px;
325
  padding: 0 4px;
326
  }
@@ -363,12 +357,12 @@ body, .gradio-container {
363
  transition: all 0.3s ease;
364
  }
365
  #lumi-mic-btn:hover {
366
- background: #182030;
367
  border-color: var(--accent);
368
  transform: scale(1.04);
369
  }
370
  #lumi-mic-btn:hover::after {
371
- border-color: rgba(79,142,247,0.3);
372
  inset: -5px;
373
  }
374
  #lumi-mic-btn.recording {
@@ -506,7 +500,7 @@ button.lb-check {
506
  border: 1px solid var(--border2) !important;
507
  }
508
  button.lb-check:hover:not(:disabled) {
509
- background: #1a2338 !important;
510
  border-color: var(--accent) !important;
511
  color: var(--accent2) !important;
512
  }
@@ -529,9 +523,9 @@ button.lb-send {
529
  letter-spacing: -0.1px !important;
530
  }
531
  button.lb-send:hover:not(:disabled) {
532
- background: #6aa0f8 !important;
533
  transform: translateY(-1px) !important;
534
- box-shadow: 0 4px 16px rgba(79,142,247,0.35) !important;
535
  }
536
  button.lb-send:disabled {
537
  background: var(--surface2) !important;
@@ -687,7 +681,7 @@ RECORDER_HTML = """
687
  if (intensity > 0.05) {
688
  const cx = W/2, cy = H/2;
689
  const grd = ctx2d.createRadialGradient(cx, cy, 0, cx, cy, W/2);
690
- grd.addColorStop(0, `rgba(79,142,247,${0.04 * intensity})`);
691
  grd.addColorStop(1, 'transparent');
692
  ctx2d.fillStyle = grd;
693
  ctx2d.fillRect(0, 0, W, H);
@@ -695,17 +689,17 @@ RECORDER_HTML = """
695
 
696
  /* waveform line */
697
  const grad = ctx2d.createLinearGradient(0, 0, W, 0);
698
- grad.addColorStop(0, 'rgba(79,142,247,0)');
699
- grad.addColorStop(0.15, `rgba(79,142,247,${0.4 + intensity * 0.6})`);
700
- grad.addColorStop(0.5, `rgba(126,184,255,${0.6 + intensity * 0.4})`);
701
- grad.addColorStop(0.85, `rgba(79,142,247,${0.4 + intensity * 0.6})`);
702
- grad.addColorStop(1, 'rgba(79,142,247,0)');
703
 
704
  ctx2d.beginPath();
705
- ctx2d.lineWidth = 1.5;
706
  ctx2d.strokeStyle = grad;
707
- ctx2d.shadowColor = '#4f8ef7';
708
- ctx2d.shadowBlur = 6 * intensity;
709
 
710
  const sw = W / buf;
711
  for (let i = 0; i < buf; i++) {
@@ -721,7 +715,7 @@ RECORDER_HTML = """
721
  ctx2d.beginPath();
722
  ctx2d.moveTo(0, canvas.height/2);
723
  ctx2d.lineTo(canvas.width, canvas.height/2);
724
- ctx2d.strokeStyle = 'rgba(29,37,53,0.8)';
725
  ctx2d.lineWidth = 1;
726
  ctx2d.stroke();
727
  }
@@ -829,4 +823,825 @@ with gr.Blocks(theme=gr.themes.Base(), css=CSS, title="Lumi Voice Collector") as
829
  btn_reset.click(fn=reset_ui, outputs=[audio_inst, status_txt, btn_send, score_state])
830
  audio_inst.change(lambda: gr.update(interactive=False), None, btn_send)
831
 
832
- demo.launch()
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
  import gradio as gr
2
  import os, uuid, librosa, threading, time, tempfile, wave, io, csv
3
  import numpy as np
 
158
  *, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
159
 
160
  :root {
161
+ --bg: #f4f6fb;
162
+ --surface: #ffffff;
163
+ --surface2: #eef1f8;
164
+ --border: #dde2ef;
165
+ --border2: #c8d0e4;
166
+ --accent: #3b7ef4;
167
+ --accent2: #1a5fe0;
168
+ --success: #16a34a;
169
+ --danger: #dc2626;
170
+ --warn: #d97706;
171
+ --text: #1a2236;
172
+ --text2: #4a5778;
173
+ --text3: #8a96b0;
174
  --mono: 'DM Mono', monospace;
175
  }
176
 
 
195
  display: flex;
196
  align-items: center;
197
  gap: 16px;
198
+ background: linear-gradient(135deg, #eef3ff 0%, #f4f6fb 100%);
199
+ border-radius: 16px;
200
+ padding: 20px;
201
+ margin-bottom: 24px;
202
+ border: 1px solid var(--border);
203
  }
204
 
205
  .lumi-icon-wrap {
 
216
  .lumi-icon-wrap::before {
217
  content: '';
218
  position: absolute; inset: 0;
219
+ background: radial-gradient(circle at 30% 30%, rgba(59,126,244,0.12), transparent 70%);
220
  }
221
 
222
  .lumi-header-text h1 {
 
305
  font-family: var(--mono);
306
  font-size: 10px;
307
  color: var(--accent);
308
+ background: rgba(59,126,244,0.08);
309
+ border: 1px solid rgba(59,126,244,0.2);
310
  border-radius: 4px;
311
  padding: 1px 5px;
312
  flex-shrink: 0;
313
  }
314
  .lumi-keyword {
315
+ color: var(--accent2);
316
  font-weight: 600;
317
+ background: rgba(59,126,244,0.08);
318
  border-radius: 4px;
319
  padding: 0 4px;
320
  }
 
357
  transition: all 0.3s ease;
358
  }
359
  #lumi-mic-btn:hover {
360
+ background: #e8effc;
361
  border-color: var(--accent);
362
  transform: scale(1.04);
363
  }
364
  #lumi-mic-btn:hover::after {
365
+ border-color: rgba(59,126,244,0.25);
366
  inset: -5px;
367
  }
368
  #lumi-mic-btn.recording {
 
500
  border: 1px solid var(--border2) !important;
501
  }
502
  button.lb-check:hover:not(:disabled) {
503
+ background: #dce8ff !important;
504
  border-color: var(--accent) !important;
505
  color: var(--accent2) !important;
506
  }
 
523
  letter-spacing: -0.1px !important;
524
  }
525
  button.lb-send:hover:not(:disabled) {
526
+ background: var(--accent2) !important;
527
  transform: translateY(-1px) !important;
528
+ box-shadow: 0 4px 16px rgba(59,126,244,0.3) !important;
529
  }
530
  button.lb-send:disabled {
531
  background: var(--surface2) !important;
 
681
  if (intensity > 0.05) {
682
  const cx = W/2, cy = H/2;
683
  const grd = ctx2d.createRadialGradient(cx, cy, 0, cx, cy, W/2);
684
+ grd.addColorStop(0, `rgba(59,126,244,${0.06 * intensity})`);
685
  grd.addColorStop(1, 'transparent');
686
  ctx2d.fillStyle = grd;
687
  ctx2d.fillRect(0, 0, W, H);
 
689
 
690
  /* waveform line */
691
  const grad = ctx2d.createLinearGradient(0, 0, W, 0);
692
+ grad.addColorStop(0, 'rgba(59,126,244,0)');
693
+ grad.addColorStop(0.15, `rgba(59,126,244,${0.5 + intensity * 0.5})`);
694
+ grad.addColorStop(0.5, `rgba(26,95,224,${0.7 + intensity * 0.3})`);
695
+ grad.addColorStop(0.85, `rgba(59,126,244,${0.5 + intensity * 0.5})`);
696
+ grad.addColorStop(1, 'rgba(59,126,244,0)');
697
 
698
  ctx2d.beginPath();
699
+ ctx2d.lineWidth = 2;
700
  ctx2d.strokeStyle = grad;
701
+ ctx2d.shadowColor = '#3b7ef4';
702
+ ctx2d.shadowBlur = 4 * intensity;
703
 
704
  const sw = W / buf;
705
  for (let i = 0; i < buf; i++) {
 
715
  ctx2d.beginPath();
716
  ctx2d.moveTo(0, canvas.height/2);
717
  ctx2d.lineTo(canvas.width, canvas.height/2);
718
+ ctx2d.strokeStyle = 'rgba(180,192,220,0.6)';
719
  ctx2d.lineWidth = 1;
720
  ctx2d.stroke();
721
  }
 
823
  btn_reset.click(fn=reset_ui, outputs=[audio_inst, status_txt, btn_send, score_state])
824
  audio_inst.change(lambda: gr.update(interactive=False), None, btn_send)
825
 
826
+ demo.launch()
827
+ # import gradio as gr
828
+ # import os, uuid, librosa, threading, time, tempfile, wave, io, csv
829
+ # import numpy as np
830
+ # from huggingface_hub import HfApi, CommitOperationAdd
831
+ # from openwakeword.model import Model
832
+
833
+ # # --- CẤU HÌNH ---
834
+ # DATASET_ID = "lumiwakeword/lumioiv1"
835
+ # HF_TOKEN = os.getenv("HF_TOKEN")
836
+ # MODEL_PATH = "loo_mee_oy_v2.onnx"
837
+
838
+ # api = HfApi()
839
+
840
+ # # --- KHỞI TẠO MODEL ---
841
+ # oww_model = None
842
+ # model_ready = threading.Event()
843
+
844
+ # def load_model():
845
+ # global oww_model
846
+ # try:
847
+ # t_start = time.perf_counter()
848
+ # dummy_wav = np.zeros(16000, dtype=np.float32)
849
+ # with tempfile.NamedTemporaryFile(suffix=".wav", delete=False) as f:
850
+ # tmp_path = f.name
851
+ # with wave.open(tmp_path, "w") as wf:
852
+ # wf.setnchannels(1); wf.setsampwidth(2); wf.setframerate(16000)
853
+ # wf.writeframes((dummy_wav * 32767).astype(np.int16).tobytes())
854
+ # librosa.load(tmp_path, sr=16000)
855
+ # os.unlink(tmp_path)
856
+ # print(f" librosa warm-up: {(time.perf_counter()-t_start)*1000:.0f}ms")
857
+
858
+ # t2 = time.perf_counter()
859
+ # m = Model(wakeword_model_paths=[MODEL_PATH])
860
+ # print(f" Model load: {(time.perf_counter()-t2)*1000:.0f}ms")
861
+
862
+ # stable_count = 0
863
+ # for i in range(80):
864
+ # t_iter = time.perf_counter()
865
+ # m.predict(np.zeros(1280, dtype=np.int16))
866
+ # elapsed_ms = (time.perf_counter() - t_iter) * 1000
867
+ # if i < 3: continue
868
+ # if elapsed_ms < 5.0:
869
+ # stable_count += 1
870
+ # if stable_count >= 5:
871
+ # print(f" ONNX JIT stable sau {i+1} iters ({elapsed_ms:.1f}ms/iter)")
872
+ # break
873
+ # else:
874
+ # stable_count = 0
875
+
876
+ # oww_model = m
877
+ # print(f"✅ Fully ready — tổng boot: {(time.perf_counter()-t_start):.1f}s")
878
+ # except Exception as e:
879
+ # print(f"❌ Model Error: {e}")
880
+ # finally:
881
+ # model_ready.set()
882
+
883
+ # threading.Thread(target=load_model, daemon=True).start()
884
+
885
+
886
+ # # --- PYTHON LOGIC ---
887
+ # def verify_audio(audio_path):
888
+ # if not model_ready.wait(timeout=30):
889
+ # return "⏳ Model đang khởi động, thử lại sau vài giây...", gr.update(interactive=False), 0.0
890
+ # if not audio_path:
891
+ # return "⚠️ Hãy ghi âm trước khi kiểm tra!", gr.update(interactive=False), 0.0
892
+ # if oww_model is None:
893
+ # return "❌ Model lỗi, vui lòng reload trang.", gr.update(interactive=False), 0.0
894
+ # try:
895
+ # y, sr = librosa.load(audio_path, sr=16000)
896
+ # audio_int16 = (y * 32767).astype(np.int16)
897
+ # oww_model.reset()
898
+ # max_score = 0.0
899
+ # chunk_size = 1280
900
+ # for i in range(0, len(audio_int16), chunk_size):
901
+ # chunk = audio_int16[i: i + chunk_size]
902
+ # if len(chunk) < chunk_size:
903
+ # chunk = np.pad(chunk, (0, chunk_size - len(chunk)))
904
+ # prediction = oww_model.predict(chunk)
905
+ # score_val = list(prediction.values())[0]
906
+ # current_score = float(score_val[0]) if isinstance(score_val, (np.ndarray, list)) else float(score_val)
907
+ # if current_score > max_score:
908
+ # max_score = current_score
909
+ # for _ in range(7):
910
+ # oww_model.predict(np.zeros(1280, dtype=np.int16))
911
+ # if max_score >= 0.8:
912
+ # return f"✅ Hợp lệ! Điểm: {max_score:.2f} — Nhấn GỬI để lưu mẫu", gr.update(interactive=True), max_score
913
+ # else:
914
+ # return f"❌ Chưa đạt (Điểm: {max_score:.2f}). Hãy nói rõ hơn và thử lại!", gr.update(interactive=False), max_score
915
+ # except Exception as e:
916
+ # return f"❌ Lỗi xử lý: {str(e)}", gr.update(interactive=False), 0.0
917
+
918
+
919
+ # def upload_final(audio_path, score):
920
+ # if not audio_path:
921
+ # return "❌ Không có file để gửi.", gr.update(interactive=False), None
922
+ # try:
923
+ # filename = f"lumi_{score:.2f}_{uuid.uuid4().hex[:4]}.wav"
924
+ # audio_repo_path = f"audios/{filename}"
925
+
926
+ # try:
927
+ # existing = api.hf_hub_download(
928
+ # repo_id=DATASET_ID,
929
+ # filename="metadata.csv",
930
+ # repo_type="dataset",
931
+ # token=HF_TOKEN
932
+ # )
933
+ # with open(existing, "r", encoding="utf-8") as f:
934
+ # rows = list(csv.reader(f))
935
+ # except Exception:
936
+ # rows = [["file_name"]]
937
+
938
+ # rows.append([audio_repo_path])
939
+
940
+ # buf = io.StringIO()
941
+ # csv.writer(buf).writerows(rows)
942
+ # csv_bytes = buf.getvalue().encode("utf-8")
943
+
944
+ # operations = [
945
+ # CommitOperationAdd(
946
+ # path_in_repo=audio_repo_path,
947
+ # path_or_fileobj=audio_path,
948
+ # ),
949
+ # CommitOperationAdd(
950
+ # path_in_repo="metadata.csv",
951
+ # path_or_fileobj=io.BytesIO(csv_bytes),
952
+ # ),
953
+ # ]
954
+
955
+ # api.create_commit(
956
+ # repo_id=DATASET_ID,
957
+ # repo_type="dataset",
958
+ # operations=operations,
959
+ # commit_message=f"add {filename}",
960
+ # token=HF_TOKEN,
961
+ # )
962
+
963
+ # return "🎉 Gửi thành công! Bạn có thể thu mẫu tiếp theo.", gr.update(interactive=False), None
964
+
965
+ # except Exception as e:
966
+ # return f"❌ Lỗi gửi: {e}", gr.update(interactive=True), audio_path
967
+
968
+
969
+ # def reset_ui():
970
+ # return (
971
+ # None,
972
+ # 'Sẵn sàng — Nhấn mic và đọc "Lumi ơi"',
973
+ # gr.update(interactive=False),
974
+ # 0.0
975
+ # )
976
+
977
+
978
+ # # ============================================================
979
+ # # CSS — Premium dark aesthetic, editorial feel
980
+ # # ============================================================
981
+ # CSS = """
982
+ # @import url('https://fonts.googleapis.com/css2?family=DM+Sans:ital,opsz,wght@0,9..40,300;0,9..40,400;0,9..40,500;0,9..40,600;1,9..40,300&family=DM+Mono:wght@400;500&display=swap');
983
+
984
+ # *, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
985
+
986
+ # :root {
987
+ # --bg: #080b10;
988
+ # --surface: #0e1219;
989
+ # --surface2: #141920;
990
+ # --border: #1d2535;
991
+ # --border2: #252f42;
992
+ # --accent: #4f8ef7;
993
+ # --accent2: #7eb8ff;
994
+ # --success: #34d399;
995
+ # --danger: #f87171;
996
+ # --warn: #fbbf24;
997
+ # --text: #e8edf5;
998
+ # --text2: #8896b0;
999
+ # --text3: #4a566e;
1000
+ # --mono: 'DM Mono', monospace;
1001
+ # }
1002
+
1003
+ # body, .gradio-container {
1004
+ # background: var(--bg) !important;
1005
+ # font-family: 'DM Sans', sans-serif !important;
1006
+ # min-height: 100vh;
1007
+ # color: var(--text) !important;
1008
+ # }
1009
+
1010
+ # .gradio-container {
1011
+ # max-width: 520px !important;
1012
+ # margin: 0 auto !important;
1013
+ # padding: 40px 20px 80px !important;
1014
+ # }
1015
+
1016
+ # /* ── HEADER ── */
1017
+ # .lumi-header {
1018
+ # margin-bottom: 36px;
1019
+ # padding-bottom: 28px;
1020
+ # border-bottom: 1px solid var(--border);
1021
+ # display: flex;
1022
+ # align-items: center;
1023
+ # gap: 16px;
1024
+ # }
1025
+
1026
+ # .lumi-icon-wrap {
1027
+ # width: 48px; height: 48px;
1028
+ # background: var(--surface2);
1029
+ # border: 1px solid var(--border2);
1030
+ # border-radius: 14px;
1031
+ # display: flex; align-items: center; justify-content: center;
1032
+ # font-size: 22px;
1033
+ # flex-shrink: 0;
1034
+ # position: relative;
1035
+ # overflow: hidden;
1036
+ # }
1037
+ # .lumi-icon-wrap::before {
1038
+ # content: '';
1039
+ # position: absolute; inset: 0;
1040
+ # background: radial-gradient(circle at 30% 30%, rgba(79,142,247,0.15), transparent 70%);
1041
+ # }
1042
+
1043
+ # .lumi-header-text h1 {
1044
+ # font-size: 17px;
1045
+ # font-weight: 600;
1046
+ # color: var(--text);
1047
+ # letter-spacing: -0.2px;
1048
+ # line-height: 1.3;
1049
+ # }
1050
+ # .lumi-header-text p {
1051
+ # font-size: 12.5px;
1052
+ # color: var(--text3);
1053
+ # margin-top: 3px;
1054
+ # font-weight: 400;
1055
+ # }
1056
+
1057
+ # /* ── PROGRESS STEPS ── */
1058
+ # .lumi-progress {
1059
+ # display: flex;
1060
+ # align-items: center;
1061
+ # gap: 0;
1062
+ # margin-bottom: 28px;
1063
+ # }
1064
+ # .lumi-prog-step {
1065
+ # display: flex;
1066
+ # align-items: center;
1067
+ # gap: 8px;
1068
+ # flex: 1;
1069
+ # }
1070
+ # .lumi-prog-dot {
1071
+ # width: 28px; height: 28px;
1072
+ # border-radius: 50%;
1073
+ # background: var(--surface2);
1074
+ # border: 1px solid var(--border);
1075
+ # display: flex; align-items: center; justify-content: center;
1076
+ # font-size: 11px;
1077
+ # font-weight: 600;
1078
+ # color: var(--text3);
1079
+ # font-family: var(--mono);
1080
+ # flex-shrink: 0;
1081
+ # }
1082
+ # .lumi-prog-info { flex: 1; }
1083
+ # .lumi-prog-title { font-size: 11px; font-weight: 600; color: var(--text3); letter-spacing: 0.3px; }
1084
+ # .lumi-prog-line { height: 1px; background: var(--border); width: 24px; flex-shrink: 0; margin: 0 4px; }
1085
+
1086
+ # /* ── INSTRUCTION BOX ── */
1087
+ # .lumi-guide {
1088
+ # background: var(--surface);
1089
+ # border: 1px solid var(--border);
1090
+ # border-radius: 12px;
1091
+ # padding: 16px 18px;
1092
+ # margin-bottom: 20px;
1093
+ # display: flex;
1094
+ # gap: 14px;
1095
+ # align-items: flex-start;
1096
+ # }
1097
+ # .lumi-guide-icon {
1098
+ # font-size: 16px;
1099
+ # margin-top: 1px;
1100
+ # flex-shrink: 0;
1101
+ # opacity: 0.8;
1102
+ # }
1103
+ # .lumi-guide-body { flex: 1; }
1104
+ # .lumi-guide-title {
1105
+ # font-size: 10.5px;
1106
+ # font-weight: 600;
1107
+ # color: var(--text3);
1108
+ # text-transform: uppercase;
1109
+ # letter-spacing: 0.8px;
1110
+ # margin-bottom: 8px;
1111
+ # }
1112
+ # .lumi-guide-steps {
1113
+ # display: flex;
1114
+ # flex-direction: column;
1115
+ # gap: 5px;
1116
+ # }
1117
+ # .lumi-guide-step {
1118
+ # display: flex;
1119
+ # align-items: baseline;
1120
+ # gap: 8px;
1121
+ # font-size: 13px;
1122
+ # color: var(--text2);
1123
+ # line-height: 1.5;
1124
+ # }
1125
+ # .lumi-guide-num {
1126
+ # font-family: var(--mono);
1127
+ # font-size: 10px;
1128
+ # color: var(--accent);
1129
+ # background: rgba(79,142,247,0.1);
1130
+ # border: 1px solid rgba(79,142,247,0.2);
1131
+ # border-radius: 4px;
1132
+ # padding: 1px 5px;
1133
+ # flex-shrink: 0;
1134
+ # }
1135
+ # .lumi-keyword {
1136
+ # color: var(--text);
1137
+ # font-weight: 600;
1138
+ # background: rgba(255,255,255,0.05);
1139
+ # border-radius: 4px;
1140
+ # padding: 0 4px;
1141
+ # }
1142
+
1143
+ # /* ── RECORDER CARD ── */
1144
+ # .lumi-recorder-card {
1145
+ # background: var(--surface);
1146
+ # border: 1px solid var(--border);
1147
+ # border-radius: 16px;
1148
+ # padding: 24px;
1149
+ # margin-bottom: 14px;
1150
+ # }
1151
+
1152
+ # #lumi-recorder-ui {
1153
+ # display: flex;
1154
+ # flex-direction: column;
1155
+ # align-items: center;
1156
+ # gap: 18px;
1157
+ # }
1158
+
1159
+ # /* Mic button */
1160
+ # #lumi-mic-btn {
1161
+ # width: 72px; height: 72px;
1162
+ # border-radius: 50%;
1163
+ # background: var(--surface2);
1164
+ # border: 1px solid var(--border2);
1165
+ # cursor: pointer;
1166
+ # display: flex; align-items: center; justify-content: center;
1167
+ # font-size: 26px;
1168
+ # outline: none;
1169
+ # transition: all 0.2s cubic-bezier(.4,0,.2,1);
1170
+ # position: relative;
1171
+ # }
1172
+ # #lumi-mic-btn::after {
1173
+ # content: '';
1174
+ # position: absolute;
1175
+ # inset: -1px;
1176
+ # border-radius: 50%;
1177
+ # border: 1px solid transparent;
1178
+ # transition: all 0.3s ease;
1179
+ # }
1180
+ # #lumi-mic-btn:hover {
1181
+ # background: #182030;
1182
+ # border-color: var(--accent);
1183
+ # transform: scale(1.04);
1184
+ # }
1185
+ # #lumi-mic-btn:hover::after {
1186
+ # border-color: rgba(79,142,247,0.3);
1187
+ # inset: -5px;
1188
+ # }
1189
+ # #lumi-mic-btn.recording {
1190
+ # background: rgba(248,113,113,0.1);
1191
+ # border-color: var(--danger);
1192
+ # animation: rec-pulse 1.8s ease infinite;
1193
+ # }
1194
+ # @keyframes rec-pulse {
1195
+ # 0%,100% { box-shadow: 0 0 0 0 rgba(248,113,113,0.3); }
1196
+ # 50% { box-shadow: 0 0 0 14px rgba(248,113,113,0); }
1197
+ # }
1198
+
1199
+ # /* Rec label */
1200
+ # #lumi-rec-label {
1201
+ # font-size: 12px;
1202
+ # color: var(--text3);
1203
+ # font-weight: 500;
1204
+ # letter-spacing: 0.2px;
1205
+ # text-align: center;
1206
+ # }
1207
+ # #lumi-rec-label.recording {
1208
+ # color: var(--danger);
1209
+ # }
1210
+
1211
+ # /* Canvas */
1212
+ # #lumi-canvas {
1213
+ # width: 100%;
1214
+ # height: 60px;
1215
+ # border-radius: 8px;
1216
+ # background: var(--bg);
1217
+ # border: 1px solid var(--border);
1218
+ # display: block;
1219
+ # }
1220
+
1221
+ # /* Processing bar */
1222
+ # #lumi-proc-bar {
1223
+ # display: none;
1224
+ # width: 100%;
1225
+ # }
1226
+ # #lumi-proc-bar .p-label {
1227
+ # font-size: 11px;
1228
+ # font-family: var(--mono);
1229
+ # color: var(--text3);
1230
+ # margin-bottom: 6px;
1231
+ # display: flex; align-items: center; gap: 6px;
1232
+ # }
1233
+ # #lumi-proc-bar .p-dot {
1234
+ # width: 6px; height: 6px;
1235
+ # border-radius: 50%;
1236
+ # background: var(--accent);
1237
+ # animation: blink 1s ease infinite;
1238
+ # }
1239
+ # @keyframes blink { 0%,100%{opacity:1} 50%{opacity:0.2} }
1240
+ # #lumi-proc-bar .p-track {
1241
+ # height: 2px;
1242
+ # background: var(--border);
1243
+ # border-radius: 99px;
1244
+ # overflow: hidden;
1245
+ # }
1246
+ # #lumi-proc-bar .p-fill {
1247
+ # height: 100%;
1248
+ # background: var(--accent);
1249
+ # border-radius: 99px;
1250
+ # animation: slide 1.4s ease-in-out infinite;
1251
+ # }
1252
+ # @keyframes slide {
1253
+ # 0% { width:0%; margin-left:0%; }
1254
+ # 50% { width:50%; margin-left:25%; }
1255
+ # 100% { width:0%; margin-left:100%; }
1256
+ # }
1257
+
1258
+ # /* Playback */
1259
+ # #lumi-audio-playback {
1260
+ # width: 100%;
1261
+ # display: none;
1262
+ # flex-direction: column;
1263
+ # gap: 6px;
1264
+ # }
1265
+ # #lumi-audio-playback .play-label {
1266
+ # font-size: 11px;
1267
+ # font-family: var(--mono);
1268
+ # color: var(--text3);
1269
+ # letter-spacing: 0.3px;
1270
+ # }
1271
+ # #lumi-audio-playback audio {
1272
+ # width: 100%;
1273
+ # height: 32px;
1274
+ # border-radius: 6px;
1275
+ # outline: none;
1276
+ # accent-color: var(--accent);
1277
+ # }
1278
+
1279
+ # /* ── STATUS ── */
1280
+ # .lumi-status {
1281
+ # background: var(--surface) !important;
1282
+ # border: 1px solid var(--border) !important;
1283
+ # border-radius: 10px !important;
1284
+ # padding: 12px 16px !important;
1285
+ # color: var(--text2) !important;
1286
+ # font-size: 13px !important;
1287
+ # font-weight: 500 !important;
1288
+ # min-height: 46px !important;
1289
+ # font-family: var(--mono) !important;
1290
+ # }
1291
+ # .lumi-status .output-class {
1292
+ # font-size: 13px !important;
1293
+ # color: var(--text2) !important;
1294
+ # font-family: var(--mono) !important;
1295
+ # }
1296
+
1297
+ # /* ── BUTTONS ── */
1298
+ # .lumi-btn-row {
1299
+ # display: grid;
1300
+ # grid-template-columns: 1fr 1fr;
1301
+ # gap: 8px;
1302
+ # margin-bottom: 8px;
1303
+ # }
1304
+
1305
+ # button.lb-check, button.lb-reset, button.lb-send {
1306
+ # border-radius: 8px !important;
1307
+ # font-family: 'DM Sans', sans-serif !important;
1308
+ # font-weight: 500 !important;
1309
+ # font-size: 13px !important;
1310
+ # padding: 11px 16px !important;
1311
+ # border: none !important;
1312
+ # cursor: pointer !important;
1313
+ # transition: all 0.18s ease !important;
1314
+ # width: 100% !important;
1315
+ # letter-spacing: 0.1px !important;
1316
+ # }
1317
+
1318
+ # button.lb-check {
1319
+ # background: var(--surface2) !important;
1320
+ # color: var(--text2) !important;
1321
+ # border: 1px solid var(--border2) !important;
1322
+ # }
1323
+ # button.lb-check:hover:not(:disabled) {
1324
+ # background: #1a2338 !important;
1325
+ # border-color: var(--accent) !important;
1326
+ # color: var(--accent2) !important;
1327
+ # }
1328
+
1329
+ # button.lb-reset {
1330
+ # background: transparent !important;
1331
+ # color: var(--text3) !important;
1332
+ # border: 1px solid var(--border) !important;
1333
+ # }
1334
+ # button.lb-reset:hover:not(:disabled) {
1335
+ # color: var(--text2) !important;
1336
+ # border-color: var(--border2) !important;
1337
+ # background: var(--surface2) !important;
1338
+ # }
1339
+
1340
+ # button.lb-send {
1341
+ # background: var(--accent) !important;
1342
+ # color: #fff !important;
1343
+ # font-weight: 600 !important;
1344
+ # letter-spacing: -0.1px !important;
1345
+ # }
1346
+ # button.lb-send:hover:not(:disabled) {
1347
+ # background: #6aa0f8 !important;
1348
+ # transform: translateY(-1px) !important;
1349
+ # box-shadow: 0 4px 16px rgba(79,142,247,0.35) !important;
1350
+ # }
1351
+ # button.lb-send:disabled {
1352
+ # background: var(--surface2) !important;
1353
+ # color: var(--text3) !important;
1354
+ # box-shadow: none !important;
1355
+ # cursor: not-allowed !important;
1356
+ # }
1357
+
1358
+ # /* ── HIDDEN AUDIO WRAP ── */
1359
+ # .hidden-audio-wrap {
1360
+ # position: absolute;
1361
+ # width: 1px; height: 1px;
1362
+ # overflow: hidden; opacity: 0;
1363
+ # pointer-events: none;
1364
+ # }
1365
+
1366
+ # /* ── DIVIDER ── */
1367
+ # .lumi-divider {
1368
+ # height: 1px;
1369
+ # background: var(--border);
1370
+ # margin: 20px 0;
1371
+ # }
1372
+
1373
+ # footer { display: none !important; }
1374
+ # .contain { background: transparent !important; }
1375
+ # """
1376
+
1377
+
1378
+ # # ============================================================
1379
+ # # RECORDER HTML
1380
+ # # ============================================================
1381
+ # RECORDER_HTML = """
1382
+ # <div id="lumi-recorder-ui">
1383
+ # <button id="lumi-mic-btn" title="Nhấn để ghi âm">🎙</button>
1384
+ # <div id="lumi-rec-label">Nhấn để bắt đầu ghi âm</div>
1385
+ # <canvas id="lumi-canvas" width="520" height="60"></canvas>
1386
+ # <div id="lumi-proc-bar">
1387
+ # <div class="p-label"><span class="p-dot"></span>Đang xử lý...</div>
1388
+ # <div class="p-track"><div class="p-fill"></div></div>
1389
+ # </div>
1390
+ # <div id="lumi-audio-playback">
1391
+ # <div class="play-label">// PLAYBACK</div>
1392
+ # <audio id="lumi-audio-el" controls></audio>
1393
+ # </div>
1394
+ # </div>
1395
+
1396
+ # <script>
1397
+ # (function () {
1398
+ # const micBtn = document.getElementById('lumi-mic-btn');
1399
+ # const recLabel = document.getElementById('lumi-rec-label');
1400
+ # const canvas = document.getElementById('lumi-canvas');
1401
+ # const ctx2d = canvas.getContext('2d');
1402
+ # const procBar = document.getElementById('lumi-proc-bar');
1403
+ # const playWrap = document.getElementById('lumi-audio-playback');
1404
+ # const audioEl = document.getElementById('lumi-audio-el');
1405
+
1406
+ # let isRecording = false, mediaRecorder = null, audioChunks = [];
1407
+ # let audioCtx = null, analyser = null, micStream = null, animId = null;
1408
+
1409
+ # micBtn.addEventListener('click', () => isRecording ? stopRecording() : startRecording());
1410
+
1411
+ # async function startRecording() {
1412
+ # try {
1413
+ # micStream = await navigator.mediaDevices.getUserMedia({ audio: true, video: false });
1414
+ # } catch (e) {
1415
+ # recLabel.textContent = '✕ Không có quyền truy cập micro';
1416
+ # return;
1417
+ # }
1418
+ # audioCtx = new (window.AudioContext || window.webkitAudioContext)();
1419
+ # analyser = audioCtx.createAnalyser();
1420
+ # analyser.fftSize = 256;
1421
+ # audioCtx.createMediaStreamSource(micStream).connect(analyser);
1422
+
1423
+ # audioChunks = [];
1424
+ # const mime = ['audio/webm;codecs=opus','audio/webm','audio/ogg']
1425
+ # .find(t => MediaRecorder.isTypeSupported(t)) || '';
1426
+ # mediaRecorder = new MediaRecorder(micStream, mime ? { mimeType: mime } : {});
1427
+ # mediaRecorder.ondataavailable = e => { if (e.data.size > 0) audioChunks.push(e.data); };
1428
+ # mediaRecorder.onstop = onDone;
1429
+ # mediaRecorder.start(100);
1430
+
1431
+ # isRecording = true;
1432
+ # micBtn.classList.add('recording');
1433
+ # micBtn.textContent = '⏹';
1434
+ # recLabel.textContent = '● Đang ghi — nhấn để dừng';
1435
+ # recLabel.classList.add('recording');
1436
+ # playWrap.style.display = 'none';
1437
+ # procBar.style.display = 'none';
1438
+ # drawWave();
1439
+ # }
1440
+
1441
+ # function stopRecording() {
1442
+ # mediaRecorder && mediaRecorder.state !== 'inactive' && mediaRecorder.stop();
1443
+ # micStream && micStream.getTracks().forEach(t => t.stop());
1444
+ # audioCtx && audioCtx.close();
1445
+ # animId && cancelAnimationFrame(animId);
1446
+ # micStream = audioCtx = analyser = null;
1447
+
1448
+ # isRecording = false;
1449
+ # micBtn.classList.remove('recording');
1450
+ # micBtn.textContent = '🎙';
1451
+ # recLabel.textContent = 'Nhấn để ghi lại';
1452
+ # recLabel.classList.remove('recording');
1453
+ # procBar.style.display = 'block';
1454
+ # drawFlatLine();
1455
+ # }
1456
+
1457
+ # function onDone() {
1458
+ # const mime = audioChunks[0]?.type || 'audio/webm';
1459
+ # const blob = new Blob(audioChunks, { type: mime });
1460
+ # const url = URL.createObjectURL(blob);
1461
+ # audioEl.src = url;
1462
+ # audioEl.oncanplay = () => {
1463
+ # procBar.style.display = 'none';
1464
+ # playWrap.style.display = 'flex';
1465
+ # };
1466
+ # injectFile(blob, mime);
1467
+ # }
1468
+
1469
+ # function injectFile(blob, mime) {
1470
+ # const ext = mime.includes('ogg') ? 'ogg' : 'webm';
1471
+ # const file = new File([blob], 'recording.' + ext, { type: mime });
1472
+ # function attempt(tries) {
1473
+ # const inp = document.querySelector('.hidden-audio-wrap input[type="file"]');
1474
+ # if (inp) {
1475
+ # const dt = new DataTransfer();
1476
+ # dt.items.add(file);
1477
+ # inp.files = dt.files;
1478
+ # inp.dispatchEvent(new Event('change', { bubbles: true }));
1479
+ # } else if (tries > 0) {
1480
+ # setTimeout(() => attempt(tries - 1), 300);
1481
+ # }
1482
+ # }
1483
+ # attempt(10);
1484
+ # }
1485
+
1486
+ # /* ── WAVEFORM ── */
1487
+ # function drawWave() {
1488
+ # if (!analyser) return;
1489
+ # animId = requestAnimationFrame(drawWave);
1490
+
1491
+ # const buf = analyser.frequencyBinCount;
1492
+ # const data = new Uint8Array(buf);
1493
+ # analyser.getByteTimeDomainData(data);
1494
+
1495
+ # const W = canvas.width, H = canvas.height;
1496
+ # ctx2d.clearRect(0, 0, W, H);
1497
+
1498
+ # const rms = Math.sqrt(data.reduce((s,v) => s+(v-128)**2, 0) / buf);
1499
+ # const intensity = Math.min(rms / 28, 1);
1500
+
1501
+ # /* subtle glow behind wave */
1502
+ # if (intensity > 0.05) {
1503
+ # const cx = W/2, cy = H/2;
1504
+ # const grd = ctx2d.createRadialGradient(cx, cy, 0, cx, cy, W/2);
1505
+ # grd.addColorStop(0, `rgba(79,142,247,${0.04 * intensity})`);
1506
+ # grd.addColorStop(1, 'transparent');
1507
+ # ctx2d.fillStyle = grd;
1508
+ # ctx2d.fillRect(0, 0, W, H);
1509
+ # }
1510
+
1511
+ # /* waveform line */
1512
+ # const grad = ctx2d.createLinearGradient(0, 0, W, 0);
1513
+ # grad.addColorStop(0, 'rgba(79,142,247,0)');
1514
+ # grad.addColorStop(0.15, `rgba(79,142,247,${0.4 + intensity * 0.6})`);
1515
+ # grad.addColorStop(0.5, `rgba(126,184,255,${0.6 + intensity * 0.4})`);
1516
+ # grad.addColorStop(0.85, `rgba(79,142,247,${0.4 + intensity * 0.6})`);
1517
+ # grad.addColorStop(1, 'rgba(79,142,247,0)');
1518
+
1519
+ # ctx2d.beginPath();
1520
+ # ctx2d.lineWidth = 1.5;
1521
+ # ctx2d.strokeStyle = grad;
1522
+ # ctx2d.shadowColor = '#4f8ef7';
1523
+ # ctx2d.shadowBlur = 6 * intensity;
1524
+
1525
+ # const sw = W / buf;
1526
+ # for (let i = 0; i < buf; i++) {
1527
+ # const y = ((data[i]/128)*H)/2;
1528
+ # i === 0 ? ctx2d.moveTo(0, y) : ctx2d.lineTo(i*sw, y);
1529
+ # }
1530
+ # ctx2d.stroke();
1531
+ # ctx2d.shadowBlur = 0;
1532
+ # }
1533
+
1534
+ # function drawFlatLine() {
1535
+ # ctx2d.clearRect(0, 0, canvas.width, canvas.height);
1536
+ # ctx2d.beginPath();
1537
+ # ctx2d.moveTo(0, canvas.height/2);
1538
+ # ctx2d.lineTo(canvas.width, canvas.height/2);
1539
+ # ctx2d.strokeStyle = 'rgba(29,37,53,0.8)';
1540
+ # ctx2d.lineWidth = 1;
1541
+ # ctx2d.stroke();
1542
+ # }
1543
+
1544
+ # drawFlatLine();
1545
+ # })();
1546
+ # </script>
1547
+ # """
1548
+
1549
+
1550
+ # # ============================================================
1551
+ # # GRADIO BLOCKS
1552
+ # # ============================================================
1553
+ # with gr.Blocks(theme=gr.themes.Base(), css=CSS, title="Lumi Voice Collector") as demo:
1554
+
1555
+ # gr.HTML("""
1556
+ # <div class="lumi-header">
1557
+ # <div class="lumi-icon-wrap">🎙</div>
1558
+ # <div class="lumi-header-text">
1559
+ # <h1>Lumi Wake Word Collector</h1>
1560
+ # <p>Thu thập giọng nói để huấn luyện trợ lý Lumi</p>
1561
+ # </div>
1562
+ # </div>
1563
+ # """)
1564
+
1565
+ # gr.HTML("""
1566
+ # <div class="lumi-progress">
1567
+ # <div class="lumi-prog-step">
1568
+ # <div class="lumi-prog-dot">01</div>
1569
+ # <div class="lumi-prog-info">
1570
+ # <div class="lumi-prog-title">GHI ÂM</div>
1571
+ # </div>
1572
+ # </div>
1573
+ # <div class="lumi-prog-line"></div>
1574
+ # <div class="lumi-prog-step">
1575
+ # <div class="lumi-prog-dot">02</div>
1576
+ # <div class="lumi-prog-info">
1577
+ # <div class="lumi-prog-title">KIỂM TRA</div>
1578
+ # </div>
1579
+ # </div>
1580
+ # <div class="lumi-prog-line"></div>
1581
+ # <div class="lumi-prog-step">
1582
+ # <div class="lumi-prog-dot">03</div>
1583
+ # <div class="lumi-prog-info">
1584
+ # <div class="lumi-prog-title">GỬI LÊN</div>
1585
+ # </div>
1586
+ # </div>
1587
+ # </div>
1588
+ # """)
1589
+
1590
+ # gr.HTML("""
1591
+ # <div class="lumi-guide">
1592
+ # <div class="lumi-guide-icon">📋</div>
1593
+ # <div class="lumi-guide-body">
1594
+ # <div class="lumi-guide-title">Hướng dẫn</div>
1595
+ # <div class="lumi-guide-steps">
1596
+ # <div class="lumi-guide-step">
1597
+ # <span class="lumi-guide-num">01</span>
1598
+ # Nhấn nút mic để bắt đầu ghi âm
1599
+ # </div>
1600
+ # <div class="lumi-guide-step">
1601
+ # <span class="lumi-guide-num">02</span>
1602
+ # Nói rõ <span class="lumi-keyword">Lumi ơi</span> vào micro
1603
+ # </div>
1604
+ # <div class="lumi-guide-step">
1605
+ # <span class="lumi-guide-num">03</span>
1606
+ # Nhấn lại để dừng — đợi xử lý xong
1607
+ # </div>
1608
+ # <div class="lumi-guide-step">
1609
+ # <span class="lumi-guide-num">04</span>
1610
+ # Kiểm tra → đạt ≥ 0.8 → Gửi lên
1611
+ # </div>
1612
+ # </div>
1613
+ # </div>
1614
+ # </div>
1615
+ # """)
1616
+
1617
+ # with gr.Group(elem_classes="lumi-recorder-card"):
1618
+ # gr.HTML(RECORDER_HTML)
1619
+
1620
+ # with gr.Group(elem_classes="hidden-audio-wrap"):
1621
+ # audio_inst = gr.Audio(sources=["upload"], type="filepath", label="hidden", visible=False)
1622
+
1623
+ # status_txt = gr.Label(
1624
+ # value='Sẵn sàng — Nhấn mic và đọc "Lumi ơi"',
1625
+ # elem_classes="lumi-status",
1626
+ # show_label=False
1627
+ # )
1628
+
1629
+ # with gr.Row(elem_classes="lumi-btn-row"):
1630
+ # btn_check = gr.Button("Kiểm tra mẫu", variant="secondary", elem_classes="lb-check")
1631
+ # btn_reset = gr.Button("Ghi lại", variant="stop", elem_classes="lb-reset")
1632
+
1633
+ # btn_send = gr.Button(
1634
+ # "Gửi lên hệ thống →",
1635
+ # variant="primary",
1636
+ # interactive=False,
1637
+ # elem_classes="lb-send"
1638
+ # )
1639
+
1640
+ # score_state = gr.State(0.0)
1641
+
1642
+ # btn_check.click(fn=verify_audio, inputs=audio_inst, outputs=[status_txt, btn_send, score_state])
1643
+ # btn_send.click( fn=upload_final, inputs=[audio_inst, score_state], outputs=[status_txt, btn_send, audio_inst])
1644
+ # btn_reset.click(fn=reset_ui, outputs=[audio_inst, status_txt, btn_send, score_state])
1645
+ # audio_inst.change(lambda: gr.update(interactive=False), None, btn_send)
1646
+
1647
+ # demo.launch()