phongps2 commited on
Commit
717bc94
·
verified ·
1 Parent(s): 01b9db9

Update app.py

Browse files

Add buffer 10 min

Files changed (1) hide show
  1. app.py +304 -74
app.py CHANGED
@@ -1,21 +1,57 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
  import gradio as gr
2
- import os, uuid, librosa, threading, time, tempfile, wave, io, csv
3
- import unicodedata, re
4
  import numpy as np
5
- from huggingface_hub import HfApi, CommitOperationAdd
6
  from openwakeword.model import Model
7
 
8
- # --- CẤU HÌNH ---
 
 
9
  DATASET_ID = "lumiwakeword/lumioiv1"
10
  HF_TOKEN = os.getenv("HF_TOKEN")
11
  MODEL_PATH = "loo_mee_oy_v2.onnx"
12
 
13
- api = HfApi()
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
14
 
15
  # --- KHỞI TẠO MODEL ---
16
  oww_model = None
17
  model_ready = threading.Event()
18
 
 
 
 
 
19
  def load_model():
20
  global oww_model
21
  try:
@@ -24,7 +60,9 @@ def load_model():
24
  with tempfile.NamedTemporaryFile(suffix=".wav", delete=False) as f:
25
  tmp_path = f.name
26
  with wave.open(tmp_path, "w") as wf:
27
- wf.setnchannels(1); wf.setsampwidth(2); wf.setframerate(16000)
 
 
28
  wf.writeframes((dummy_wav * 32767).astype(np.int16).tobytes())
29
  librosa.load(tmp_path, sr=16000)
30
  os.unlink(tmp_path)
@@ -39,7 +77,8 @@ def load_model():
39
  t_iter = time.perf_counter()
40
  m.predict(np.zeros(1280, dtype=np.int16))
41
  elapsed_ms = (time.perf_counter() - t_iter) * 1000
42
- if i < 3: continue
 
43
  if elapsed_ms < 5.0:
44
  stable_count += 1
45
  if stable_count >= 5:
@@ -55,10 +94,240 @@ def load_model():
55
  finally:
56
  model_ready.set()
57
 
 
58
  threading.Thread(target=load_model, daemon=True).start()
59
 
60
 
61
- # --- PYTHON LOGIC ---
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
62
  def verify_audio(audio_path):
63
  if not model_ready.wait(timeout=30):
64
  return "⏳ Model đang khởi động, thử lại sau vài giây...", gr.update(interactive=False), 0.0
@@ -95,51 +364,20 @@ def upload_final(audio_path, score, speaker_name):
95
  if not audio_path:
96
  return "❌ Không có file để gửi.", gr.update(interactive=False), None
97
  try:
98
- spk = slugify(speaker_name) if speaker_name else 'unknown'
99
- filename = f"lumi_{spk}_{score:.2f}_{uuid.uuid4().hex[:4]}.wav"
100
- audio_repo_path = f"audios/{filename}"
101
-
102
- try:
103
- existing = api.hf_hub_download(
104
- repo_id=DATASET_ID,
105
- filename="metadata.csv",
106
- repo_type="dataset",
107
- token=HF_TOKEN
108
- )
109
- with open(existing, "r", encoding="utf-8") as f:
110
- rows = list(csv.reader(f))
111
- except Exception:
112
- rows = [["file_name"]]
113
 
114
- rows.append([audio_repo_path])
 
 
115
 
116
- buf = io.StringIO()
117
- csv.writer(buf).writerows(rows)
118
- csv_bytes = buf.getvalue().encode("utf-8")
119
-
120
- operations = [
121
- CommitOperationAdd(
122
- path_in_repo=audio_repo_path,
123
- path_or_fileobj=audio_path,
124
- ),
125
- CommitOperationAdd(
126
- path_in_repo="metadata.csv",
127
- path_or_fileobj=io.BytesIO(csv_bytes),
128
- ),
129
- ]
130
-
131
- api.create_commit(
132
- repo_id=DATASET_ID,
133
- repo_type="dataset",
134
- operations=operations,
135
- commit_message=f"add {filename}",
136
- token=HF_TOKEN,
137
  )
