Spaces:
Sleeping
Sleeping
| from fastapi import FastAPI, HTTPException | |
| from fastapi.middleware.cors import CORSMiddleware | |
| from pydantic import BaseModel | |
| from transformers import pipeline | |
| from sentence_transformers import SentenceTransformer | |
| from sklearn.metrics.pairwise import cosine_similarity | |
| import pandas as pd | |
| import os | |
| app = FastAPI(title="MoodRead API", version="2.0.0") | |
| app.add_middleware( | |
| CORSMiddleware, | |
| allow_origins=["*"], | |
| allow_methods=["*"], | |
| allow_headers=["*"], | |
| ) | |
| # ββ 1. Load model emosi RoBERTa ββββββββββββββββββββββββββββββββββββββββββββββ | |
| MODEL_NAME = "StevenLimcorn/indonesian-roberta-base-emotion-classifier" | |
| print("Memuat model emosi RoBERTa...") | |
| emotion_pipeline = pipeline( | |
| task="text-classification", | |
| model=MODEL_NAME, | |
| device=-1 # CPU | |
| ) | |
| print("β Model emosi siap!") | |
| # ββ 2. Load Sentence Transformer (Dania) βββββββββββββββββββββββββββββββββββββ | |
| ST_MODEL = "paraphrase-multilingual-MiniLM-L12-v2" | |
| print("Memuat Sentence Transformer...") | |
| sentence_model = SentenceTransformer(ST_MODEL) | |
| print("β Sentence Transformer siap!") | |
| # ββ 3. Load dataset buku βββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| DATASET_PATH = "books_clean.csv" | |
| if os.path.exists(DATASET_PATH): | |
| books_df = pd.read_csv(DATASET_PATH, encoding="utf-8") | |
| books_df["genre"] = books_df["genre"].str.lower().str.strip() | |
| print(f"β Dataset buku dimuat: {len(books_df)} buku") | |
| else: | |
| books_df = pd.DataFrame() | |
| print("β οΈ books_clean.csv tidak ditemukan!") | |
| # ββ Schema βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| class TextInput(BaseModel): | |
| text: str | |
| top_k: int = 5 | |
| # ββ Fungsi Yuri: Hybrid Contextual Genre Mapping βββββββββββββββββββββββββββββ | |
| def get_recommended_genres(emotion: str, text_user: str) -> list[str]: | |
| """ | |
| Logika hybrid: kombinasi hasil model AI + aturan kata kunci kontekstual. | |
| """ | |
| teks_bersih = text_user.lower().strip() | |
| emosi_bersih = emotion.lower().strip() | |
| kata_kunci_karir = [ | |
| 'presentasi', 'ujian', 'sidang', 'kerja', 'tugas', 'dosen', | |
| 'kuliah', 'gagal', 'bos', 'kantor', 'capek', 'skripsi', 'kelulusan' | |
| ] | |
| kata_kunci_sosial = [ | |
| 'sahabat', 'cinta', 'putus', 'dikhianati', 'pacar', 'mantan', | |
| 'teman', 'kucing', 'kehilangan', 'ditipu', 'rekan bisnis' | |
| ] | |
| if emosi_bersih in ['happy', 'joy']: | |
| return ['pengembangan_diri', 'novel'] | |
| elif emosi_bersih == 'love': | |
| return ['romance'] | |
| elif emosi_bersih == 'fear': | |
| if any(k in teks_bersih for k in kata_kunci_karir): | |
| return ['pengembangan_diri'] | |
| else: | |
| return ['novel', 'horor'] | |
| elif emosi_bersih == 'sadness': | |
| if any(k in teks_bersih for k in ['marah', 'kesal', 'ditipu', 'ketidakadilan', 'geram']): | |
| return ['novel', 'thriller'] | |
| elif any(k in teks_bersih for k in kata_kunci_sosial): | |
| return ['romance'] | |
| elif any(k in teks_bersih for k in kata_kunci_karir): | |
| return ['pengembangan_diri'] | |
| else: | |
| return ['fiksi_sastra'] | |
| elif emosi_bersih == 'anger': | |
| if any(k in teks_bersih for k in kata_kunci_karir): | |
| return ['cerpen'] | |
| else: | |
| return ['novel', 'thriller'] | |
| return ['novel', 'fiksi_sastra'] | |
| # ββ Fungsi Yuri: Deteksi emosi mentah ββββββββββββββββββββββββββββββββββββββββ | |
| def detect_emotion(text: str, top_k: int = 3) -> dict: | |
| if not text.strip(): | |
| return { | |
| 'primary_emotion': 'neutral', | |
| 'primary_score': 0.0, | |
| 'top_emotions': [], | |
| } | |
| raw = emotion_pipeline(text, top_k=top_k) | |
| return { | |
| 'primary_emotion': raw[0]['label'].lower(), | |
| 'primary_score': round(raw[0]['score'], 4), | |
| 'top_emotions': [ | |
| {'emotion': r['label'].lower(), 'score': round(r['score'], 4)} | |
| for r in raw | |
| ], | |
| } | |
| # ββ Fungsi Yuri: Full pipeline emosi + hybrid genre ββββββββββββββββββββββββββ | |
| def full_emotion_pipeline(text: str) -> dict: | |
| analisis = detect_emotion(text) | |
| genres = get_recommended_genres(analisis['primary_emotion'], text) | |
| return { | |
| 'emotion': analisis['primary_emotion'], | |
| 'confidence': analisis['primary_score'], | |
| 'recommended_genres': genres, | |
| 'all_emotions': analisis['top_emotions'], | |
| } | |
| # ββ Fungsi Dania: Rekomendasi buku via Sentence Transformer ββββββββββββββββββ | |
| def get_book_recommendations(user_text: str, target_genre: str, top_k: int = 5) -> list[dict]: | |
| if books_df.empty: | |
| return [] | |
| df_filtered = books_df[ | |
| books_df['genre'].str.contains(target_genre, case=False, na=False) | |
| ].copy() | |
| if df_filtered.empty: | |
| return [] | |
| user_vector = sentence_model.encode([user_text]) | |
| title_vectors = sentence_model.encode(df_filtered['judul'].tolist()) | |
| similarities = cosine_similarity(user_vector, title_vectors)[0] | |
| df_filtered['similarity_score'] = similarities | |
| top_books = ( | |
| df_filtered | |
| .sort_values('similarity_score', ascending=False) | |
| .head(top_k) | |
| ) | |
| return top_books.drop(columns=['similarity_score']).to_dict('records') | |
| # ββ Fungsi utama: moodread_pipeline ββββββββββββββββββββββββββββββββββββββββββ | |
| def moodread_pipeline(teks_curhat: str, top_k: int = 5) -> dict: | |
| """ | |
| Pipeline lengkap end-to-end: | |
| Tahap 1 β Deteksi emosi + hybrid genre mapping (Yuri) | |
| Tahap 2 β Rekomendasi buku via Sentence Transformer (Dania) | |
| """ | |
| # Tahap 1: emosi & genre | |
| hasil_emosi = full_emotion_pipeline(teks_curhat) | |
| genre_utama = hasil_emosi['recommended_genres'][0] | |
| # Tahap 2: rekomendasi buku semantik | |
| buku = get_book_recommendations(teks_curhat, genre_utama, top_k=top_k) | |
| return { | |
| 'emotion': hasil_emosi['emotion'], | |
| 'confidence': hasil_emosi['confidence'], | |
| 'all_emotions': hasil_emosi['all_emotions'], | |
| 'recommended_genres': hasil_emosi['recommended_genres'], | |
| 'book_recommendations': buku, | |
| } | |
| # ββ Endpoints ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| def root(): | |
| return {"message": "MoodRead API v2 aktif π", "docs": "/docs"} | |
| def health(): | |
| return {"status": "ok"} | |
| def endpoint_detect(payload: TextInput): | |
| if not payload.text.strip(): | |
| raise HTTPException(status_code=400, detail="Teks tidak boleh kosong.") | |
| # Panggil moodread_pipeline sebagai entry point utama | |
| hasil = moodread_pipeline(payload.text, top_k=payload.top_k) | |
| return { | |
| "primary_emotion": hasil["emotion"], | |
| "primary_score": hasil["confidence"], | |
| "top_emotions": hasil["all_emotions"], | |
| "recommended_genres": hasil["recommended_genres"], | |
| "book_recommendations": hasil["book_recommendations"], | |
| } | |
| def list_emotions(): | |
| return {"emotions": ["happy", "joy", "love", "sadness", "anger", "fear", "surprise", "neutral"]} | |
| def list_genres(): | |
| if books_df.empty: | |
| return {"genres": []} | |
| return {"genres": books_df["genre"].unique().tolist()} |