almfz commited on
Commit
9fcaeae
·
verified ·
1 Parent(s): d4857a5

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +275 -265
app.py CHANGED
@@ -1,265 +1,275 @@
1
- import streamlit as st
2
- import torch
3
- import os
4
- import sys
5
- import librosa
6
- import numpy as np
7
- import pandas as pd
8
- import joblib # Atau pickle, sesuaikan dengan cara Anda menyimpan model
9
- from speechbrain.inference.speaker import SpeakerRecognition
10
-
11
- # --- KONFIGURASI APLIKASI ---
12
- st.set_page_config(page_title="Verifikasi Suara", layout="centered")
13
- st.title("🔐 Sistem Verifikasi Perintah Suara")
14
- st.write("Aplikasi ini hanya akan merespon perintah 'Buka' atau 'Tutup' jika diucapkan oleh pengguna yang terdaftar.")
15
-
16
- # --- PATH & PENGATURAN MODEL ---
17
- # Sesuaikan path ini
18
- APP_DIR = os.path.dirname(os.path.abspath(__file__))
19
-
20
- # Buat semua path lain berdasarkan APP_DIR
21
- PATH_MODEL_KWS = os.path.join(APP_DIR, "model_kws.pkl")
22
- PATH_LABEL_ENCODER = os.path.join(APP_DIR, "label_encoder.pkl")
23
- PATH_ANDA = os.path.join(APP_DIR, "enroll", "v_ilham")
24
- PATH_TEMAN = os.path.join(APP_DIR, "enroll", "v_danendra")
25
-
26
- # Pengaturan threshold (tetap sama)
27
- THRESHOLD = 0.85 # Sesuaikan ini!
28
-
29
- # --- FUNGSI BANTUAN MODEL 2 (SpeechBrain) ---
30
- # Fungsi-fungsi ini dari skrip kita sebelumnya
31
-
32
- def get_embedding(file_path, model_sv):
33
- if not os.path.exists(file_path):
34
- st.error(f"File not found: {file_path}")
35
- return None
36
- try:
37
- embedding = model_sv.encode_file(file_path)
38
- return embedding.squeeze()
39
- except Exception as e:
40
- st.error(f"Error processing {file_path}: {e}")
41
- return None
42
-
43
- def get_similarity(emb1, emb2, model_sv):
44
- emb1_batch = emb1.unsqueeze(0)
45
- emb2_batch = emb2.unsqueeze(0)
46
- score = model_sv.similarity(emb1_batch, emb2_batch)
47
- return score.item()
48
-
49
- def create_master_voiceprint(directory_path, model_sv):
50
- embeddings = []
51
- if not os.path.isdir(directory_path):
52
- st.warning(f"Direktori pendaftaran tidak ditemukan: {directory_path}")
53
- return None
54
-
55
- for file_name in os.listdir(directory_path):
56
- if file_name.endswith(".wav"):
57
- file_path = os.path.join(directory_path, file_name)
58
- emb = get_embedding(file_path, model_sv)
59
- if emb is not None:
60
- embeddings.append(emb)
61
-
62
- if not embeddings:
63
- st.error(f"Tidak ada file .wav ditemukan di {directory_path}")
64
- return None
65
-
66
- master_voiceprint = torch.mean(torch.stack(embeddings), dim=0)
67
- return master_voiceprint
68
-
69
- # --- LOADING MODEL (DENGAN CACHE) ---
70
- # Ini agar model tidak di-load ulang setiap kali ada interaksi
71
-
72
- @st.cache_resource
73
- def load_model_sv():
74
- st.info("Memuat Model Verifikasi Suara (SpeechBrain)...")
75
- try:
76
- model = SpeakerRecognition.from_hparams(
77
- source="speechbrain/spkrec-ecapa-tdnn",
78
- savedir="pretrained_models/spkrec-ecapa-tdnn",
79
- use_auth_token=False # Tambahkan ini untuk menghindari error 401
80
- )
81
- st.success("Model Verifikasi Suara siap.")
82
- return model
83
- except Exception as e:
84
- st.exception(e)
85
- st.error("Gagal memuat model SpeechBrain. Cek koneksi internet.")
86
- return None
87
-
88
- @st.cache_resource
89
- def load_model_kws(path):
90
- st.info("Memuat Model Pengenal Kata Kunci (KWS)...")
91
- if not os.path.exists(path):
92
- st.error(f"File model KWS tidak ditemukan di: {path}")
93
- return None
94
- try:
95
- model = joblib.load(path)
96
- st.success("Model KWS siap.")
97
- return model
98
- except Exception as e:
99
- st.exception(e)
100
- return None
101
-
102
- @st.cache_resource
103
- def load_voiceprints(_model_sv):
104
- st.info("Membuat master voiceprint...")
105
- voiceprints = {}
106
-
107
- vp_a = create_master_voiceprint(PATH_ANDA, _model_sv)
108
- if vp_a is not None:
109
- voiceprints["anda"] = vp_a
110
- st.success("Voiceprint 'anda' dibuat.")
111
-
112
- vp_b = create_master_voiceprint(PATH_TEMAN, _model_sv)
113
- if vp_b is not None:
114
- voiceprints["teman"] = vp_b
115
- st.success("Voiceprint 'teman' dibuat.")
116
-
117
- if not voiceprints:
118
- st.error("Gagal membuat voiceprint. Pastikan folder 'enroll' ada.")
119
- return None
120
- return voiceprints
121
-
122
- # --- FUNGSI PIPELINE UTAMA ---
123
-
124
- def ekstrak_fitur_kws(audio_file):
125
- """
126
- PENTING: Fungsi ini HARUS mengekstrak fitur yang SAMA PERSIS
127
- dengan yang Anda gunakan untuk melatih Model 1 di Ppreprocessing.pdf.
128
- Ini hanya contoh!
129
- """
130
- y, sr = librosa.load(audio_file, sr=16000)
131
-
132
- # Preprocessing dari PDF Anda: trim & normalisasi
133
- y_trimmed, _ = librosa.effects.trim(y, top_db=20)
134
- y_norm = y_trimmed / np.max(np.abs(y_trimmed))
135
-
136
- # Ekstrak fitur (HARUS SAMA DENGAN PDF ANDA)
137
- # [cite_start]Ini hanya contoh dari PDF Anda [cite: 194-212]
138
- fitur = {
139
- 'spectral_centroid': np.mean(librosa.feature.spectral_centroid(y=y_norm, sr=sr)),
140
- 'spectral_bandwidth': np.mean(librosa.feature.spectral_bandwidth(y=y_norm, sr=sr)),
141
- 'spectral_rolloff': np.mean(librosa.feature.spectral_rolloff(y=y_norm, sr=sr)),
142
- 'spectral_contrast': np.mean(librosa.feature.spectral_contrast(y=y_norm, sr=sr)),
143
- 'spectral_flatness': np.mean(librosa.feature.spectral_flatness(y=y_norm)),
144
- 'mfcc_delta2_mean': np.mean(librosa.feature.delta(librosa.feature.mfcc(y=y_norm, sr=sr, n_mfcc=13), order=2)),
145
- 'f0_mean': np.nanmean(librosa.pyin(y_norm, fmin=50, fmax=400, sr=sr)[0]),
146
- 'rms': np.mean(librosa.feature.rms(y=y_norm)),
147
- 'duration': librosa.get_duration(y=y_norm, sr=sr),
148
- 'std_amplitude': np.std(y_norm)
149
- }
150
-
151
- # Mengisi NaN jika ada (misal dari f0)
152
- df = pd.DataFrame([fitur]).fillna(0)
153
-
154
- # Pastikan urutan kolom sama dengan saat training!
155
- # Ini hanya contoh berdasarkan 10 fitur akhir di PDF Anda
156
- nama_fitur_akhir = [
157
- 'spectral_centroid', 'spectral_bandwidth', 'spectral_rolloff',
158
- 'spectral_contrast', 'spectral_flatness', 'mfcc_delta2_mean',
159
- 'f0_mean', 'rms', 'duration', 'std_amplitude'
160
- ]
161
-
162
- # Filter dan urutkan
163
- df = df[nama_fitur_akhir]
164
- return df
165
-
166
-
167
- def cek_keyword(audio_file, model_kws):
168
- """
169
- Menjalankan Model 1 (KWS) untuk mendeteksi kata kunci.
170
- """
171
- try:
172
- fitur = ekstrak_fitur_kws(audio_file)
173
- prediksi = model_kws.predict(fitur)
174
- return prediksi[0] # Mengambil hasil prediksi (misal: "buka" atau "tutup")
175
- except Exception as e:
176
- st.exception(e)
177
- st.error("Gagal mengekstrak fitur KWS.")
178
- return "error"
179
-
180
- def verifikasi_suara(audio_file, model_sv, voiceprints, threshold):
181
- """
182
- Menjalankan Model 2 (SV) untuk verifikasi pembicara.
183
- """
184
- try:
185
- test_embedding = get_embedding(audio_file, model_sv)
186
- if test_embedding is None:
187
- return False, 0.0, "Gagal buat embedding"
188
-
189
- best_score = -1.0
190
- best_match = "None"
191
-
192
- for name, master_vp in voiceprints.items():
193
- score = get_similarity(test_embedding, master_vp, model_sv)
194
- if score > best_score:
195
- best_score = score
196
- best_match = name
197
-
198
- if best_score >= threshold:
199
- return True, best_score, best_match
200
- else:
201
- return False, best_score, "None"
202
-
203
- except Exception as e:
204
- st.exception(e)
205
- st.error("Gagal saat verifikasi suara.")
206
- return False, 0.0, "Error"
207
-
208
- # --- MAIN APP ---
209
- # Memuat semua model saat aplikasi dimulai
210
- model_sv = load_model_sv()
211
- model_kws = load_model_kws(PATH_MODEL_KWS)
212
- voiceprints = load_voiceprints(model_sv)
213
-
214
- # Cek jika model gagal di-load
215
- if not all([model_sv, model_kws, voiceprints]):
216
- st.error("Gagal memuat semua model atau voiceprint. Aplikasi tidak bisa berjalan.")
217
- else:
218
- st.header("Upload Audio Perintah (.wav)")
219
- uploaded_file = st.file_uploader("Pilih file audio...", type=["wav"])
220
-
221
- # Buat file audio sementara
222
- temp_audio_path = None
223
- if uploaded_file is not None:
224
- # Simpan file yang di-upload ke disk sementara
225
- # karena librosa & speechbrain butuh path file
226
- with open("temp_audio.wav", "wb") as f:
227
- f.write(uploaded_file.getbuffer())
228
- temp_audio_path = "temp_audio.wav"
229
-
230
- st.audio(temp_audio_path)
231
-
232
- if st.button("Proses Perintah", disabled=(temp_audio_path is None)):
233
- if temp_audio_path:
234
- with st.spinner("Menganalisis audio..."):
235
-
236
- # --- LANGKAH 1: Cek Kata Kunci ---
237
- st.subheader("Hasil Model 1: Pengenalan Kata Kunci")
238
- kata_kunci = cek_keyword(temp_audio_path, model_kws)
239
-
240
- if kata_kunci in ["buka", "tutup"]:
241
- st.info(f"Kata kunci terdeteksi: **{kata_kunci.upper()}**")
242
-
243
- # --- LANGKAH 2: Verifikasi Suara ---
244
- st.subheader("Hasil Model 2: Verifikasi Suara")
245
- terverifikasi, skor, nama = verifikasi_suara(
246
- temp_audio_path, model_sv, voiceprints, THRESHOLD
247
- )
248
-
249
- st.info(f"Skor kemiripan tertinggi: **{skor:.2%}** (dengan '{nama}')")
250
-
251
- # --- KEPUTUSAN AKHIR ---
252
- st.header("Keputusan Akhir")
253
- if terverifikasi:
254
- st.success(f"✅ DITERIMA. Suara terverifikasi sebagai '{nama}'. Perintah **{kata_kunci.upper()}** dijalankan.")
255
- else:
256
- st.error(f"❌ DITOLAK. Suara tidak dikenal. Perintah **{kata_kunci.upper()}** dibatalkan.")
257
-
258
- elif kata_kunci == "error":
259
- st.error("Terjadi error saat memproses kata kunci.")
260
- else:
261
- st.header("Keputusan Akhir")
262
- st.warning(f"❌ DITOLAK. Perintah tidak dikenal (terdeteksi sebagai: '{kata_kunci}').")
263
-
264
- # Hapus file sementara
265
- os.remove(temp_audio_path)
 
 
 
 
 
 
 
 
 
 
 