138
-
139
- return "🎉 Gửi thành công! Bạn có thể thu mẫu tiếp theo.", gr.update(interactive=False), None
140
-
141
  except Exception as e:
142
- return f"❌ Lỗi gửi: {e}", gr.update(interactive=True), audio_path
143
 
144
 
145
  def slugify(text):
@@ -155,7 +393,7 @@ def reset_ui():
155
  None,
156
  'Sẵn sàng — Nhấn mic và đọc "Lumi ơi"',
157
  gr.update(interactive=False),
158
- 0.0
159
  )
160
 
161
 
@@ -198,7 +436,6 @@ body, .gradio-container {
198
  margin: 0 auto !important;
199
  padding: clamp(16px,4vw,40px) clamp(12px,4vw,36px) 60px !important;
200
  }
201
- /* ── HEADER ── */
202
  .lumi-header {
203
  display: flex; align-items: center; gap: var(--sp-md);
204
  background: linear-gradient(135deg, #eef3ff 0%, #f4f6fb 100%);
@@ -226,7 +463,6 @@ body, .gradio-container {
226
  .lumi-header-text p {
227
  font-size: var(--fs-sm); color: var(--text3); margin-top: 2px;
228
  }
229
- /* ── PROGRESS ── */
230
  .lumi-progress {
231
  display: flex; align-items: center;
232
  margin-bottom: var(--sp-md);
@@ -245,7 +481,6 @@ body, .gradio-container {
245
  height: 1px; background: var(--border);
246
  width: clamp(14px,3vw,28px); flex-shrink: 0; margin: 0 var(--sp-xs);
247
  }
248
- /* ── GUIDE ── */
249
  .lumi-guide {
250
  background: var(--surface); border: 1px solid var(--border);
251
  border-radius: clamp(10px,2vw,14px);
@@ -278,7 +513,6 @@ body, .gradio-container {
278
  color: var(--accent2); font-weight: 600;
279
  background: rgba(59,126,244,0.08); border-radius: 4px; padding: 0 4px;
280
  }
281
- /* ── TIPS ── */
282
  .lumi-tips {
283
  margin-top: var(--sp-sm); padding-top: var(--sp-sm);
284
  border-top: 1px solid var(--border);
@@ -293,7 +527,6 @@ body, .gradio-container {
293
  }
294
  .lumi-tip b { color: var(--text); font-weight: 600; }
295
  .tip-icon { font-size: var(--fs-base); flex-shrink: 0; }
296
- /* ── RECORDER CARD ── */
297
  .lumi-recorder-card {
298
  background: linear-gradient(145deg, #1e2d5a 0%, #162447 55%, #1a3060 100%);
299
  border: 1px solid rgba(99,140,255,0.25);
@@ -322,7 +555,6 @@ body, .gradio-container {
322
  align-items: center; gap: var(--sp-md);
323
  position: relative; z-index: 1;
324
  }
325
- /* Mic button */
326
  #lumi-mic-btn {
327
  width: clamp(60px,13vw,88px); height: clamp(60px,13vw,88px);
328
  border-radius: 50%;
@@ -350,19 +582,16 @@ body, .gradio-container {
350
  0%,100% { box-shadow: 0 0 0 0 rgba(248,113,113,0.4); }
351
  50% { box-shadow: 0 0 0 14px rgba(248,113,113,0); }
352
  }
353
- /* Rec label */
354
  #lumi-rec-label {
355
  font-size: var(--fs-sm); color: rgba(255,255,255,0.45);
356
  font-weight: 500; letter-spacing: 0.3px; text-align: center;
357
  }
358
  #lumi-rec-label.recording { color: #fca5a5; }
359
- /* Canvas */
360
  #lumi-canvas {
361
  width: 100%; height: clamp(44px,7vw,68px);
362
  border-radius: 8px; background: rgba(0,0,0,0.22);
363
  border: 1px solid rgba(255,255,255,0.07); display: block;
364
  }
365
- /* Processing bar */
366
  #lumi-proc-bar { display: none; width: 100%; }
367
  #lumi-proc-bar .p-label {
368
  font-size: var(--fs-xs); font-family: var(--mono);
@@ -387,7 +616,6 @@ body, .gradio-container {
387
  50% {width:50%; margin-left:25%;}
388
  100%{width:0%; margin-left:100%;}
389
  }
390
- /* Playback */
391
  #lumi-audio-playback { width: 100%; display: none; flex-direction: column; gap: 5px; }
392
  #lumi-audio-playback .play-label {
393
  font-size: var(--fs-xs); font-family: var(--mono); color: rgba(255,255,255,0.35);
@@ -395,7 +623,6 @@ body, .gradio-container {
395
  #lumi-audio-playback audio {
396
  width: 100%; height: 30px; border-radius: 6px; outline: none; accent-color: #638cff;
397
  }
398
- /* ── STATUS ── */
399
  .lumi-status {
400
  background: var(--surface) !important; border: 1px solid var(--border) !important;
401
  border-radius: 10px !important;
@@ -408,7 +635,6 @@ body, .gradio-container {
408
  font-size: var(--fs-base) !important; color: var(--text2) !important;
409
  font-family: var(--mono) !important;
410
  }
411
- /* ── BUTTONS ── */
412
  .lumi-btn-row {
413
  display: grid; grid-template-columns: 1fr 1fr;
414
  gap: var(--sp-xs); margin-bottom: var(--sp-xs);
@@ -453,7 +679,6 @@ button.lb-send:disabled {
453
  position: absolute; width: 1px; height: 1px;
454
  overflow: hidden; opacity: 0; pointer-events: none;
455
  }
456
- /* ── SPEAKER INPUT ── */
457
  .lumi-speaker-input { margin-bottom: var(--sp-xs) !important; }
458
  .lumi-speaker-input input {
459
  background: var(--surface) !important;
@@ -578,7 +803,6 @@ RECORDER_HTML = """
578
  }
579
  attempt(10);
580
  }
581
- /* ── WAVEFORM ── */
582
  function drawWave() {
583
  if (!analyser) return;
584
  animId = requestAnimationFrame(drawWave);
@@ -671,7 +895,6 @@ with gr.Blocks(theme=gr.themes.Base(), css=CSS, title="Lumi Voice Collector") as
671
  </div>
672
  """)
673
 
674
- # Card 1 — Hướng dẫn các bước
675
  gr.HTML("""
676
  <div class="lumi-guide">
677
  <div class="lumi-guide-icon">📋</div>
@@ -703,7 +926,6 @@ with gr.Blocks(theme=gr.themes.Base(), css=CSS, title="Lumi Voice Collector") as
703
  </div>
704
  """)
705
 
706
- # Card 2 — Khoảng cách & Góc thu âm
707
  gr.HTML("""
708
  <div class="lumi-guide">
709
  <div class="lumi-guide-icon">📐</div>
@@ -745,31 +967,39 @@ with gr.Blocks(theme=gr.themes.Base(), css=CSS, title="Lumi Voice Collector") as
745
  placeholder="Nhập tên của bạn...",
746
  label="Tên người thu âm",
747
  max_lines=1,
748
- elem_classes="lumi-speaker-input"
749
  )
750
 
751
  status_txt = gr.Label(
752
  value='Sẵn sàng — Nhấn mic và đọc "Lumi ơi"',
753
  elem_classes="lumi-status",
754
- show_label=False
755
  )
756
 
757
  with gr.Row(elem_classes="lumi-btn-row"):
758
  btn_check = gr.Button("Kiểm tra mẫu", variant="secondary", elem_classes="lb-check")
759
- btn_reset = gr.Button("Ghi lại", variant="stop", elem_classes="lb-reset")
760
 
761
  btn_send = gr.Button(
762
  "Gửi lên hệ thống →",
763
  variant="primary",
764
  interactive=False,
765
- elem_classes="lb-send"
766
  )
767
 
768
  score_state = gr.State(0.0)
769
 
770
- btn_check.click(fn=verify_audio, inputs=audio_inst, outputs=[status_txt, btn_send, score_state])
771
- btn_send.click( fn=upload_final, inputs=[audio_inst, score_state, speaker_input], outputs=[status_txt, btn_send, audio_inst])
772
- btn_reset.click(fn=reset_ui, outputs=[audio_inst, status_txt, btn_send, score_state])
773
  audio_inst.change(lambda: gr.update(interactive=False), None, btn_send)
774
 
775
- demo.launch()
 
 
 
 
 
 
 
 
 
1
+ bash -lc cat > /mnt/data/app.py <<'PY'
2
+ import atexit
3
+ import csv
4
+ import io
5
+ import os
6
+ import re
7
+ import shutil
8
+ import tempfile
9
+ import threading
10
+ import time
11
+ import unicodedata
12
+ import uuid
13
+ import wave
14
+ from datetime import datetime
15
+
16
  import gradio as gr
17
+ import librosa
 
18
  import numpy as np
19
+ from huggingface_hub import CommitOperationAdd, HfApi, hf_hub_download
20
  from openwakeword.model import Model
21
 
22
+ # ============================================================
23
+ # CẤU HÌNH
24
+ # ============================================================
25
  DATASET_ID = "lumiwakeword/lumioiv1"
26
  HF_TOKEN = os.getenv("HF_TOKEN")
27
  MODEL_PATH = "loo_mee_oy_v2.onnx"
28
 
29
+ # Buffer / batch upload
30
+ BUFFER_DIR = "buffer"
31
+ AUDIO_BUFFER_DIR = os.path.join(BUFFER_DIR, "audios")
32
+ PENDING_QUEUE_FILE = os.path.join(BUFFER_DIR, "pending_queue.csv")
33
+ FLUSH_INTERVAL_SEC = 600 # 10 phút
34
+ MAX_PENDING_BEFORE_FLUSH = 100 # flush sớm nếu đủ nhiều mẫu
35
+
36
+ # Metadata trên Hub: giữ nguyên schema cũ để tương thích pipeline hiện tại
37
+ HUB_METADATA_HEADER = ["file_name"]
38
+
39
+ api = HfApi(token=HF_TOKEN)
40
+
41
+ os.makedirs(AUDIO_BUFFER_DIR, exist_ok=True)
42
+
43
+ buffer_lock = threading.Lock()
44
+ flush_in_progress = threading.Event()
45
+ shutdown_event = threading.Event()
46
 
47
  # --- KHỞI TẠO MODEL ---
48
  oww_model = None
49
  model_ready = threading.Event()
50
 
51
+
52
+ # ============================================================
53
+ # MODEL LOADER
54
+ # ============================================================
55
  def load_model():
56
  global oww_model
57
  try:
 
60
  with tempfile.NamedTemporaryFile(suffix=".wav", delete=False) as f:
61
  tmp_path = f.name
62
  with wave.open(tmp_path, "w") as wf:
63
+ wf.setnchannels(1)
64
+ wf.setsampwidth(2)
65
+ wf.setframerate(16000)
66
  wf.writeframes((dummy_wav * 32767).astype(np.int16).tobytes())
67
  librosa.load(tmp_path, sr=16000)
68
  os.unlink(tmp_path)
 
77
  t_iter = time.perf_counter()
78
  m.predict(np.zeros(1280, dtype=np.int16))
79
  elapsed_ms = (time.perf_counter() - t_iter) * 1000
80
+ if i < 3:
81
+ continue
82
  if elapsed_ms < 5.0:
83
  stable_count += 1
84
  if stable_count >= 5:
 
94
  finally:
95
  model_ready.set()
96
 
97
+
98
  threading.Thread(target=load_model, daemon=True).start()
99
 
100
 
101
+ # ============================================================
102
+ # BUFFER / QUEUE HELPERS
103
+ # ============================================================
104
+ def ensure_pending_queue():
105
+ if not os.path.exists(PENDING_QUEUE_FILE):
106
+ with open(PENDING_QUEUE_FILE, "w", newline="", encoding="utf-8") as f:
107
+ writer = csv.writer(f)
108
+ writer.writerow(["local_path", "repo_path", "speaker", "score", "created_at"])
109
+
110
+
111
+ def read_pending_queue():
112
+ ensure_pending_queue()
113
+ with open(PENDING_QUEUE_FILE, "r", encoding="utf-8") as f:
114
+ rows = list(csv.reader(f))
115
+ if not rows:
116
+ return ["local_path", "repo_path", "speaker", "score", "created_at"], []
117
+ return rows[0], rows[1:]
118
+
119
+
120
+ def write_pending_queue(header, rows):
121
+ with open(PENDING_QUEUE_FILE, "w", newline="", encoding="utf-8") as f:
122
+ writer = csv.writer(f)
123
+ writer.writerow(header)
124
+ writer.writerows(rows)
125
+
126
+
127
+ def get_pending_count():
128
+ with buffer_lock:
129
+ _, rows = read_pending_queue()
130
+ return len(rows)
131
+
132
+
133
+ def fetch_existing_metadata_rows():
134
+ try:
135
+ existing = hf_hub_download(
136
+ repo_id=DATASET_ID,
137
+ filename="metadata.csv",
138
+ repo_type="dataset",
139
+ token=HF_TOKEN,
140
+ )
141
+ with open(existing, "r", encoding="utf-8") as f:
142
+ rows = list(csv.reader(f))
143
+ if not rows:
144
+ return [HUB_METADATA_HEADER]
145
+ # Nếu file cũ chỉ có 1 cột hoặc header khác, vẫn cố chuẩn hóa tối thiểu.
146
+ header = rows[0] if rows[0] else HUB_METADATA_HEADER
147
+ if len(header) == 1:
148
+ normalized = [HUB_METADATA_HEADER]
149
+ for row in rows[1:]:
150
+ if row:
151
+ normalized.append([row[0]])
152
+ return normalized
153
+ return rows
154
+ except Exception:
155
+ return [HUB_METADATA_HEADER]
156
+
157
+
158
+ def enqueue_sample_local(audio_path, score, speaker_name):
159
+ ensure_pending_queue()
160
+
161
+ spk = slugify(speaker_name) if speaker_name else "unknown"
162
+ created_at = datetime.utcnow().replace(microsecond=0).isoformat() + "Z"
163
+ ext = ".wav"
164
+ filename = f"lumi_{spk}_{uuid.uuid4().hex[:8]}{ext}"
165
+ local_buffer_path = os.path.join(AUDIO_BUFFER_DIR, filename)
166
+ repo_audio_path = f"audios/{filename}"
167
+
168
+ # Chuẩn hóa thành WAV 16k mono để dataset đồng nhất.
169
+ y, _ = librosa.load(audio_path, sr=16000, mono=True)
170
+ y = np.clip(y, -1.0, 1.0)
171
+ audio_int16 = (y * 32767).astype(np.int16)
172
+
173
+ with wave.open(local_buffer_path, "wb") as wf:
174
+ wf.setnchannels(1)
175
+ wf.setsampwidth(2)
176
+ wf.setframerate(16000)
177
+ wf.writeframes(audio_int16.tobytes())
178
+
179
+ with buffer_lock:
180
+ header, rows = read_pending_queue()
181
+ rows.append([local_buffer_path, repo_audio_path, spk, f"{score:.4f}", created_at])
182
+ write_pending_queue(header, rows)
183
+
184
+ return filename, local_buffer_path, repo_audio_path
185
+
186
+
187
+ def flush_buffer_to_hf():
188
+ if flush_in_progress.is_set():
189
+ return False, "flush đang chạy"
190
+
191
+ if not HF_TOKEN:
192
+ return False, "HF_TOKEN chưa được cấu hình"
193
+
194
+ flush_in_progress.set()
195
+ try:
196
+ with buffer_lock:
197
+ header, pending_rows = read_pending_queue()
198
+ if not pending_rows:
199
+ return True, "không có mẫu chờ"
200
+ snapshot_rows = list(pending_rows)
201
+
202
+ valid_rows = []
203
+ missing_rows = []
204
+ for row in snapshot_rows:
205
+ if not row or len(row) < 2:
206
+ continue
207
+ local_path = row[0]
208
+ if os.path.exists(local_path):
209
+ valid_rows.append(row)
210
+ else:
211
+ missing_rows.append(row)
212
+
213
+ if not valid_rows and missing_rows:
214
+ with buffer_lock:
215
+ cur_header, cur_rows = read_pending_queue()
216
+ missing_keys = {(r[0], r[1]) for r in missing_rows if len(r) >= 2}
217
+ remaining_rows = [
218
+ r for r in cur_rows
219
+ if len(r) >= 2 and (r[0], r[1]) not in missing_keys
220
+ ]
221
+ write_pending_queue(cur_header, remaining_rows)
222
+ return False, "có bản ghi pending bị mất file local"
223
+
224
+ if not valid_rows:
225
+ return True, "không có file hợp lệ để flush"
226
+
227
+ existing_rows = fetch_existing_metadata_rows()
228
+ existing_data = existing_rows[1:] if len(existing_rows) > 1 else []
229
+ existing_set = {row[0] for row in existing_data if row}
230
+
231
+ operations = []
232
+ new_metadata_rows = []
233
+ for row in valid_rows:
234
+ local_path, repo_path = row[0], row[1]
235
+ operations.append(
236
+ CommitOperationAdd(
237
+ path_in_repo=repo_path,
238
+ path_or_fileobj=local_path,
239
+ )
240
+ )
241
+ if repo_path not in existing_set:
242
+ new_metadata_rows.append([repo_path])
243
+
244
+ merged_rows = [HUB_METADATA_HEADER] + existing_data + new_metadata_rows
245
+ buf = io.StringIO()
246
+ csv.writer(buf).writerows(merged_rows)
247
+ csv_bytes = buf.getvalue().encode("utf-8")
248
+
249
+ operations.append(
250
+ CommitOperationAdd(
251
+ path_in_repo="metadata.csv",
252
+ path_or_fileobj=io.BytesIO(csv_bytes),
253
+ )
254
+ )
255
+
256
+ commit_msg = (
257
+ f"batch upload {len(valid_rows)} samples @ "
258
+ f"{datetime.utcnow().replace(microsecond=0).isoformat()}Z"
259
+ )
260
+
261
+ api.create_commit(
262
+ repo_id=DATASET_ID,
263
+ repo_type="dataset",
264
+ operations=operations,
265
+ commit_message=commit_msg,
266
+ token=HF_TOKEN,
267
+ )
268
+
269
+ # Chỉ xóa queue/file local sau khi commit thành công.
270
+ with buffer_lock:
271
+ cur_header, cur_rows = read_pending_queue()
272
+ flushed_keys = {(r[0], r[1]) for r in valid_rows if len(r) >= 2}
273
+ missing_keys = {(r[0], r[1]) for r in missing_rows if len(r) >= 2}
274
+
275
+ remaining_rows = [
276
+ r for r in cur_rows
277
+ if len(r) >= 2 and (r[0], r[1]) not in flushed_keys and (r[0], r[1]) not in missing_keys
278
+ ]
279
+ write_pending_queue(cur_header, remaining_rows)
280
+
281
+ for row in valid_rows:
282
+ local_path = row[0]
283
+ try:
284
+ if os.path.exists(local_path):
285
+ os.remove(local_path)
286
+ except Exception as rm_err:
287
+ print(f"⚠️ Không xóa được file buffer {local_path}: {rm_err}")
288
+
289
+ print(f"✅ Flush thành công {len(valid_rows)} mẫu lên Hugging Face")
290
+ return True, f"đã flush {len(valid_rows)} mẫu"
291
+
292
+ except Exception as e:
293
+ print(f"❌ Flush lỗi: {e}")
294
+ return False, str(e)
295
+ finally:
296
+ flush_in_progress.clear()
297
+
298
+
299
+ def flush_worker():
300
+ while not shutdown_event.is_set():
301
+ try:
302
+ shutdown_event.wait(FLUSH_INTERVAL_SEC)
303
+ if shutdown_event.is_set():
304
+ break
305
+ ok, msg = flush_buffer_to_hf()
306
+ print(f"[flush_worker] ok={ok} msg={msg}")
307
+ except Exception as e:
308
+ print(f"❌ Flush worker error: {e}")
309
+
310
+
311
+ threading.Thread(target=flush_worker, daemon=True).start()
312
+
313
+
314
+ def shutdown_cleanup():
315
+ shutdown_event.set()
316
+ try:
317
+ pending = get_pending_count()
318
+ if pending > 0:
319
+ ok, msg = flush_buffer_to_hf()
320
+ print(f"[shutdown_flush] ok={ok} msg={msg}")
321
+ except Exception as e:
322
+ print(f"⚠️ shutdown cleanup error: {e}")
323
+
324
+
325
+ atexit.register(shutdown_cleanup)
326
+
327
+
328
+ # ============================================================
329
+ # PYTHON LOGIC
330
+ # ============================================================
331
  def verify_audio(audio_path):
332
  if not model_ready.wait(timeout=30):
333
  return "⏳ Model đang khởi động, thử lại sau vài giây...", gr.update(interactive=False), 0.0
 
364
  if not audio_path:
365
  return "❌ Không có file để gửi.", gr.update(interactive=False), None
366
  try:
367
+ _, _, _ = enqueue_sample_local(audio_path, score, speaker_name)
368
+ pending_count = get_pending_count()
 
 
 
 
 
 
 
 
 
 
 
 
 
369
 
370
+ # Flush sớm nếu hàng đợi đạt ngưỡng.
371
+ if pending_count >= MAX_PENDING_BEFORE_FLUSH and not flush_in_progress.is_set():
372
+ threading.Thread(target=flush_buffer_to_hf, daemon=True).start()
373
 
374
+ return (
375
+ f"🎉 Đã nhận mẫu! Đang chờ đồng bộ. Số mẫu trong hàng đợi: {pending_count}",
376
+ gr.update(interactive=False),
377
+ None,
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
378
  )
 
 
 
379
  except Exception as e:
380
+ return f"❌ Lỗi lưu buffer: {e}", gr.update(interactive=True), audio_path
381
 
382
 
383
  def slugify(text):
 
393
  None,
394
  'Sẵn sàng — Nhấn mic và đọc "Lumi ơi"',
395
  gr.update(interactive=False),
396
+ 0.0,
397
  )
398
 
399
 
 
436
  margin: 0 auto !important;
437
  padding: clamp(16px,4vw,40px) clamp(12px,4vw,36px) 60px !important;
438
  }
 
439
  .lumi-header {
440
  display: flex; align-items: center; gap: var(--sp-md);
441
  background: linear-gradient(135deg, #eef3ff 0%, #f4f6fb 100%);
 
463
  .lumi-header-text p {
464
  font-size: var(--fs-sm); color: var(--text3); margin-top: 2px;
465
  }
 
466
  .lumi-progress {
467
  display: flex; align-items: center;
468
  margin-bottom: var(--sp-md);
 
481
  height: 1px; background: var(--border);
482
  width: clamp(14px,3vw,28px); flex-shrink: 0; margin: 0 var(--sp-xs);
483
  }
 
484
  .lumi-guide {
485
  background: var(--surface); border: 1px solid var(--border);
486
  border-radius: clamp(10px,2vw,14px);
 
513
  color: var(--accent2); font-weight: 600;
514
  background: rgba(59,126,244,0.08); border-radius: 4px; padding: 0 4px;
515
  }
 
516
  .lumi-tips {
517
  margin-top: var(--sp-sm); padding-top: var(--sp-sm);
518
  border-top: 1px solid var(--border);
 
527
  }
528
  .lumi-tip b { color: var(--text); font-weight: 600; }
529
  .tip-icon { font-size: var(--fs-base); flex-shrink: 0; }
 
530
  .lumi-recorder-card {
531
  background: linear-gradient(145deg, #1e2d5a 0%, #162447 55%, #1a3060 100%);
532
  border: 1px solid rgba(99,140,255,0.25);
 
555
  align-items: center; gap: var(--sp-md);
556
  position: relative; z-index: 1;
557
  }
 
558
  #lumi-mic-btn {
559
  width: clamp(60px,13vw,88px); height: clamp(60px,13vw,88px);
560
  border-radius: 50%;
 
582
  0%,100% { box-shadow: 0 0 0 0 rgba(248,113,113,0.4); }
583
  50% { box-shadow: 0 0 0 14px rgba(248,113,113,0); }
584
  }
 
585
  #lumi-rec-label {
586
  font-size: var(--fs-sm); color: rgba(255,255,255,0.45);
587
  font-weight: 500; letter-spacing: 0.3px; text-align: center;
588
  }
589
  #lumi-rec-label.recording { color: #fca5a5; }
 
590
  #lumi-canvas {
591
  width: 100%; height: clamp(44px,7vw,68px);
592
  border-radius: 8px; background: rgba(0,0,0,0.22);
593
  border: 1px solid rgba(255,255,255,0.07); display: block;
594
  }
 
595
  #lumi-proc-bar { display: none; width: 100%; }
596
  #lumi-proc-bar .p-label {
597
  font-size: var(--fs-xs); font-family: var(--mono);
 
616
  50% {width:50%; margin-left:25%;}
617
  100%{width:0%; margin-left:100%;}
618
  }
 
619
  #lumi-audio-playback { width: 100%; display: none; flex-direction: column; gap: 5px; }
620
  #lumi-audio-playback .play-label {
621
  font-size: var(--fs-xs); font-family: var(--mono); color: rgba(255,255,255,0.35);
 
623
  #lumi-audio-playback audio {
624
  width: 100%; height: 30px; border-radius: 6px; outline: none; accent-color: #638cff;
625
  }
 
626
  .lumi-status {
627
  background: var(--surface) !important; border: 1px solid var(--border) !important;
628
  border-radius: 10px !important;
 
635
  font-size: var(--fs-base) !important; color: var(--text2) !important;
636
  font-family: var(--mono) !important;
637
  }
 
638
  .lumi-btn-row {
639
  display: grid; grid-template-columns: 1fr 1fr;
640
  gap: var(--sp-xs); margin-bottom: var(--sp-xs);
 
679
  position: absolute; width: 1px; height: 1px;
680
  overflow: hidden; opacity: 0; pointer-events: none;
681
  }
 
682
  .lumi-speaker-input { margin-bottom: var(--sp-xs) !important; }
683
  .lumi-speaker-input input {
684
  background: var(--surface) !important;
 
803
  }
804
  attempt(10);
805
  }
 
806
  function drawWave() {
807
  if (!analyser) return;
808
  animId = requestAnimationFrame(drawWave);
 
895
  </div>
896
  """)
897
 
 
898
  gr.HTML("""
899
  <div class="lumi-guide">
900
  <div class="lumi-guide-icon">📋</div>
 
926
  </div>
927
  """)
928
 
 
929
  gr.HTML("""
930
  <div class="lumi-guide">
931
  <div class="lumi-guide-icon">📐</div>
 
967
  placeholder="Nhập tên của bạn...",
968
  label="Tên người thu âm",
969
  max_lines=1,
970
+ elem_classes="lumi-speaker-input",
971
  )
972
 
973
  status_txt = gr.Label(
974
  value='Sẵn sàng — Nhấn mic và đọc "Lumi ơi"',
975
  elem_classes="lumi-status",
976
+ show_label=False,
977
  )
978
 
979
  with gr.Row(elem_classes="lumi-btn-row"):
980
  btn_check = gr.Button("Kiểm tra mẫu", variant="secondary", elem_classes="lb-check")
981
+ btn_reset = gr.Button("Ghi lại", variant="stop", elem_classes="lb-reset")
982
 
983
  btn_send = gr.Button(
984
  "Gửi lên hệ thống →",
985
  variant="primary",
986
  interactive=False,
987
+ elem_classes="lb-send",
988
  )
989
 
990
  score_state = gr.State(0.0)
991
 
992
+ btn_check.click(fn=verify_audio, inputs=audio_inst, outputs=[status_txt, btn_send, score_state])
993
+ btn_send.click(fn=upload_final, inputs=[audio_inst, score_state, speaker_input], outputs=[status_txt, btn_send, audio_inst])
994
+ btn_reset.click(fn=reset_ui, outputs=[audio_inst, status_txt, btn_send, score_state])
995
  audio_inst.change(lambda: gr.update(interactive=False), None, btn_send)
996
 
997
+
998
+ if __name__ == "__main__":
999
+ print("🚀 Starting Lumi Wake Word Collector")
1000
+ print(f" DATASET_ID={DATASET_ID}")
1001
+ print(f" FLUSH_INTERVAL_SEC={FLUSH_INTERVAL_SEC}")
1002
+ print(f" MAX_PENDING_BEFORE_FLUSH={MAX_PENDING_BEFORE_FLUSH}")
1003
+ print(f" HF_TOKEN={'OK' if HF_TOKEN else 'MISSING'}")
1004
+ demo.launch()
1005
+ PY