first commit

This commit is contained in:
Achillean-1
2025-07-11 15:28:18 +07:00
commit 5d2b8bb14d
9 changed files with 1353 additions and 0 deletions

14
README.md Normal file
View File

@ -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

71
main.py Normal file
View File

@ -0,0 +1,71 @@
import streamlit as st
def set_custom_css():
st.markdown(
"""
<style>
body {
background-color: #f4f4f4;
font-family: Arial, sans-serif;
margin: 0; /* Menghilangkan margin default body */
padding: 0; /* Menghilangkan padding default body */
}
.title {
color: #f4f4f7;
text-align: center;
font-size: 2rem;
font-weight: bold;
margin-bottom: 10px;
}
.subtitle {
text-align: center;
font-size: 1.2rem;
color: #f4f4f7;
margin-bottom: 20px;
}
.stButton > button {
background-color: #007bff;
color: white;
padding: 10px 24px;
border-radius: 8px;
font-size: 1rem;
border: none;
cursor: pointer;
transition: 0.3s;
display: block;
margin: 0 auto;
width: 50%;
}
.stButton > button:hover {
background-color: #0056b3;
box-shadow: 0 4px 8px rgba(0,0,0,0.1);
}
.center-container {
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
}
</style>
""",
unsafe_allow_html=True,
)
def main():
st.set_page_config(page_title="Aplikasi Pendeteksi Depresi", layout="wide")
set_custom_css()
st.markdown('<div class="title">Selamat Datang di Aplikasi Pendeteksi Indikasi Depresi</div>', unsafe_allow_html=True)
st.markdown('<div class="subtitle">Aplikasi ini menganalisis ekspresi wajah dan teks untuk mendeteksi potensi indikasi depresi.</div>', unsafe_allow_html=True)
st.markdown('<div class="center-container">', unsafe_allow_html=True)
if st.button("Mulai Analisis"):
st.switch_page("pages/1_analisis_wajah.py")
st.markdown('</div>', unsafe_allow_html=True)
if __name__ == "__main__":
main()

Binary file not shown.

Binary file not shown.

BIN
model/tokenizer_Valid.pkl Normal file

Binary file not shown.

584
pages/1_analisis_wajah.py Normal file
View File

