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 ──────────────────────────────────────────────────────────────── @app.get("/") def root(): return {"message": "MoodRead API v2 aktif 🎉", "docs": "/docs"} @app.get("/health") def health(): return {"status": "ok"} @app.post("/detect-emotion") 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"], } @app.get("/emotions") def list_emotions(): return {"emotions": ["happy", "joy", "love", "sadness", "anger", "fear", "surprise", "neutral"]} @app.get("/genres") def list_genres(): if books_df.empty: return {"genres": []} return {"genres": books_df["genre"].unique().tolist()}