from pathlib import Path import librosa import numpy as np import pandas as pd import streamlit as st from audio_utils_app import ( SUPPORTED_AUDIO_EXTENSIONS, cleanup_temp_files, convert_audio_to_wav, save_uploaded_file_to_temp, ) from predict_app import ( CONFIDENCE_THRESHOLD, MODEL_NOT_FOUND_MESSAGE, MODEL_PATH, get_model_info, predict_audio, ) st.set_page_config( page_title="ConfiVoice", page_icon="CV", layout="wide", ) SUPPORTED_UPLOAD_TYPES = [ extension.replace(".", "") for extension in sorted(SUPPORTED_AUDIO_EXTENSIONS) ] def load_audio_for_plot(wav_path): y, sr = librosa.load(wav_path, sr=22050, mono=True) if y.size == 0: raise ValueError("Audio kosong atau tidak memiliki sinyal suara.") return y, sr def render_waveform(wav_path): y, sr = load_audio_for_plot(wav_path) max_points = 3000 step = max(1, len(y) // max_points) y_plot = y[::step] time_axis = np.arange(len(y_plot)) * step / sr waveform = pd.DataFrame( { "Waktu (detik)": time_axis, "Amplitudo": y_plot, } ).set_index("Waktu (detik)") st.subheader("Waveform") st.line_chart(waveform, height=260) def render_spectrogram(wav_path): y, sr = load_audio_for_plot(wav_path) spectrogram = librosa.amplitude_to_db(np.abs(librosa.stft(y)), ref=np.max) min_value = float(np.min(spectrogram)) max_value = float(np.max(spectrogram)) if max_value > min_value: spectrogram_image = (spectrogram - min_value) / (max_value - min_value) else: spectrogram_image = np.zeros_like(spectrogram) st.subheader("Spectrogram") st.image( np.flipud(spectrogram_image), caption="Frekuensi rendah di bawah, frekuensi tinggi di atas.", use_container_width=True, clamp=True, ) def render_audio_uploader(key): return st.file_uploader( "Upload audio", type=SUPPORTED_UPLOAD_TYPES, key=key, ) def prepare_uploaded_audio(uploaded_file): temp_input_path = save_uploaded_file_to_temp(uploaded_file) temp_wav_path = convert_audio_to_wav(temp_input_path) return temp_input_path, temp_wav_path def render_prediction_result(result): if result.get("is_valid_audio") is False: st.subheader("Audio Tidak Valid") st.error(result.get("error_message") or result.get("explanation") or "Audio tidak valid.") with st.expander("Kualitas audio"): st.json(result.get("audio_quality", {})) return probability_pd = result["probabilities"]["PD"] probability_tpd = result["probabilities"]["TPD"] confidence = result["confidence"] st.subheader("Hasil Prediksi") metric_cols = st.columns(3) metric_cols[0].metric("Label", result["label"]) metric_cols[1].metric("Keterangan", result["description"]) metric_cols[2].metric("Confidence", f"{confidence * 100:.2f}%") st.progress(probability_pd, text=f"Probabilitas PD: {probability_pd * 100:.2f}%") st.progress(probability_tpd, text=f"Probabilitas TPD: {probability_tpd * 100:.2f}%") if confidence < CONFIDENCE_THRESHOLD: st.warning("Model belum yakin. Confidence di bawah 60%, pertimbangkan untuk merekam ulang audio.") else: st.success(f"Hasil utama: {result['label']} - {result['description']}") if result.get("explanation"): st.info(result["explanation"]) with st.expander("Kualitas audio dan indikator suara"): st.write("Kualitas audio") st.json(result.get("audio_quality", {})) st.write("Indikator suara") st.json(result.get("voice_indicators", {})) with st.expander("Output dictionary"): st.json(result) def render_upload_preview(uploaded_file, show_visuals=True): temp_input_path = None temp_wav_path = None try: temp_input_path, temp_wav_path = prepare_uploaded_audio(uploaded_file) st.audio(uploaded_file.getvalue(), format=uploaded_file.type or "audio/wav") if show_visuals: waveform_col, spectrogram_col = st.columns(2) with waveform_col: render_waveform(temp_wav_path) with spectrogram_col: render_spectrogram(temp_wav_path) return temp_input_path except Exception as error: st.error(f"Gagal memproses audio: {error}") return None finally: cleanup_temp_files(temp_wav_path) def show_home(): st.title("ConfiVoice") st.write("Aplikasi Streamlit untuk memprediksi suara PD atau TPD dari model machine learning.") st.info( "Folder aplikasi ini hanya membaca model dari `../ml/models/` dan fitur dari `../ml/features.py`." ) st.write("Menu yang tersedia:") st.write("- Beranda") st.write("- Prediksi Suara") st.write("- Visualisasi Audio") st.write("- Informasi Model") st.write("- Tentang Sistem") def show_prediction(): st.title("Prediksi Suara") st.write("Upload audio dalam format WAV, MP3, M4A, OGG, FLAC, WEBM, atau AAC.") uploaded_file = render_audio_uploader("prediction_upload") if uploaded_file is None: return temp_input_path = None temp_wav_path = None try: temp_input_path, temp_wav_path = prepare_uploaded_audio(uploaded_file) st.audio(uploaded_file.getvalue(), format=uploaded_file.type or "audio/wav") waveform_col, spectrogram_col = st.columns(2) with waveform_col: render_waveform(temp_wav_path) with spectrogram_col: render_spectrogram(temp_wav_path) if st.button("Prediksi", type="primary"): with st.spinner("Mengekstraksi fitur dan menjalankan model..."): result = predict_audio(temp_input_path) render_prediction_result(result) except FileNotFoundError as error: message = MODEL_NOT_FOUND_MESSAGE if str(error) == MODEL_NOT_FOUND_MESSAGE else str(error) st.error(message) except Exception as error: st.error(f"Gagal memproses prediksi: {error}") finally: cleanup_temp_files(temp_input_path, temp_wav_path) def show_visualization(): st.title("Visualisasi Audio") uploaded_file = render_audio_uploader("visualization_upload") if uploaded_file is None: return temp_input_path = None temp_wav_path = None try: temp_input_path, temp_wav_path = prepare_uploaded_audio(uploaded_file) st.audio(uploaded_file.getvalue(), format=uploaded_file.type or "audio/wav") render_waveform(temp_wav_path) render_spectrogram(temp_wav_path) except Exception as error: st.error(f"Gagal membuat visualisasi: {error}") finally: cleanup_temp_files(temp_input_path, temp_wav_path) def show_model_info(): st.title("Informasi Model") try: info = get_model_info() except FileNotFoundError: st.error(MODEL_NOT_FOUND_MESSAGE) return except Exception as error: st.error(f"Gagal membaca informasi model: {error}") return st.write(f"Path model: `{info['model_path']}`") st.write(f"Tipe model: `{info['model_type']}`") st.write(f"Class model: `{', '.join(info['classes'])}`") st.write(f"Jumlah fitur yang diharapkan: `{info['expected_features']}`") st.write(f"Model utama: `{MODEL_PATH.relative_to(MODEL_PATH.parents[2])}`") def show_about(): st.title("Tentang Sistem") st.write( "ConfiVoice memisahkan aplikasi Streamlit dari proses machine learning. " "Folder `cv_app/` berisi antarmuka aplikasi, sementara folder `ml/` tetap menjadi tempat dataset, training, fitur audio, dan model." ) st.write( "Audio upload dikonversi sementara ke WAV mono 22050 Hz, lalu fitur diekstraksi memakai `../ml/features.py`. " "Tidak ada audio upload yang disimpan permanen ke `ml/data/`." ) MENU_HANDLERS = { "Beranda": show_home, "Prediksi Suara": show_prediction, "Visualisasi Audio": show_visualization, "Informasi Model": show_model_info, "Tentang Sistem": show_about, } selected_menu = st.sidebar.radio("Menu", list(MENU_HANDLERS.keys())) MENU_HANDLERS[selected_menu]()