1
+ import streamlit as st
2
+ import torch
3
+ import os
4
+ import sys
5
+ import librosa
6
+ import numpy as np
7
+ import pandas as pd
8
+ import joblib # Memuat .pkl
9
+ from speechbrain.inference.speaker import SpeakerRecognition
10
+
11
+ # --- KONFIGURASI APLIKASI ---
12
+ st.set_page_config(page_title="Verifikasi Suara", layout="centered")
13
+ st.title("🔐 Sistem Verifikasi Perintah Suara")
14
+ st.write("Aplikasi ini hanya akan merespon perintah 'Buka' atau 'Tutup' jika diucapkan oleh pengguna yang terdaftar.")
15
+
16
+ # --- PATH & PENGATURAN MODEL ---
17
+ APP_DIR = os.path.dirname(os.path.abspath(__file__))
18
+ PATH_MODEL_KWS = os.path.join(APP_DIR, "model_kws.pkl")
19
+ PATH_LABEL_ENCODER = os.path.join(APP_DIR, "label_encoder.pkl") # DIPERLUKAN
20
+ PATH_ILHAM = os.path.join(APP_DIR, "enroll", "v_ilham") # Disesuaikan
21
+ PATH_DANENDRA = os.path.join(APP_DIR, "enroll", "v_danendra") # Disesuaikan
22
+
23
+ THRESHOLD = 0.85 # Sesuaikan ini!
24
+
25
+ # --- FUNGSI BANTUAN MODEL 2 (SpeechBrain) ---
26
+
27
+ def get_embedding(file_path, model_sv):
28
+ if not os.path.exists(file_path):
29
+ st.error(f"File not found: {file_path}")
30
+ return None
31
+ try:
32
+ embedding = model_sv.encode_file(file_path)
33
+ return embedding.squeeze()
34
+ except Exception as e:
35
+ st.error(f"Error processing {file_path}: {e}")
36
+ return None
37
+
38
+ def get_similarity(emb1, emb2, model_sv):
39
+ emb1_batch = emb1.unsqueeze(0)
40
+ emb2_batch = emb2.unsqueeze(0)
41
+ score = model_sv.similarity(emb1_batch, emb2_batch)
42
+ return score.item()
43
+
44
+ def create_master_voiceprint(directory_path, model_sv):
45
+ embeddings = []
46
+ if not os.path.isdir(directory_path):
47
+ st.warning(f"Direktori pendaftaran tidak ditemukan: {directory_path}")
48
+ return None
49
+
50
+ for file_name in os.listdir(directory_path):
51
+ if file_name.endswith(".wav"):
52
+ file_path = os.path.join(directory_path, file_name)
53
+ emb = get_embedding(file_path, model_sv)
54
+ if emb is not None:
55
+ embeddings.append(emb)
56
+
57
+ if not embeddings:
58
+ st.error(f"Tidak ada file .wav ditemukan di {directory_path}")
59
+ return None
60
+
61
+ master_voiceprint = torch.mean(torch.stack(embeddings), dim=0)
62
+ return master_voiceprint
63
+
64
+ # --- LOADING MODEL (DENGAN CACHE) ---
65
+
66
+ @st.cache_resource
67
+ def load_model_sv():
68
+ st.info("Memuat Model Verifikasi Suara (SpeechBrain)...")
69
+ try:
70
+ model = SpeakerRecognition.from_hparams(
71
+ source="speechbrain/spkrec-ecapa-tdnn",
72
+ savedir="pretrained_models/spkrec-ecapa-tdnn",
73
+ use_auth_token=False
74
+ )
75
+ st.success("Model Verifikasi Suara siap.")
76
+ return model
77
+ except Exception as e:
78
+ st.exception(e)
79
+ st.error("Gagal memuat model SpeechBrain. Cek koneksi internet.")
80
+ return None
81
+
82
+ @st.cache_resource
83
+ def load_model_kws_and_le(path_model, path_le):
84
+ st.info("Memuat Model Pengenal Kata Kunci (KWS) & Label Encoder...")
85
+ model_kws = None
86
+ le = None
87
+
88
+ if not os.path.exists(path_model):
89
+ st.error(f"File model KWS tidak ditemukan di: {path_model}")
90
+ else:
91
+ try:
92
+ model_kws = joblib.load(path_model)
93
+ st.success("Model KWS siap.")
94
+ except Exception as e:
95
+ st.exception(e)
96
+ st.error("Gagal memuat model KWS.")
97
+
98
+ if not os.path.exists(path_le):
99
+ st.error(f"File Label Encoder tidak ditemukan di: {path_le}")
100
+ else:
101
+ try:
102
+ le = joblib.load(path_le)
103
+ st.success(f"Label Encoder siap (Kelas: {le.classes_}).")
104
+ except Exception as e:
105
+ st.exception(e)
106
+ st.error("Gagal memuat Label Encoder.")
107
+
108
+ return model_kws, le
109
+
110
+ @st.cache_resource
111
+ def load_voiceprints(_model_sv):
112
+ st.info("Membuat master voiceprint...")
113
+ voiceprints = {}
114
+
115
+ vp_a = create_master_voiceprint(PATH_ILHAM, _model_sv)
116
+ if vp_a is not None:
117
+ voiceprints["v_ilham"] = vp_a # Perbaikan: Gunakan nama yang benar
118
+ st.success("Voiceprint 'v_ilham' dibuat.")
119
+
120
+ vp_b = create_master_voiceprint(PATH_DANENDRA, _model_sv)
121
+ if vp_b is not None:
122
+ voiceprints["v_danendra"] = vp_b # Perbaikan: Gunakan nama yang benar
123
+ st.success("Voiceprint 'v_danendra' dibuat.")
124
+
125
+ if not voiceprints:
126
+ st.error("Gagal membuat voiceprint. Pastikan folder 'enroll' ada.")
127
+ return None
128
+ return voiceprints
129
+
130
+ # --- FUNGSI PIPELINE UTAMA ---
131
+
132
+ def ekstrak_fitur_kws(audio_file):
133
+ """
134
+ Fungsi ini mengekstrak fitur yang SAMA PERSIS
135
+ dengan yang Anda gunakan untuk melatih Model 1.
136
+ """
137
+ try:
138
+ y, sr = librosa.load(audio_file, sr=16000)
139
+
140
+ y_trimmed, _ = librosa.effects.trim(y, top_db=20)
141
+
142
+ # Perbaikan: Pencegahan error jika audio hening total
143
+ if len(y_trimmed) == 0:
144
+ st.warning("Audio terdeteksi hening. Fitur tidak bisa diekstrak.")
145
+ return None # Kembalikan None jika hening
146
+
147
+ # Perbaikan: Pencegahan error pembagian dengan nol
148
+ y_norm = y_trimmed / (np.max(np.abs(y_trimmed)) + 1e-6)
149
+
150
+ fitur = {
151
+ 'spectral_centroid': np.mean(librosa.feature.spectral_centroid(y=y_norm, sr=sr)),
152
+ 'spectral_bandwidth': np.mean(librosa.feature.spectral_bandwidth(y=y_norm, sr=sr)),
153
+ 'spectral_rolloff': np.mean(librosa.feature.spectral_rolloff(y=y_norm, sr=sr)),
154
+ 'spectral_contrast': np.mean(librosa.feature.spectral_contrast(y=y_norm, sr=sr)),
155
+ 'spectral_flatness': np.mean(librosa.feature.spectral_flatness(y=y_norm)),
156
+ 'mfcc_delta2_mean': np.mean(librosa.feature.delta(librosa.feature.mfcc(y=y_norm, sr=sr, n_mfcc=13), order=2)),
157
+ 'f0_mean': np.nanmean(librosa.pyin(y_norm, fmin=librosa.note_to_hz('C2'), fmax=librosa.note_to_hz('C7'), sr=sr)[0]),
158
+ 'rms': np.mean(librosa.feature.rms(y=y_norm)),
159
+ 'duration': librosa.get_duration(y=y_norm, sr=sr),
160
+ 'std_amplitude': np.std(y_norm)
161
+ }
162
+
163
+ df = pd.DataFrame([fitur]).fillna(0)
164
+
165
+ nama_fitur_akhir = [
166
+ 'spectral_centroid', 'spectral_bandwidth', 'spectral_rolloff',
167
+ 'spectral_contrast', 'spectral_flatness', 'mfcc_delta2_mean',
168
+ 'f0_mean', 'rms', 'duration', 'std_amplitude'
169
+ ]
170
+
171
+ df = df[nama_fitur_akhir]
172
+ return df
173
+
174
+ except Exception as e:
175
+ st.exception(e)
176
+ st.error("Gagal mengekstrak fitur KWS.")
177
+ return None
178
+
179
+
180
+ def cek_keyword(audio_file, model_kws, le): # Perbaikan: Tambahkan 'le'
181
+ """
182
+ Menjalankan Model 1 (KWS) untuk mendeteksi kata kunci.
183
+ """
184
+ fitur = ekstrak_fitur_kws(audio_file)
185
+ if fitur is None:
186
+ return "error"
187
+
188
+ prediksi_angka = model_kws.predict(fitur)
189
+ prediksi_label = le.inverse_transform(prediksi_angka) # Perbaikan: Ubah angka jadi teks
190
+ return prediksi_label[0]
191
+
192
+ def verifikasi_suara(audio_file, model_sv, voiceprints, threshold):
193
+ """
194
+ Menjalankan Model 2 (SV) untuk verifikasi pembicara.
195
+ """
196
+ try:
197
+ test_embedding = get_embedding(audio_file, model_sv)
198
+ if test_embedding is None:
199
+ return False, 0.0, "Gagal buat embedding"
200
+
201
+ best_score = -1.0
202
+ best_match = "None"
203
+
204
+ for name, master_vp in voiceprints.items():
205
+ score = get_similarity(test_embedding, master_vp, model_sv)
206
+ if score > best_score:
207
+ best_score = score
208
+ best_match = name
209
+
210
+ if best_score >= threshold:
211
+ return True, best_score, best_match
212
+ else:
213
+ return False, best_score, "None"
214
+
215
+ except Exception as e:
216
+ st.exception(e)
217
+ st.error("Gagal saat verifikasi suara.")
218
+ return False, 0.0, "Error"
219
+
220
+ # --- MAIN APP ---
221
+ model_sv = load_model_sv()
222
+ # Perbaikan: Muat model dan label encoder
223
+ model_kws, le = load_model_kws_and_le(PATH_MODEL_KWS, PATH_LABEL_ENCODER)
224
+ voiceprints = load_voiceprints(model_sv)
225
+
226
+ # Cek jika model gagal di-load
227
+ if not all([model_sv, model_kws, le, voiceprints]):
228
+ st.error("Gagal memuat semua model/voiceprint. Aplikasi tidak bisa berjalan. Cek error di atas.")
229
+ else:
230
+ st.header("Upload Audio Perintah (.wav)")
231
+ uploaded_file = st.file_uploader("Pilih file audio...", type=["wav"])
232
+
233
+ temp_audio_path = None
234
+ if uploaded_file is not None:
235
+ with open("temp_audio.wav", "wb") as f:
236
+ f.write(uploaded_file.getbuffer())
237
+ temp_audio_path = "temp_audio.wav"
238
+ st.audio(temp_audio_path)
239
+
240
+ if st.button("Proses Perintah", disabled=(temp_audio_path is None)):
241
+ if temp_audio_path:
242
+ with st.spinner("Menganalisis audio..."):
243
+
244
+ # --- LANGKAH 1: Cek Kata Kunci ---
245
+ st.subheader("Hasil Model 1: Pengenalan Kata Kunci")
246
+ kata_kunci = cek_keyword(temp_audio_path, model_kws, le) # Perbaikan: kirim 'le'
247
+
248
+ # Perbaikan: Cek dengan le.classes_
249
+ if kata_kunci in le.classes_:
250
+ st.info(f"Kata kunci terdeteksi: **{kata_kunci.upper()}**")
251
+
252
+ # --- LANGKAH 2: Verifikasi Suara ---
253
+ st.subheader("Hasil Model 2: Verifikasi Suara")
254
+ terverifikasi, skor, nama = verifikasi_suara(
255
+ temp_audio_path, model_sv, voiceprints, THRESHOLD
256
+ )
257
+
258
+ st.info(f"Skor kemiripan tertinggi: **{skor:.2%}** (dengan '{nama}')")
259
+
260
+ # --- KEPUTUSAN AKHIR ---
261
+ st.header("Keputusan Akhir")
262
+ if terverifikasi:
263
+ st.success(f"✅ DITERIMA. Suara terverifikasi sebagai '{nama}'. Perintah **{kata_kunci.upper()}** dijalankan.")
264
+ else:
265
+ st.error(f"❌ DITOLAK. Suara tidak dikenal. Perintah **{kata_kunci.upper()}** dibatalkan.")
266
+
267
+ elif kata_kunci == "error":
268
+ st.error("Terjadi error saat memproses kata kunci.")
269
+ else:
270
+ st.header("Keputusan Akhir")
271
+ st.warning(f"❌ DITOLAK. Perintah tidak dikenal (terdeteksi sebagai: '{kata_kunci}').")
272
+
273
+ # Hapus file sementara
274
+ if os.path.exists("temp_audio.wav"):
275
+ os.remove("temp_audio.wav")