commit 5d2b8bb14dc9a613f81277acd3a35df602bdaa84 Author: Achillean-1 Date: Fri Jul 11 15:28:18 2025 +0700 first commit diff --git a/README.md b/README.md new file mode 100644 index 0000000..29c147f --- /dev/null +++ b/README.md @@ -0,0 +1,14 @@ +### Setup Environtment +python -m venv depression_detection_app + +### Powershell Administrator +Set-ExecutionPolicy -Scope CurrentUser -ExecutionPolicy RemoteSigned + +### Environment Activation +.\depression_detection_app\Scripts\activate + +### Install Requirements +pip install -r requirements.txt + +### Start Program +streamlit run main.py \ No newline at end of file diff --git a/main.py b/main.py new file mode 100644 index 0000000..24c2d11 --- /dev/null +++ b/main.py @@ -0,0 +1,71 @@ +import streamlit as st + +def set_custom_css(): + st.markdown( + """ + + """, + unsafe_allow_html=True, + ) + +def main(): + st.set_page_config(page_title="Aplikasi Pendeteksi Depresi", layout="wide") + set_custom_css() + + st.markdown('
Selamat Datang di Aplikasi Pendeteksi Indikasi Depresi
', unsafe_allow_html=True) + st.markdown('
Aplikasi ini menganalisis ekspresi wajah dan teks untuk mendeteksi potensi indikasi depresi.
', unsafe_allow_html=True) + + st.markdown('
', unsafe_allow_html=True) + if st.button("Mulai Analisis"): + st.switch_page("pages/1_analisis_wajah.py") + st.markdown('
', unsafe_allow_html=True) + +if __name__ == "__main__": + main() \ No newline at end of file diff --git a/model/Model_EfficientNet.tflite b/model/Model_EfficientNet.tflite new file mode 100644 index 0000000..8e2c674 Binary files /dev/null and b/model/Model_EfficientNet.tflite differ diff --git a/model/Pemodelan_GRU_Valid.keras b/model/Pemodelan_GRU_Valid.keras new file mode 100644 index 0000000..6d9115c Binary files /dev/null and b/model/Pemodelan_GRU_Valid.keras differ diff --git a/model/tokenizer_Valid.pkl b/model/tokenizer_Valid.pkl new file mode 100644 index 0000000..0eace44 Binary files /dev/null and b/model/tokenizer_Valid.pkl differ diff --git a/pages/1_analisis_wajah.py b/pages/1_analisis_wajah.py new file mode 100644 index 0000000..0a083cb --- /dev/null +++ b/pages/1_analisis_wajah.py @@ -0,0 +1,584 @@ +#1_analisis_wajah.py +import streamlit as st +import cv2 +import numpy as np +import time +import matplotlib.pyplot as plt +from collections import Counter, deque +import tensorflow as tf +from tensorflow.keras.applications.efficientnet import preprocess_input +from contextlib import contextmanager +import os +import logging + +logging.basicConfig(level=logging.INFO) +logger = logging.getLogger(__name__) + +CATEGORIES = ['Angry', 'Sad', 'Happy', 'Fearful', 'Disgust', 'Neutral', 'Surprised'] +POSITIVE_EMOTIONS = {'Happy', 'Surprised', 'Neutral'} +NEGATIVE_EMOTIONS = {'Angry', 'Sad', 'Fearful', 'Disgust'} +FRAME_INTERVAL = 0.5 +MODEL_PATH = "model/Model_EfficientNet.tflite" +CAMERA_WIDTH = 320 +CAMERA_HEIGHT = 180 +MEDIA_WIDTH = 320 +MEDIA_HEIGHT = 180 +YOUTUBE_URL = "https://www.youtube.com/embed/3XA0bB79oGc?autoplay={}&mute=1" +MAX_PREDICTIONS_BUFFER = 1000 +NEUTRAL_INDEX = CATEGORIES.index('Neutral') + +def load_custom_css(): + css = """ + + """ + st.markdown(css, unsafe_allow_html=True) + +class EmotionAnalyzer: + def __init__(self): + self.interpreter = None + self.input_details = None + self.output_details = None + self.is_loaded = False + + @st.cache_resource + def load_model(_self): + if not os.path.exists(MODEL_PATH): + raise FileNotFoundError(f"Model file {MODEL_PATH} tidak ditemukan") + + try: + with st.spinner('Memuat model TFLite...'): + start_time = time.time() + interpreter = tf.lite.Interpreter(model_path=MODEL_PATH) + interpreter.allocate_tensors() + + input_details = interpreter.get_input_details() + output_details = interpreter.get_output_details() + + load_time = time.time() - start_time + logger.info(f"Model loaded successfully in {load_time:.2f} seconds") + + return interpreter, input_details, output_details + + except Exception as e: + logger.error(f"Error loading model: {e}") + raise + + def initialize(self): + if not self.is_loaded: + self.interpreter, self.input_details, self.output_details = self.load_model() + self.is_loaded = True + + def preprocess_image(self, image): + try: + image_resized = cv2.resize(image, (224, 224)) + image_array = np.expand_dims(image_resized, axis=0).astype(np.float32) + image_array = preprocess_input(image_array) + return image_array + except Exception as e: + logger.error(f"Error in preprocessing: {e}") + return None + + def predict(self, image): + if not self.is_loaded: + raise RuntimeError("Model belum dimuat") + + try: + processed_image = self.preprocess_image(image) + if processed_image is None: + return NEUTRAL_INDEX, 1.0 + + self.interpreter.set_tensor(self.input_details[0]['index'], processed_image) + + self.interpreter.invoke() + + predictions = self.interpreter.get_tensor(self.output_details[0]['index'])[0] + + pred_idx = np.argmax(predictions) + confidence = predictions[pred_idx] + + return pred_idx, confidence + + except Exception as e: + logger.error(f"Error in prediction: {e}") + return NEUTRAL_INDEX, 1.0 + +class EmotionDataManager: + def __init__(self): + self.predictions = deque(maxlen=MAX_PREDICTIONS_BUFFER) + self.timestamps = deque(maxlen=MAX_PREDICTIONS_BUFFER) + self.start_time = None + + def reset(self): + self.predictions.clear() + self.timestamps.clear() + self.start_time = None + + def add_prediction(self, pred_idx, timestamp=None): + if self.start_time is None: + self.start_time = time.time() + + if timestamp is None: + timestamp = time.time() - self.start_time + + self.predictions.append(pred_idx) + self.timestamps.append(timestamp) + + def get_emotion_scores(self): + if not self.predictions: + return 0, 0 + + emotion_names = [CATEGORIES[idx] for idx in self.predictions] + total = len(emotion_names) + + negative_count = sum(1 for emotion in emotion_names if emotion in NEGATIVE_EMOTIONS) + positive_count = sum(1 for emotion in emotion_names if emotion in POSITIVE_EMOTIONS) + + negative_score = (negative_count / total) * 100 + positive_score = (positive_count / total) * 100 + + return negative_score, positive_score + + def get_dominant_emotions(self, top_n=3): + if not self.predictions: + return [] + + emotion_counts = Counter(self.predictions) + most_common = emotion_counts.most_common(top_n) + total = sum(emotion_counts.values()) + + return [(CATEGORIES[emotion], count, (count/total)*100) + for emotion, count in most_common] + + def get_emotion_changes(self, top_n=3): + if len(self.predictions) < 2: + return [] + + changes = [] + for i in range(1, len(self.predictions)): + if self.predictions[i] != self.predictions[i-1]: + from_emotion = CATEGORIES[self.predictions[i-1]] + to_emotion = CATEGORIES[self.predictions[i]] + change_time = self.timestamps[i] + changes.append((from_emotion, to_emotion, change_time)) + + return sorted(changes, key=lambda x: x[2])[:top_n] + +@contextmanager +def camera_context(): + cap = cv2.VideoCapture(0) + cap.set(cv2.CAP_PROP_FRAME_WIDTH, CAMERA_WIDTH) + cap.set(cv2.CAP_PROP_FRAME_HEIGHT, CAMERA_HEIGHT) + cap.set(cv2.CAP_PROP_BUFFERSIZE, 1) + + try: + yield cap + finally: + cap.release() + +def create_metric_display(label, value, container_class="metric-container"): + return f""" +
+
{label}
+
{value}
+
+ """ + +def create_status_indicator(status): + status_map = { + 'analyzing': ('Sedang Menganalisis', 'status-analyzing'), + 'stopped': ('Analisis Dihentikan', 'status-stopped'), + 'ready': ('Siap Memulai', 'status-ready') + } + + text, css_class = status_map.get(status, ('Unknown', 'status-ready')) + return f'{text}' + +def initialize_session_state(): + defaults = { + 'is_analyzing': False, + 'results_ready': False, + 'current_expression': "-", + 'current_accuracy': "-", + 'last_capture_time': 0, + 'analyzer': None, + 'data_manager': None, + 'video_started': False, + 'analysis_start_time': None + } + + for key, value in defaults.items(): + if key not in st.session_state: + st.session_state[key] = value + + if st.session_state.analyzer is None: + st.session_state.analyzer = EmotionAnalyzer() + + if st.session_state.data_manager is None: + st.session_state.data_manager = EmotionDataManager() + +def render_results(): + data_manager = st.session_state.data_manager + + if not data_manager.predictions: + st.warning("Tidak ada data yang dikumpulkan selama analisis.") + return + + st.success("✅ Analisis emosi wajah selesai!") + + negative_score, positive_score = data_manager.get_emotion_scores() + + col1, col2 = st.columns(2) + + with col1: + st.markdown(create_metric_display( + "Emosi Negatif", + f"{negative_score:.1f}%", + "metric-container negative-score" + ), unsafe_allow_html=True) + + with col2: + st.markdown(create_metric_display( + "Emosi Positif", + f"{positive_score:.1f}%", + "metric-container positive-score" + ), unsafe_allow_html=True) + + st.markdown("
", unsafe_allow_html=True) + if len(data_manager.predictions) > 1: + fig, ax = plt.subplots(figsize=(12, 6)) + fig.patch.set_facecolor('#f4f4f7') + ax.set_facecolor('#f4f4f7') + + timestamps = list(data_manager.timestamps) + expressions = [CATEGORIES[idx] for idx in data_manager.predictions] + + ax.scatter(timestamps, expressions, c='#3a7aff', alpha=0.7, s=30) + ax.plot(timestamps, expressions, color='#3a7aff', alpha=0.5, linewidth=1) + + ax.set_xlabel("Waktu (detik)", color='#111c4e', fontsize=12) + ax.set_ylabel("Ekspresi", color='#111c4e', fontsize=12) + ax.set_title("Timeline Perubahan Ekspresi", color='#111c4e', fontsize=14, fontweight='bold') + + ax.tick_params(axis='both', colors='#111c4e', labelsize=10) + for spine in ax.spines.values(): + spine.set_color('#111c4e') + + ax.grid(True, alpha=0.3) + plt.tight_layout() + st.pyplot(fig) + plt.close(fig) + + col1, col2 = st.columns(2) + + with col1: + st.subheader("Emosi Dominan") + dominant_emotions = data_manager.get_dominant_emotions() + for emotion, count, percentage in dominant_emotions: + st.write(f"**{emotion}**: {percentage:.1f}% ({count} deteksi)") + + with col2: + st.subheader("Perubahan Tercepat") + emotion_changes = data_manager.get_emotion_changes() + if emotion_changes: + for from_emotion, to_emotion, change_time in emotion_changes: + st.write(f"**{from_emotion}** → **{to_emotion}** ({change_time:.1f}s)") + else: + st.info("Tidak ada perubahan emosi terdeteksi") + + st.markdown("
", unsafe_allow_html=True) + +def main(): + st.set_page_config( + page_title="Analisis Emosi Wajah", + layout="wide" + ) + + initialize_session_state() + load_custom_css() + + st.markdown("

Analisis Emosi Wajah Real-time

", unsafe_allow_html=True) + + status = 'analyzing' if st.session_state.is_analyzing else ('ready' if not st.session_state.results_ready else 'stopped') + st.markdown(create_status_indicator(status), unsafe_allow_html=True) + + col1, col2 = st.columns([2, 1]) + + with col1: + youtube_placeholder = st.empty() + youtube_placeholder.markdown( + f'', + unsafe_allow_html=True + ) + st.markdown('', unsafe_allow_html=True) + + with col2: + video_placeholder = st.empty() + st.markdown('', unsafe_allow_html=True) + + st.subheader("📊 Metrik Real-time") + + expression_placeholder = st.empty() + accuracy_placeholder = st.empty() + + st.subheader("Kontrol") + button_text = 'Akhiri Analisis' if st.session_state.is_analyzing else 'Mulai Analisis' + start_stop_button = st.button(button_text, key="main_control") + + st.markdown('', unsafe_allow_html=True) + + expression_placeholder.markdown( + create_metric_display("Ekspresi Terdeteksi", st.session_state.current_expression), + unsafe_allow_html=True + ) + accuracy_placeholder.markdown( + create_metric_display("Tingkat Keyakinan", st.session_state.current_accuracy), + unsafe_allow_html=True + ) + + if start_stop_button: + if not st.session_state.is_analyzing: + try: + st.session_state.analyzer.initialize() + + st.session_state.data_manager.reset() + st.session_state.is_analyzing = True + st.session_state.results_ready = False + st.session_state.last_capture_time = 0 + st.session_state.video_started = False + st.session_state.analysis_start_time = time.time() + + youtube_placeholder.markdown( + f'', + unsafe_allow_html=True + ) + + st.rerun() + + except Exception as e: + st.error(f"Error memulai analisis: {e}") + st.session_state.is_analyzing = False + else: + st.session_state.is_analyzing = False + st.session_state.results_ready = True + st.session_state.current_expression = "-" + st.session_state.current_accuracy = "-" + st.session_state.video_started = False + youtube_placeholder.markdown( + f'', + unsafe_allow_html=True + ) + video_placeholder.image(np.zeros((MEDIA_HEIGHT, MEDIA_WIDTH, 3), dtype=np.uint8), channels="RGB", width=MEDIA_WIDTH) + st.rerun() + + if st.session_state.is_analyzing: + try: + with camera_context() as cap: + if not cap.isOpened(): + st.error("❌ Tidak dapat mengakses kamera") + st.session_state.is_analyzing = False + youtube_placeholder.markdown( + f'', + unsafe_allow_html=True + ) + st.rerun() + + while st.session_state.is_analyzing: + ret, frame = cap.read() + if not ret: + st.error("❌ Error membaca frame dari kamera") + break + + current_time = time.time() + frame_rgb = cv2.cvtColor(frame, cv2.COLOR_BGR2RGB) + + video_placeholder.image(frame_rgb, channels="RGB", width=MEDIA_WIDTH) + + if not st.session_state.video_started and (current_time - st.session_state.analysis_start_time) >= 10: + st.session_state.video_started = True + youtube_placeholder.markdown( + f'', + unsafe_allow_html=True + ) + + if current_time - st.session_state.last_capture_time >= FRAME_INTERVAL: + pred_idx, confidence = st.session_state.analyzer.predict(frame_rgb) + + if pred_idx is not None: + st.session_state.data_manager.add_prediction(pred_idx) + + current_expression = CATEGORIES[pred_idx] + current_accuracy = f"{confidence*100:.1f}%" + + st.session_state.current_expression = current_expression + st.session_state.current_accuracy = current_accuracy + + expression_placeholder.markdown( + create_metric_display("Ekspresi Terdeteksi", current_expression), + unsafe_allow_html=True + ) + accuracy_placeholder.markdown( + create_metric_display("Tingkat Keyakinan", current_accuracy), + unsafe_allow_html=True + ) + + st.session_state.last_capture_time = current_time + + time.sleep(0.03) + + except Exception as e: + st.error(f"Error selama analisis: {e}") + st.session_state.is_analyzing = False + youtube_placeholder.markdown( + f'', + unsafe_allow_html=True + ) + video_placeholder.image(np.zeros((MEDIA_HEIGHT, MEDIA_WIDTH, 3), dtype=np.uint8), channels="RGB", width=MEDIA_WIDTH) + + if not st.session_state.is_analyzing: + expression_placeholder.markdown( + create_metric_display("Ekspresi Terdeteksi", "-"), + unsafe_allow_html=True + ) + accuracy_placeholder.markdown( + create_metric_display("Tingkat Keyakinan", "-"), + unsafe_allow_html=True + ) + + if st.session_state.results_ready and not st.session_state.is_analyzing: + render_results() + + col1, col2 = st.columns(2) + + with col1: + if st.button("🔄 Reset Analisis", key="reset"): + st.session_state.data_manager.reset() + st.session_state.is_analyzing = False + st.session_state.results_ready = False + st.session_state.current_expression = "-" + st.session_state.current_accuracy = "-" + st.session_state.video_started = False + youtube_placeholder.markdown( + f'', + unsafe_allow_html=True + ) + video_placeholder.image(np.zeros((MEDIA_HEIGHT, MEDIA_WIDTH, 3), dtype=np.uint8), channels="RGB", width=MEDIA_WIDTH) + st.rerun() + + with col2: + if st.button("📝 Lanjut ke Jurnal", key="journal"): + try: + st.switch_page("pages/2_jurnaling.py") + except Exception as e: + st.error(f"Error: Halaman journaling tidak tersedia - {e}") + +if __name__ == "__main__": + main() \ No newline at end of file diff --git a/pages/2_jurnaling.py b/pages/2_jurnaling.py new file mode 100644 index 0000000..2829792 --- /dev/null +++ b/pages/2_jurnaling.py @@ -0,0 +1,334 @@ +#2_jurnaling.py +import streamlit as st +import numpy as np +import pickle +import tensorflow as tf +import matplotlib.pyplot as plt +import pandas as pd +import gdown +import os +import re +from Sastrawi.Stemmer.StemmerFactory import StemmerFactory +from nltk.corpus import stopwords + +import nltk +nltk.download('stopwords') + +st.set_page_config( + page_title="Analisis Journaling", + layout="wide", + initial_sidebar_state="collapsed" +) + +MODEL_URL = "https://drive.google.com/uc?id=1ArBDPUBcPMdsUzH_dKwdjVEK-DyXG9qz" +TOKENIZER_URL = "https://drive.google.com/uc?id=1YVdwW-58y1Jie01MOjkd-4nY3bWFmt0E" +MODEL_PATH = "model/Pemodelan_GRU_Valid.keras" +TOKENIZER_PATH = "model/tokenizer_Valid.pkl" + +os.makedirs("model", exist_ok=True) + +@st.cache_resource +def load_model(): + try: + if not os.path.exists(MODEL_PATH): + st.info("Mengunduh model dari Google Drive...") + gdown.download(MODEL_URL, MODEL_PATH, quiet=False) + return tf.keras.models.load_model(MODEL_PATH) + except Exception as e: + st.error(f"Gagal memuat model: {str(e)}") + st.warning("Model tidak ditemukan. Menggunakan data demo.") + return None + +@st.cache_resource +def load_tokenizer(): + try: + if not os.path.exists(TOKENIZER_PATH): + st.info("Mengunduh tokenizer dari Google Drive...") + gdown.download(TOKENIZER_URL, TOKENIZER_PATH, quiet=False) + with open(TOKENIZER_PATH, "rb") as handle: + return pickle.load(handle) + except Exception as e: + st.error(f"Gagal memuat tokenizer: {str(e)}") + st.warning("Tokenizer tidak ditemukan. Menggunakan data demo.") + return None + +model = load_model() +tokenizer = load_tokenizer() + +MAXLEN = 14 + +stop_words = set(stopwords.words('indonesian')) +stemmer = StemmerFactory().create_stemmer() + +st.markdown(""" + +""", unsafe_allow_html=True) + +st.markdown('
Analisis Jurnaling
', unsafe_allow_html=True) +st.markdown('
TULISKAN EKSPRESIMU DENGAN KATA-KATA
', unsafe_allow_html=True) + +text_input = st.text_area("", height=200, placeholder="Tuliskan isi jurnal anda di sini...") + +col1, col2, col3 = st.columns([1, 2, 1]) +with col2: + st.markdown('
', unsafe_allow_html=True) + analyze_button = st.button("Analisis Teks", key="analyze", use_container_width=True) + st.markdown('
', unsafe_allow_html=True) + +def clean_text(text): + text = text.lower() + text = re.sub(r'http\S+', '', text) + text = re.sub(r'[^a-zA-Z\s]', '', text) + text = re.sub(r'\s+', ' ', text).strip() + + words = text.split() + words = [stemmer.stem(word) for word in words if word not in stop_words] + return ' '.join(words) + +def analyze_text(text): + if model is None or tokenizer is None: + return { + "emotions": { + "marah": 0.01, + "sedih": 0.02, + "jijik": 0.048, + "takut": 0.01, + "bahagia": 0.01, + "netral": 0.945, + "terkejut": 0.005 + }, + "dominant_emotion": "netral", + "text": text + } + + clean_text_input = clean_text(text) + + text_seq = tokenizer.texts_to_sequences([clean_text_input]) + + if not text_seq[0]: + st.warning("Teks tidak mengandung kata yang dikenali oleh model. Coba gunakan kata-kata yang lebih umum.") + return { + "emotions": {label: 0.0 for label in ["marah", "sedih", "bahagia", "takut", "jijik", "netral", "terkejut"]}, + "dominant_emotion": "tidak_dikenali", + "text": text + } + + text_padded = tf.keras.preprocessing.sequence.pad_sequences(text_seq, maxlen=MAXLEN, padding='post') + + prediction = model.predict(text_padded, verbose=0) + predicted_class = np.argmax(prediction, axis=1)[0] + + label_mapping = {0: "marah", 1: "sedih", 2: "bahagia", 3: "takut", 4: "jijik", 5: "netral", 6: "terkejut"} + emotion_label = label_mapping[predicted_class] + + emotions = {} + for i, label in label_mapping.items(): + emotions[label] = float(prediction[0][i]) + + return { + "emotions": emotions, + "dominant_emotion": emotion_label, + "text": text + } + +if analyze_button: + if text_input: + result = analyze_text(text_input) + st.session_state.text_analysis_result = result + st.rerun() + else: + st.warning("Silakan masukkan teks terlebih dahulu.") + +if 'text_analysis_result' in st.session_state: + result = st.session_state.text_analysis_result + + st.markdown("### Hasil:") + st.markdown("#### Emosi Yang Terdeteksi:") + + emotion_colors = { + "marah": "#E53935", + "sedih": "#7986CB", + "jijik": "#8BC34A", + "takut": "#FFB74D", + "bahagia": "#4CAF50", + "netral": "#9E9E9E", + "terkejut": "#1E88E5" + } + + top_emotions = sorted(result["emotions"].items(), key=lambda x: x[1], reverse=True)[:3] + + for emotion, score in top_emotions: + emotion_name = emotion.capitalize() + score_percent = score * 100 + color = emotion_colors.get(emotion, "#FFFFFF") + + st.markdown( + f'
{emotion_name} {score_percent:.1f}%
', + unsafe_allow_html=True + ) + + st.markdown('
Top 3 Emosi
', unsafe_allow_html=True) + + emotions = [e[0].capitalize() for e in top_emotions] + scores = [e[1]*100 for e in top_emotions] + + fig, ax = plt.subplots(figsize=(10, 4)) + + colors = [emotion_colors.get(emotion.lower(), "#1E88E5") for emotion in emotions] + + bars = ax.barh(emotions, scores, color=colors, height=0.5) + + for bar in bars: + width = bar.get_width() + ax.text(width + 1, bar.get_y() + bar.get_height()/2, f'{width:.1f}%', + va='center', fontweight='bold') + + ax.set_xlim(0, 100) + ax.set_xlabel('Confidence (%)') + ax.spines['top'].set_visible(False) + ax.spines['right'].set_visible(False) + ax.spines['bottom'].set_color('#DDDDDD') + ax.spines['left'].set_color('#DDDDDD') + ax.tick_params(bottom=False, left=False) + ax.set_axisbelow(True) + ax.grid(axis='x', linestyle='-', alpha=0.2) + + st.pyplot(fig) + + positive_emotions = ["bahagia", "netral", "terkejut"] + negative_emotions = ["marah", "sedih", "jijik", "takut"] + + positive_score = sum(result["emotions"][e] for e in positive_emotions) * 100 + negative_score = sum(result["emotions"][e] for e in negative_emotions) * 100 + + st.markdown('
', unsafe_allow_html=True) + + st.markdown( + f'
Positive Sentiment {positive_score:.1f}%
', + unsafe_allow_html=True + ) + + st.markdown( + f'
Negative Sentiment {negative_score:.1f}%
', + unsafe_allow_html=True + ) + + fig2, ax2 = plt.subplots(figsize=(10, 2)) + + sentiments = ["Positive", "Negative"] + sentiment_scores = [positive_score, negative_score] + sentiment_colors = ["#4CAF50", "#E53935"] + + bars2 = ax2.barh(sentiments, sentiment_scores, color=sentiment_colors, height=0.5) + + for bar in bars2: + width = bar.get_width() + ax2.text(width + 1, bar.get_y() + bar.get_height()/2, f'{width:.1f}%', + va='center', fontweight='bold') + + ax2.set_xlim(0, 100) + ax2.set_xlabel('Sentiment Score (%)') + ax2.spines['top'].set_visible(False) + ax2.spines['right'].set_visible(False) + ax2.spines['bottom'].set_color('#DDDDDD') + ax2.spines['left'].set_color('#DDDDDD') + ax2.tick_params(bottom=False, left=False) + ax2.set_axisbelow(True) + ax2.grid(axis='x', linestyle='-', alpha=0.2) + + st.pyplot(fig2) + + st.markdown('
', unsafe_allow_html=True) + multimodal_button = st.button("Lihat Hasil Multimodal", key="multimodal", use_container_width=True) + st.markdown('
', unsafe_allow_html=True) + + if multimodal_button: + st.switch_page("pages/3_hasil.py") \ No newline at end of file diff --git a/pages/3_hasil.py b/pages/3_hasil.py new file mode 100644 index 0000000..55d180e --- /dev/null +++ b/pages/3_hasil.py @@ -0,0 +1,340 @@ +#3_hasil.py +import streamlit as st +import matplotlib.pyplot as plt +import numpy as np +from collections import Counter + +CATEGORIES = ['Angry', 'Sad', 'Happy', 'Fearful', 'Disgust', 'Neutral', 'Surprised'] + +st.set_page_config(page_title="Analisis Multimodal", layout="wide") + +st.markdown(""" + +""", unsafe_allow_html=True) + +st.markdown('
📊 Laporan Analisis Multimodal
', unsafe_allow_html=True) + +emotion_mapping = { + 'Happy': 'bahagia', + 'Surprised': 'terkejut', + 'Neutral': 'netral', + 'Angry': 'marah', + 'Sad': 'sedih', + 'Fearful': 'takut', + 'Disgust': 'jijik', + 'bahagia': 'bahagia', + 'terkejut': 'terkejut', + 'netral': 'netral', + 'marah': 'marah', + 'sedih': 'sedih', + 'takut': 'takut', + 'jijik': 'jijik' +} + +positive_emotions = ['bahagia', 'terkejut', 'netral'] +negative_emotions = ['marah', 'sedih', 'takut', 'jijik'] + +if 'data_manager' not in st.session_state or st.session_state.data_manager is None: + st.error("Data emosi wajah tidak tersedia. Silakan lakukan analisis wajah terlebih dahulu.") + st.markdown('
', unsafe_allow_html=True) + if st.button("Lakukan Analisis Wajah"): + try: + st.switch_page("pages/1_analisis_wajah.py") + except FileNotFoundError: + st.error("Halaman analisis wajah tidak ditemukan.") + st.markdown('
', unsafe_allow_html=True) + st.stop() + +if 'text_analysis_result' not in st.session_state or not st.session_state.text_analysis_result: + st.error("Data analisis teks tidak tersedia. Silakan lakukan analisis teks terlebih dahulu.") + st.markdown('
', unsafe_allow_html=True) + if st.button("Lakukan Analisis Teks"): + try: + st.switch_page("pages/2_analisis_teks.py") + except FileNotFoundError: + st.error("Halaman analisis teks tidak ditemukan.") + st.markdown('
', unsafe_allow_html=True) + st.stop() + +data_manager = st.session_state.data_manager +if not data_manager.predictions or not data_manager.timestamps: + st.error("Tidak ada data emosi wajah yang valid untuk analisis.") + st.stop() + +face_emotion_data = [(timestamp, CATEGORIES[pred_idx]) for pred_idx, timestamp in zip(data_manager.predictions, data_manager.timestamps)] +for timestamp, emotion in face_emotion_data: + if not isinstance(timestamp, (int, float)) or not isinstance(emotion, str): + st.error("Format data emosi wajah tidak valid.") + st.stop() + if emotion not in emotion_mapping: + st.warning(f"Emosi tidak dikenal: {emotion}. Melewati emosi ini.") + face_emotion_data = [(t, e) for t, e in face_emotion_data if e in emotion_mapping] + if not face_emotion_data: + st.error("Tidak ada data emosi wajah yang valid.") + st.stop() + +text_result = st.session_state.text_analysis_result +if not isinstance(text_result, dict) or 'text' not in text_result: + st.error("Format data analisis teks tidak valid.") + st.stop() + +# Process face emotions +dominant_face_emotions = {} +for _, emotion in face_emotion_data: + standardized_emotion = emotion_mapping.get(emotion) + if standardized_emotion: + dominant_face_emotions[standardized_emotion] = dominant_face_emotions.get(standardized_emotion, 0) + 1 + +total_face_frames = len(face_emotion_data) +if total_face_frames == 0: + st.error("Tidak ada data emosi wajah yang valid untuk analisis.") + st.stop() + +face_emotion_percentages = {emotion: (count / total_face_frames) * 100 + for emotion, count in dominant_face_emotions.items()} +top_face_emotions = sorted(face_emotion_percentages.items(), key=lambda x: x[1], reverse=True)[:3] + +text_emotions = {} +if "emotions" in text_result: + text_emotions = {emotion_mapping.get(emotion, emotion): score * 100 + for emotion, score in text_result["emotions"].items() + if emotion_mapping.get(emotion, emotion) in emotion_mapping.values()} +elif "top_emotions" in text_result: + text_emotions = {emotion_mapping.get(emotion, emotion): score * 100 + for emotion, score in text_result["top_emotions"] + if emotion_mapping.get(emotion, emotion) in emotion_mapping.values()} + +top_text_emotions = sorted(text_emotions.items(), key=lambda x: x[1], reverse=True)[:3] + +combined_emotions = {} +for emotion, percentage in face_emotion_percentages.items(): + combined_emotions[emotion] = combined_emotions.get(emotion, 0) + percentage + +standardized_text_emotions = {emotion_mapping.get(e, e): score for e, score in text_emotions.items()} +for emotion, percentage in standardized_text_emotions.items(): + if emotion in emotion_mapping.values(): + combined_emotions[emotion] = combined_emotions.get(emotion, 0) + percentage + +for emotion in combined_emotions: + appeared_in_face = emotion in face_emotion_percentages + appeared_in_text = emotion in standardized_text_emotions + divisor = 1 + (1 if appeared_in_face and appeared_in_text else 0) + combined_emotions[emotion] /= divisor + +total_combined = sum(combined_emotions.values()) +if total_combined > 0: + combined_emotions = {emotion: (score / total_combined) * 100 + for emotion, score in combined_emotions.items()} +top_combined_emotions = sorted(combined_emotions.items(), key=lambda x: x[1], reverse=True)[:3] + +face_positive_score = sum(face_emotion_percentages.get(emotion, 0) for emotion in positive_emotions) +face_negative_score = sum(face_emotion_percentages.get(emotion, 0) for emotion in negative_emotions) +total_face_score = face_positive_score + face_negative_score +if total_face_score > 0: + face_positive_score = (face_positive_score / total_face_score) * 100 + face_negative_score = (face_negative_score / total_face_score) * 100 +else: + face_positive_score = 0 + face_negative_score = 0 + st.warning("Tidak ada data emosi wajah yang valid untuk menghitung skor positif/negatif.") + +if "positive_score" in text_result and "negative_score" in text_result: + text_positive_score = text_result['positive_score'] * 100 + text_negative_score = text_result['negative_score'] * 100 +else: + text_positive_score = sum(text_emotions.get(emotion, 0) for emotion in positive_emotions) + text_negative_score = sum(text_emotions.get(emotion, 0) for emotion in negative_emotions) + total_text_score = text_positive_score + text_negative_score + if total_text_score > 0: + text_positive_score = (text_positive_score / total_text_score) * 100 + text_negative_score = (text_negative_score / total_text_score) * 100 + else: + text_positive_score = 0 + text_negative_score = 0 + st.warning("Tidak ada data emosi teks yang valid untuk menghitung skor positif/negatif.") + +avg_positive_score = (face_positive_score + text_positive_score) / 2 +avg_negative_score = (face_negative_score + text_negative_score) / 2 + +emotion_changes = {} +for i in range(1, len(face_emotion_data)): + if face_emotion_data[i][1] != face_emotion_data[i-1][1]: + key = f"{face_emotion_data[i-1][1]} → {face_emotion_data[i][1]}" + time_diff = face_emotion_data[i][0] - face_emotion_data[i-1][0] + emotion_changes[key] = time_diff + +st.markdown('

📝 Ringkasan Umum

', unsafe_allow_html=True) +st.write(f"**Durasi Analisis:** {face_emotion_data[-1][0]:.2f} detik") +st.write(f"**Jumlah Perubahan Emosi:** {len(emotion_changes)}") +st.markdown("### Skor Rata-rata Emosi (Wajah & Teks)") +st.write(f"🟢 Rata-rata Emosi Positif: {avg_positive_score:.1f}%", unsafe_allow_html=True) +st.write(f"🔴 Rata-rata Emosi Negatif: {avg_negative_score:.1f}%", unsafe_allow_html=True) + +if avg_positive_score > 0 or avg_negative_score > 0: + fig, ax = plt.subplots(figsize=(10, 3)) + labels = ['Positif', 'Negatif'] + values = [avg_positive_score, avg_negative_score] + colors = ['#2ecc71', '#e74c3c'] + + bars = ax.barh(labels, values, color=colors, height=0.5) + + for bar in bars: + width = bar.get_width() + ax.text(width + 1, bar.get_y() + bar.get_height()/2, f'{width:.1f}%', + va='center', fontweight='bold') + + ax.set_xlim(0, 100) + ax.set_xlabel('Persentase (%)') + ax.spines['top'].set_visible(False) + ax.spines['right'].set_visible(False) + ax.spines['bottom'].set_color('#DDDDDD') + ax.spines['left'].set_color('#DDDDDD') + ax.tick_params(bottom=False, left=False) + ax.set_axisbelow(True) + ax.grid(axis='x', linestyle='-', alpha=0.2) + + st.pyplot(fig) +else: + st.warning("Tidak cukup data untuk menampilkan grafik skor positif/negatif.") + +st.write("### Top 3 Emosi Gabungan (Wajah & Teks)") +for emotion, score in top_combined_emotions: + st.write(f"- {emotion.capitalize()}: {score:.1f}%") + +st.markdown('', unsafe_allow_html=True) +st.markdown('

😀 Analisis Ekspresi Wajah

', unsafe_allow_html=True) +st.write("### Top 3 Emosi Wajah") +for emotion, percentage in top_face_emotions: + count = dominant_face_emotions[emotion] + st.write(f"- {emotion.capitalize()}: {percentage:.1f}% ({count} kali)") + +st.write(f"### Skor Emosi Wajah") +st.write(f"🟢 Skor Positif: {face_positive_score:.1f}%", unsafe_allow_html=True) +st.write(f"🔴 Skor Negatif: {face_negative_score:.1f}%", unsafe_allow_html=True) + +if len(face_emotion_data) > 1: + fig, ax = plt.subplots(figsize=(12, 6)) + fig.patch.set_facecolor('#f4f4f7') + ax.set_facecolor('#f4f4f7') + + timestamps = [data[0] for data in face_emotion_data] + emotions = [data[1] for data in face_emotion_data] + + ax.scatter(timestamps, emotions, c='#3a7aff', alpha=0.7, s=30) + ax.plot(timestamps, emotions, color='#3a7aff', alpha=0.5, linewidth=1) + + ax.set_xlabel("Waktu (detik)", color='#111c4e', fontsize=12) + ax.set_ylabel("Ekspresi", color='#111c4e', fontsize=12) + ax.set_title("Timeline Perubahan Ekspresi", color='#111c4e', fontsize=14, fontweight='bold') + + ax.tick_params(axis='both', colors='#111c4e', labelsize=10) + for spine in ax.spines.values(): + spine.set_color('#111c4e') + + ax.grid(True, alpha=0.3) + + plt.tight_layout() + + st.pyplot(fig) + plt.close(fig) + +if emotion_changes: + st.write("### Perubahan Emosi Tercepat") + for change, time in sorted(emotion_changes.items(), key=lambda x: x[1])[:3]: + st.write(f"- {change} dalam {time:.2f} detik") + +st.markdown('', unsafe_allow_html=True) +st.markdown('

📝 Analisis Teks Jurnaling

', unsafe_allow_html=True) +st.write("### Top 3 Emosi Terdeteksi") +for emotion, percentage in top_text_emotions: + st.write(f"- {emotion.capitalize()}: {percentage:.1f}%") + +st.write(f"### Skor Emosi") +st.write(f"🟢 Skor Positif: {text_positive_score:.1f}%", unsafe_allow_html=True) +st.write(f"🔴 Skor Negatif: {text_negative_score:.1f}%", unsafe_allow_html=True) +st.write("### Teks Jurnal") +st.write(text_result['text']) + +st.markdown('', unsafe_allow_html=True) +st.markdown('

🔍 Kesimpulan Akhir

', unsafe_allow_html=True) +st.markdown("### Fusion dengan Majority Voting") +st.write("Metode ini menggabungkan keputusan dari setiap modalitas dengan menerapkan prinsip voting mayoritas.") + +face_classification = 1 if face_negative_score >= face_positive_score else 0 +text_classification = 1 if text_negative_score >= text_positive_score else 0 +fused_score = np.mean([face_classification, text_classification]) + +if avg_negative_score >= 50: + st.error("⚠️ Analisis menunjukkan potensi indikasi depresi.") + st.write("Disarankan untuk berbicara dengan konselor atau psikolog.") +elif avg_negative_score < 49: + st.success("✅ Analisis menunjukkan kondisi emosi stabil.") + st.write("Tetap jaga kesehatan mental dan lanjutkan kegiatan positif.") +else: + st.warning("⚖️ Analisis menunjukkan kondisi emosi netral (skor negatif 49%).") + st.write("Pertimbangkan konsultasi jika merasa perlu.") + +st.write(f"- Klasifikasi dari analisis wajah: {'Indikasi Negatif' if face_classification == 1 else 'Indikasi Positif'}") +st.write(f"- Klasifikasi dari analisis teks: {'Indikasi Negatif' if text_classification == 1 else 'Indikasi Positif'}") + +st.markdown('
', unsafe_allow_html=True) +if st.button("Ulangi Analisis"): + st.session_state.pop('data_manager', None) + st.session_state.pop('text_analysis_result', None) + try: + st.switch_page("pages/1_analisis_wajah.py") + except FileNotFoundError: + st.error("Halaman analisis wajah tidak ditemukan.") +st.markdown('
', unsafe_allow_html=True) \ No newline at end of file diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..3e9b16a --- /dev/null +++ b/requirements.txt @@ -0,0 +1,10 @@ +opencv-contrib-python==4.9.0.80 +opencv-python==4.8.1.78 +pandas==2.1.4 +matplotlib==3.9.2 +numpy==1.26.2 +tensorflow==2.18.0 +streamlit==1.39.0 +gdown==5.2.0 +Sastrawi==1.0.1 +nltk==3.9.1 \ No newline at end of file