@ -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 = """
<style>
.main {
background-color: #0b0f2e;
color: white;
}
.stApp {
background-color: #0b0f2e;
}
.metric-container {
background-color: #1a2352;
padding: 15px;
border-radius: 8px;
text-align: center;
box-shadow: 0 2px 4px rgba(0,0,0,0.2);
}
.metric-label {
font-size: 14px;
opacity: 0.8;
margin-bottom: 5px;
}
.metric-value {
font-size: 20px;
font-weight: bold;
color: white;
}
.results-container {
background-color: #111c4e;
border-radius: 10px;
padding: 20px;
margin-top: 20px;
}
.emotion-score {
font-size: 24px;
font-weight: bold;
text-align: center;
margin: 10px 0;
}
.negative-score { color: #ff6b6b; }
.positive-score { color: #69db7c; }
.stButton > button {
background-color: #007bff;
color: white;
border-radius: 8px;
padding: 12px 24px;
width: 100%;
transition: background-color 0.3s ease;
font-size: 16px;
border: none;
cursor: pointer;
}
.stButton > button:hover {
background-color: #0056b3;
}
.status-indicator {
padding: 8px 16px;
border-radius: 20px;
font-size: 14px;
font-weight: 600;
display: inline-block;
margin: 15px 0;
}
.status-analyzing { background-color: #28a745; color: white; }
.status-stopped { background-color: #dc3545; color: white; }
.status-ready { background-color: #ffc107; color: black; }
.youtube-container {
background-color: #1a2352;
border-radius: 8px;
padding: 15px;
box-shadow: 0 2px 4px rgba(0,0,0,0.2);
margin-bottom: 20px;
}
.camera-container {
background-color: #1a2352;
border-radius: 8px;
padding: 15px;
box-shadow: 0 2px 4px rgba(0,0,0,0.2);
margin-bottom: 20px;
}
.control-panel {
background-color: #1a2352;
border-radius: 8px;
padding: 20px;
box-shadow: 0 2px 4px rgba(0,0,0,0.2);
}
.panel-title {
font-size: 18px;
font-weight: bold;
margin-bottom: 15px;
color: white;
}
.main-title {
font-size: 32px;
font-weight: bold;
margin-bottom: 15px;
color: white;
}
.hidden {
display: none;
}
.stImage > img {
border-radius: 8px;
}
</style>
"""
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"""
<div class="{container_class}">
<div class="metric-label">{label}</div>
<div class="metric-value">{value}</div>
</div>
"""
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'<span class="status-indicator {css_class}">{text}</span>'
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("<div class='results-container'>", 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("</div>", 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("<h1 class='main-title'>Analisis Emosi Wajah Real-time</h1>", 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'<iframe width="100%" height="350" src="{YOUTUBE_URL.format(1 if st.session_state.video_started else 0)}" '
f'frameborder="0" allowfullscreen style="border-radius: 10px;"></iframe>',
unsafe_allow_html=True
)
st.markdown('</div>', unsafe_allow_html=True)
with col2:
video_placeholder = st.empty()
st.markdown('</div>', 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('</div>', 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'<iframe width="100%" height="350" src="{YOUTUBE_URL.format(0)}" '
f'frameborder="0" allowfullscreen style="border-radius: 10px;"></iframe>',
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'<iframe width="100%" height="350" src="{YOUTUBE_URL.format(0)}" '
f'frameborder="0" allowfullscreen style="border-radius: 10px;"></iframe>',
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'<iframe width="100%" height="350" src="{YOUTUBE_URL.format(0)}" '
f'frameborder="0" allowfullscreen style="border-radius: 10px;"></iframe>',
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'<iframe width="100%" height="350" src="{YOUTUBE_URL.format(1)}" '
f'frameborder="0" allowfullscreen style="border-radius: 10px;"></iframe>',
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'<iframe width="100%" height="350" src="{YOUTUBE_URL.format(0)}" '
f'frameborder="0" allowfullscreen style="border-radius: 10px;"></iframe>',
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'<iframe width="100%" height="350" src="{YOUTUBE_URL.format(0)}" '
f'frameborder="0" allowfullscreen style="border-radius: 10px;"></iframe>',
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()

334
pages/2_jurnaling.py Normal file
View File

@ -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("""
<style>
.main-header {
color: white;
background-color: #1E1E5A;
padding: 1.5rem;
text-align: center;
border-radius: 10px 10px 0 0;
margin-bottom: 0;
font-size: 2rem;
font-weight: bold;
}
.sub-header {
color: white;
background-color: #1E1E5A;
font-size: 1rem;
padding: 0.7rem;
text-align: center;
border-radius: 0 0 10px 10px;
margin-bottom: 2rem;
letter-spacing: 1px;
}
.text-input-container {
background-color: #f7f7f7;
padding: 1rem;
border-radius: 10px;
margin-bottom: 1.5rem;
box-shadow: 0 2px 5px rgba(0,0,0,0.1);
}
.stButton > button {
background-color: #007bff;
color: white;
padding: 10px 24px;
border-radius: 8px;
font-size: 1rem;
border: none;
cursor: pointer;
transition: 0.3s;
display: block;
margin: 0 auto;
width: 100%;
}
.stButton > button:hover {
background-color: #0056b3;
box-shadow: 0 4px 8px rgba(0,0,0,0.1);
}
.result-container {
background-color: white;
border-radius: 10px;
padding: 1.5rem;
box-shadow: 0 3px 10px rgba(0,0,0,0.1);
margin-top: 2rem;
margin-bottom: 2rem;
animation: fadeIn 0.5s;
}
.custom-button-container {
margin-top: 20px;
margin-bottom: 20px;
}
@keyframes fadeIn {
0% { opacity: 0; }
100% { opacity: 1; }
}
.emotion-label {
font-weight: bold;
font-size: 1.1rem;
margin-bottom: 0.5rem;
}
.emotion-score {
margin-left: 10px;
color: #555;
}
.sentiment-label {
font-weight: bold;
font-size: 1.1rem;
margin-top: 1rem;
margin-bottom: 0.5rem;
}
.chart-title {
text-align: center;
font-weight: bold;
margin-bottom: 1rem;
color: #333;
}
.divider {
margin-top: 1.5rem;
margin-bottom: 1.5rem;
border-top: 1px solid #eee;
}
</style>
""", unsafe_allow_html=True)
st.markdown('<div class="main-header">Analisis Jurnaling</div>', unsafe_allow_html=True)
st.markdown('<div class="sub-header">TULISKAN EKSPRESIMU DENGAN KATA-KATA</div>', 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('<div class="custom-button-container">', unsafe_allow_html=True)
analyze_button = st.button("Analisis Teks", key="analyze", use_container_width=True)
st.markdown('</div>', 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'<div class="emotion-label" style="color:{color};">{emotion_name} <span class="emotion-score">{score_percent:.1f}%</span></div>',
unsafe_allow_html=True
)
st.markdown('<div class="chart-title">Top 3 Emosi</div>', 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('<div class="divider"></div>', unsafe_allow_html=True)
st.markdown(
f'<div class="sentiment-label" style="color:#4CAF50;">Positive Sentiment <span class="emotion-score">{positive_score:.1f}%</span></div>',
unsafe_allow_html=True
)
st.markdown(
f'<div class="sentiment-label" style="color:#E53935;">Negative Sentiment <span class="emotion-score">{negative_score:.1f}%</span></div>',
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('<div class="custom-button-container">', unsafe_allow_html=True)
multimodal_button = st.button("Lihat Hasil Multimodal", key="multimodal", use_container_width=True)
st.markdown('</div>', unsafe_allow_html=True)
if multimodal_button:
st.switch_page("pages/3_hasil.py")

340
pages/3_hasil.py Normal file
View File

@ -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("""
<style>
.main-header {
color: white;
background-color: #1E1E5A;
padding: 1.5rem;
text-align: center;
border-radius: 10px 10px 0 0;
margin-bottom: 2rem;
font-size: 2rem;
font-weight: bold;
}
.section-header {
color: #FFFFFF;
font-weight: bold;
margin-bottom: 20px;
border-bottom: 2px solid #3498db;
padding-bottom: 10px;
}
.positive-score {
color: #2ecc71;
font-weight: bold;
}
.negative-score {
color: #e74c3c;
font-weight: bold;
}
.stButton > button {
background-color: #007bff;
color: white;
padding: 10px 24px;
border-radius: 8px;
font-size: 1rem;
border: none;
cursor: pointer;
transition: 0.3s;
display: block;
margin: 0 auto;
width: 100%;
}
.stButton > button:hover {
background-color: #0056b3;
box-shadow: 0 4px 8px rgba(0,0,0,0.1);
}
.custom-button-container {
margin-top: 20px;
margin-bottom: 20px;
}
.divider {
margin-top: 1.5rem;
margin-bottom: 1.5rem;
border-top: 1px solid #eee;
}
</style>
""", unsafe_allow_html=True)
st.markdown('<div class="main-header">📊 Laporan Analisis Multimodal</div>', 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('<div class="custom-button-container">', 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('</div>', 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('<div class="custom-button-container">', 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('</div>', 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('<h2 class="section-header">📝 Ringkasan Umum</h2>', 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: <span class='positive-score'>{avg_positive_score:.1f}%</span>", unsafe_allow_html=True)
st.write(f"🔴 Rata-rata Emosi Negatif: <span class='negative-score'>{avg_negative_score:.1f}%</span>", 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('</div>', unsafe_allow_html=True)
st.markdown('<h2 class="section-header">😀 Analisis Ekspresi Wajah</h2>', 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: <span class='positive-score'>{face_positive_score:.1f}%</span>", unsafe_allow_html=True)
st.write(f"🔴 Skor Negatif: <span class='negative-score'>{face_negative_score:.1f}%</span>", 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('</div>', unsafe_allow_html=True)
st.markdown('<h2 class="section-header">📝 Analisis Teks Jurnaling</h2>', 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: <span class='positive-score'>{text_positive_score:.1f}%</span>", unsafe_allow_html=True)
st.write(f"🔴 Skor Negatif: <span class='negative-score'>{text_negative_score:.1f}%</span>", unsafe_allow_html=True)
st.write("### Teks Jurnal")
st.write(text_result['text'])
st.markdown('</div>', unsafe_allow_html=True)
st.markdown('<h2 class="section-header">🔍 Kesimpulan Akhir</h2>', 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('<div class="custom-button-container">', 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('</div>', unsafe_allow_html=True)

10
requirements.txt Normal file
View File

@ -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