Spaces:
Running
Running
Commit ·
599f4e5
1
Parent(s): 60e8c0a
feat: add emotion, framing, fake-score, opinion-fact endpoints
Browse files- app/analyzers/emotion.py +46 -0
- app/analyzers/fakescore.py +83 -0
- app/analyzers/framing.py +47 -0
- app/analyzers/opinionfact.py +68 -0
- app/main.py +31 -8
- app/schemas.py +70 -3
app/analyzers/emotion.py
ADDED
|
@@ -0,0 +1,46 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
Emotion detection granular.
|
| 3 |
+
Deteksi emosi: anger, fear, joy, sadness, surprise, disgust, trust, anticipation.
|
| 4 |
+
"""
|
| 5 |
+
from typing import List, Dict
|
| 6 |
+
import re
|
| 7 |
+
|
| 8 |
+
# Lexicon emosi Bahasa Indonesia (extensible)
|
| 9 |
+
EMOTION_LEXICON: Dict[str, List[str]] = {
|
| 10 |
+
"anger": ["marah", "murka", "geram", "berang", "emosi", "kesal", "jengkel", "protes", "demo", "kecam", "tolak", "rusuh", "amuk"],
|
| 11 |
+
"fear": ["takut", "khawatir", "cemas", "panik", "ancaman", "bahaya", "waspada", "darurat", "teror", "ngeri", "was-was", "resah"],
|
| 12 |
+
"joy": ["senang", "gembira", "bahagia", "sukses", "juara", "menang", "prestasi", "bangga", "puas", "optimis", "harapan", "selebrasi"],
|
| 13 |
+
"sadness": ["sedih", "duka", "meninggal", "tewas", "korban", "tragis", "pilu", "menderita", "kehilangan", "bela sungkawa", "nestapa"],
|
| 14 |
+
"surprise": ["kejutan", "mendadak", "tiba-tiba", "mengejutkan", "tak terduga", "heboh", "viral", "gempar", "terungkap", "ternyata"],
|
| 15 |
+
"disgust": ["jijik", "muak", "korupsi", "skandal", "busuk", "curang", "manipulasi", "penipuan", "menjijikkan", "tercela"],
|
| 16 |
+
"trust": ["percaya", "yakin", "aman", "terpercaya", "jaminan", "komitmen", "konsisten", "transparan", "akuntabel", "integritas"],
|
| 17 |
+
"anticipation": ["harap", "rencana", "siap", "target", "proyeksi", "prediksi", "antisipasi", "agenda", "akan", "berencana"],
|
| 18 |
+
}
|
| 19 |
+
|
| 20 |
+
|
| 21 |
+
def detect_emotions(text: str) -> Dict[str, float]:
|
| 22 |
+
text_lower = text.lower()
|
| 23 |
+
tokens = re.findall(r'\b\w+\b', text_lower)
|
| 24 |
+
token_set = set(tokens)
|
| 25 |
+
total_tokens = max(len(tokens), 1)
|
| 26 |
+
|
| 27 |
+
scores = {}
|
| 28 |
+
for emotion, keywords in EMOTION_LEXICON.items():
|
| 29 |
+
hits = sum(1 for kw in keywords if kw in text_lower or kw in token_set)
|
| 30 |
+
scores[emotion] = round(hits / total_tokens * 10, 3) # normalized
|
| 31 |
+
|
| 32 |
+
return scores
|
| 33 |
+
|
| 34 |
+
|
| 35 |
+
def analyze_batch(items: List) -> List[Dict]:
|
| 36 |
+
results = []
|
| 37 |
+
for item in items:
|
| 38 |
+
scores = detect_emotions(item.text)
|
| 39 |
+
dominant = max(scores, key=scores.get) if any(v > 0 for v in scores.values()) else "neutral"
|
| 40 |
+
results.append({
|
| 41 |
+
"id": item.id,
|
| 42 |
+
"emotions": scores,
|
| 43 |
+
"dominant_emotion": dominant,
|
| 44 |
+
"dominant_score": scores.get(dominant, 0),
|
| 45 |
+
})
|
| 46 |
+
return results
|
app/analyzers/fakescore.py
ADDED
|
@@ -0,0 +1,83 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
Fake news score (heuristik).
|
| 3 |
+
Skor 0-100 indikasi potensi hoax/misinformasi berdasarkan pola teks.
|
| 4 |
+
"""
|
| 5 |
+
from typing import List, Dict
|
| 6 |
+
import re
|
| 7 |
+
|
| 8 |
+
# Indikator clickbait/hoax
|
| 9 |
+
HOAX_INDICATORS = [
|
| 10 |
+
"terungkap", "ternyata", "rahasia", "viral", "heboh", "bikin",
|
| 11 |
+
"terbongkar", "fakta mencengangkan", "anda harus tahu",
|
| 12 |
+
"jangan sampai", "ini dia", "wow", "gempar",
|
| 13 |
+
]
|
| 14 |
+
|
| 15 |
+
CREDIBILITY_MARKERS = [
|
| 16 |
+
"menurut", "berdasarkan", "data", "riset", "penelitian",
|
| 17 |
+
"sumber", "narasumber", "konfirmasi", "verifikasi", "resmi",
|
| 18 |
+
]
|
| 19 |
+
|
| 20 |
+
|
| 21 |
+
def calculate_fake_score(text: str, title: str = "") -> Dict:
|
| 22 |
+
text_lower = (title + " " + text).lower()
|
| 23 |
+
score = 0
|
| 24 |
+
reasons = []
|
| 25 |
+
|
| 26 |
+
# 1. Kata-kata pemicu hoax di judul
|
| 27 |
+
title_lower = title.lower()
|
| 28 |
+
hoax_hits = sum(1 for w in HOAX_INDICATORS if w in title_lower)
|
| 29 |
+
if hoax_hits > 0:
|
| 30 |
+
score += min(30, hoax_hits * 15)
|
| 31 |
+
reasons.append(f"{hoax_hits} kata pemicu hoax")
|
| 32 |
+
|
| 33 |
+
# 2. Tanda seru/tanya berlebihan
|
| 34 |
+
excl = title.count("!") + title.count("?")
|
| 35 |
+
if excl > 1:
|
| 36 |
+
score += min(15, excl * 7)
|
| 37 |
+
reasons.append(f"{excl} tanda seru/tanya")
|
| 38 |
+
|
| 39 |
+
# 3. ALL CAPS
|
| 40 |
+
caps = len(re.findall(r'\b[A-Z]{3,}\b', title))
|
| 41 |
+
if caps > 1:
|
| 42 |
+
score += min(15, caps * 7)
|
| 43 |
+
reasons.append(f"{caps} kata KAPITAL")
|
| 44 |
+
|
| 45 |
+
# 4. Tidak ada sumber/narasumber
|
| 46 |
+
cred_hits = sum(1 for w in CREDIBILITY_MARKERS if w in text_lower)
|
| 47 |
+
if cred_hits == 0:
|
| 48 |
+
score += 20
|
| 49 |
+
reasons.append("tidak ada sumber terverifikasi")
|
| 50 |
+
elif cred_hits >= 3:
|
| 51 |
+
score -= 10
|
| 52 |
+
reasons.append(f"{cred_hits} marker kredibilitas")
|
| 53 |
+
|
| 54 |
+
# 5. Teks sangat pendek (kurang substansi)
|
| 55 |
+
word_count = len(text.split())
|
| 56 |
+
if word_count < 50:
|
| 57 |
+
score += 15
|
| 58 |
+
reasons.append("konten sangat pendek")
|
| 59 |
+
|
| 60 |
+
# 6. Banyak klaim tanpa bukti (kalimat deklaratif tanpa attributor)
|
| 61 |
+
sentences = re.split(r'[.!?]', text)
|
| 62 |
+
declarative = sum(1 for s in sentences if len(s.strip()) > 20 and not any(m in s.lower() for m in CREDIBILITY_MARKERS))
|
| 63 |
+
ratio = declarative / max(len(sentences), 1)
|
| 64 |
+
if ratio > 0.8:
|
| 65 |
+
score += 10
|
| 66 |
+
reasons.append("mayoritas klaim tanpa atribusi")
|
| 67 |
+
|
| 68 |
+
score = max(0, min(100, score))
|
| 69 |
+
level = "tinggi" if score >= 60 else "sedang" if score >= 30 else "rendah"
|
| 70 |
+
|
| 71 |
+
return {"score": score, "level": level, "reasons": reasons}
|
| 72 |
+
|
| 73 |
+
|
| 74 |
+
def analyze_batch(items: List) -> List[Dict]:
|
| 75 |
+
results = []
|
| 76 |
+
for item in items:
|
| 77 |
+
# Pisah title dari text (asumsi: kalimat pertama = title)
|
| 78 |
+
parts = item.text.split(". ", 1)
|
| 79 |
+
title = parts[0] if len(parts) > 1 else ""
|
| 80 |
+
content = parts[1] if len(parts) > 1 else item.text
|
| 81 |
+
result = calculate_fake_score(content, title)
|
| 82 |
+
results.append({"id": item.id, **result})
|
| 83 |
+
return results
|
app/analyzers/framing.py
ADDED
|
@@ -0,0 +1,47 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
Narrative framing detection.
|
| 3 |
+
Identifikasi angle/frame yang digunakan media dalam meliput suatu berita.
|
| 4 |
+
"""
|
| 5 |
+
from typing import List, Dict
|
| 6 |
+
import re
|
| 7 |
+
|
| 8 |
+
# Frame categories berdasarkan teori framing media
|
| 9 |
+
FRAME_PATTERNS: Dict[str, List[str]] = {
|
| 10 |
+
"conflict": ["konflik", "versus", "lawan", "sengketa", "pertentangan", "tuduh", "serang", "bantah", "polemik", "debat", "perang"],
|
| 11 |
+
"human_interest": ["korban", "keluarga", "anak", "ibu", "kisah", "cerita", "perjuangan", "nasib", "derita", "harapan hidup"],
|
| 12 |
+
"economic": ["rupiah", "inflasi", "harga", "ekonomi", "bisnis", "investasi", "saham", "anggaran", "pajak", "untung", "rugi", "pasar"],
|
| 13 |
+
"morality": ["etika", "moral", "haram", "halal", "dosa", "adil", "korupsi", "integritas", "tanggung jawab", "amanah"],
|
| 14 |
+
"attribution": ["pemerintah", "presiden", "menteri", "DPR", "partai", "kebijakan", "regulasi", "aturan", "instruksi", "perintah"],
|
| 15 |
+
"solution": ["solusi", "program", "langkah", "upaya", "strategi", "inovasi", "rencana", "pembangunan", "perbaikan", "reformasi"],
|
| 16 |
+
"sensational": ["viral", "heboh", "gempar", "terungkap", "rahasia", "skandal", "mencengangkan", "bikin", "wow", "ternyata"],
|
| 17 |
+
}
|
| 18 |
+
|
| 19 |
+
|
| 20 |
+
def detect_framing(text: str) -> Dict:
|
| 21 |
+
text_lower = text.lower()
|
| 22 |
+
scores = {}
|
| 23 |
+
|
| 24 |
+
for frame, keywords in FRAME_PATTERNS.items():
|
| 25 |
+
hits = sum(1 for kw in keywords if kw in text_lower)
|
| 26 |
+
scores[frame] = hits
|
| 27 |
+
|
| 28 |
+
total = sum(scores.values())
|
| 29 |
+
if total == 0:
|
| 30 |
+
return {"frames": {k: 0.0 for k in FRAME_PATTERNS}, "dominant_frame": "neutral", "score": 0.0}
|
| 31 |
+
|
| 32 |
+
normalized = {k: round(v / total, 3) for k, v in scores.items()}
|
| 33 |
+
dominant = max(scores, key=scores.get)
|
| 34 |
+
|
| 35 |
+
return {
|
| 36 |
+
"frames": normalized,
|
| 37 |
+
"dominant_frame": dominant,
|
| 38 |
+
"score": round(scores[dominant] / total, 3),
|
| 39 |
+
}
|
| 40 |
+
|
| 41 |
+
|
| 42 |
+
def analyze_batch(items: List) -> List[Dict]:
|
| 43 |
+
results = []
|
| 44 |
+
for item in items:
|
| 45 |
+
framing = detect_framing(item.text)
|
| 46 |
+
results.append({"id": item.id, **framing})
|
| 47 |
+
return results
|
app/analyzers/opinionfact.py
ADDED
|
@@ -0,0 +1,68 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
Opinion vs Fact classifier.
|
| 3 |
+
Klasifikasi apakah artikel bersifat opini/editorial atau berita faktual.
|
| 4 |
+
"""
|
| 5 |
+
from typing import List, Dict
|
| 6 |
+
import re
|
| 7 |
+
|
| 8 |
+
OPINION_MARKERS = [
|
| 9 |
+
"menurut saya", "saya rasa", "seharusnya", "sebaiknya", "idealnya",
|
| 10 |
+
"opini", "editorial", "kolom", "perspektif", "pandangan",
|
| 11 |
+
"hemat saya", "saya pikir", "kita harus", "perlu diakui",
|
| 12 |
+
"jelas bahwa", "tidak bisa dipungkiri", "menariknya",
|
| 13 |
+
]
|
| 14 |
+
|
| 15 |
+
FACT_MARKERS = [
|
| 16 |
+
"berdasarkan data", "menurut", "kata", "ujar", "ungkap",
|
| 17 |
+
"dilaporkan", "tercatat", "statistik", "survei", "rilis",
|
| 18 |
+
"laporan", "konferensi pers", "siaran pers", "resmi",
|
| 19 |
+
"diumumkan", "ditetapkan", "diresmikan",
|
| 20 |
+
]
|
| 21 |
+
|
| 22 |
+
SUBJECTIVE_WORDS = [
|
| 23 |
+
"terbaik", "terburuk", "luar biasa", "mengecewakan", "menyedihkan",
|
| 24 |
+
"mengesankan", "fantastis", "mengerikan", "sempurna", "parah",
|
| 25 |
+
"indah", "jelek", "hebat", "bodoh", "cerdas",
|
| 26 |
+
]
|
| 27 |
+
|
| 28 |
+
|
| 29 |
+
def classify(text: str) -> Dict:
|
| 30 |
+
text_lower = text.lower()
|
| 31 |
+
|
| 32 |
+
opinion_hits = sum(1 for m in OPINION_MARKERS if m in text_lower)
|
| 33 |
+
fact_hits = sum(1 for m in FACT_MARKERS if m in text_lower)
|
| 34 |
+
subjective_hits = sum(1 for w in SUBJECTIVE_WORDS if w in text_lower)
|
| 35 |
+
|
| 36 |
+
opinion_score = opinion_hits * 3 + subjective_hits * 2
|
| 37 |
+
fact_score = fact_hits * 3
|
| 38 |
+
|
| 39 |
+
total = opinion_score + fact_score
|
| 40 |
+
if total == 0:
|
| 41 |
+
return {"classification": "unknown", "opinion_pct": 50, "fact_pct": 50, "confidence": 0}
|
| 42 |
+
|
| 43 |
+
opinion_pct = round((opinion_score / total) * 100)
|
| 44 |
+
fact_pct = 100 - opinion_pct
|
| 45 |
+
|
| 46 |
+
if opinion_pct > 60:
|
| 47 |
+
classification = "opinion"
|
| 48 |
+
elif fact_pct > 60:
|
| 49 |
+
classification = "fact"
|
| 50 |
+
else:
|
| 51 |
+
classification = "mixed"
|
| 52 |
+
|
| 53 |
+
confidence = round(abs(opinion_pct - 50) / 50, 2)
|
| 54 |
+
|
| 55 |
+
return {
|
| 56 |
+
"classification": classification,
|
| 57 |
+
"opinion_pct": opinion_pct,
|
| 58 |
+
"fact_pct": fact_pct,
|
| 59 |
+
"confidence": confidence,
|
| 60 |
+
}
|
| 61 |
+
|
| 62 |
+
|
| 63 |
+
def analyze_batch(items: List) -> List[Dict]:
|
| 64 |
+
results = []
|
| 65 |
+
for item in items:
|
| 66 |
+
result = classify(item.text)
|
| 67 |
+
results.append({"id": item.id, **result})
|
| 68 |
+
return results
|
app/main.py
CHANGED
|
@@ -1,13 +1,10 @@
|
|
| 1 |
"""
|
| 2 |
BrainWatches Python Analysis Service
|
| 3 |
====================================
|
| 4 |
-
FastAPI microservice untuk analisis NLP lanjutan
|
| 5 |
-
topic modeling, summarization, semantic similarity).
|
| 6 |
-
|
| 7 |
-
Dipanggil oleh Laravel via HTTP. Otentikasi via header X-Service-Token.
|
| 8 |
|
| 9 |
Jalankan:
|
| 10 |
-
uvicorn app.main:app --host 0.0.0.0 --port
|
| 11 |
"""
|
| 12 |
from fastapi import FastAPI, Header, HTTPException, Depends
|
| 13 |
from fastapi.middleware.cors import CORSMiddleware
|
|
@@ -18,10 +15,12 @@ from app.schemas import (
|
|
| 18 |
SummarizeRequest, SummarizeResponse,
|
| 19 |
TopicRequest, TopicResponse,
|
| 20 |
SimilarityRequest, SimilarityResponse,
|
|
|
|
|
|
|
| 21 |
)
|
| 22 |
-
from app.analyzers import sentiment, topics, summary, similarity
|
| 23 |
|
| 24 |
-
app = FastAPI(title="BrainWatches Analysis Service", version="1.
|
| 25 |
|
| 26 |
app.add_middleware(
|
| 27 |
CORSMiddleware,
|
|
@@ -39,7 +38,7 @@ def verify_token(x_service_token: str = Header(default="")):
|
|
| 39 |
|
| 40 |
@app.get("/health")
|
| 41 |
def health():
|
| 42 |
-
return {"status": "ok", "model_mode": settings.MODEL_MODE}
|
| 43 |
|
| 44 |
|
| 45 |
@app.post("/sentiment", response_model=SentimentResponse, dependencies=[Depends(verify_token)])
|
|
@@ -65,6 +64,30 @@ def similarity_endpoint(req: SimilarityRequest):
|
|
| 65 |
return {"pairs": pairs}
|
| 66 |
|
| 67 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 68 |
if __name__ == "__main__":
|
| 69 |
import uvicorn
|
| 70 |
uvicorn.run("app.main:app", host=settings.HOST, port=settings.PORT, reload=True)
|
|
|
|
| 1 |
"""
|
| 2 |
BrainWatches Python Analysis Service
|
| 3 |
====================================
|
| 4 |
+
FastAPI microservice untuk analisis NLP lanjutan.
|
|
|
|
|
|
|
|
|
|
| 5 |
|
| 6 |
Jalankan:
|
| 7 |
+
uvicorn app.main:app --host 0.0.0.0 --port 7860
|
| 8 |
"""
|
| 9 |
from fastapi import FastAPI, Header, HTTPException, Depends
|
| 10 |
from fastapi.middleware.cors import CORSMiddleware
|
|
|
|
| 15 |
SummarizeRequest, SummarizeResponse,
|
| 16 |
TopicRequest, TopicResponse,
|
| 17 |
SimilarityRequest, SimilarityResponse,
|
| 18 |
+
TextItemsRequest, EmotionResponse,
|
| 19 |
+
FramingResponse, FakeScoreResponse, OpinionFactResponse,
|
| 20 |
)
|
| 21 |
+
from app.analyzers import sentiment, topics, summary, similarity, emotion, framing, fakescore, opinionfact
|
| 22 |
|
| 23 |
+
app = FastAPI(title="BrainWatches Analysis Service", version="1.1.0")
|
| 24 |
|
| 25 |
app.add_middleware(
|
| 26 |
CORSMiddleware,
|
|
|
|
| 38 |
|
| 39 |
@app.get("/health")
|
| 40 |
def health():
|
| 41 |
+
return {"status": "ok", "model_mode": settings.MODEL_MODE, "version": "1.1.0"}
|
| 42 |
|
| 43 |
|
| 44 |
@app.post("/sentiment", response_model=SentimentResponse, dependencies=[Depends(verify_token)])
|
|
|
|
| 64 |
return {"pairs": pairs}
|
| 65 |
|
| 66 |
|
| 67 |
+
@app.post("/emotion", response_model=EmotionResponse, dependencies=[Depends(verify_token)])
|
| 68 |
+
def emotion_endpoint(req: TextItemsRequest):
|
| 69 |
+
results = emotion.analyze_batch(req.items)
|
| 70 |
+
return {"results": results}
|
| 71 |
+
|
| 72 |
+
|
| 73 |
+
@app.post("/framing", response_model=FramingResponse, dependencies=[Depends(verify_token)])
|
| 74 |
+
def framing_endpoint(req: TextItemsRequest):
|
| 75 |
+
results = framing.analyze_batch(req.items)
|
| 76 |
+
return {"results": results}
|
| 77 |
+
|
| 78 |
+
|
| 79 |
+
@app.post("/fake-score", response_model=FakeScoreResponse, dependencies=[Depends(verify_token)])
|
| 80 |
+
def fake_score_endpoint(req: TextItemsRequest):
|
| 81 |
+
results = fakescore.analyze_batch(req.items)
|
| 82 |
+
return {"results": results}
|
| 83 |
+
|
| 84 |
+
|
| 85 |
+
@app.post("/opinion-fact", response_model=OpinionFactResponse, dependencies=[Depends(verify_token)])
|
| 86 |
+
def opinion_fact_endpoint(req: TextItemsRequest):
|
| 87 |
+
results = opinionfact.analyze_batch(req.items)
|
| 88 |
+
return {"results": results}
|
| 89 |
+
|
| 90 |
+
|
| 91 |
if __name__ == "__main__":
|
| 92 |
import uvicorn
|
| 93 |
uvicorn.run("app.main:app", host=settings.HOST, port=settings.PORT, reload=True)
|
app/schemas.py
CHANGED
|
@@ -1,5 +1,5 @@
|
|
| 1 |
"""Pydantic schemas untuk request/response."""
|
| 2 |
-
from typing import List, Optional
|
| 3 |
from pydantic import BaseModel
|
| 4 |
|
| 5 |
|
|
@@ -8,14 +8,22 @@ class TextItem(BaseModel):
|
|
| 8 |
text: str
|
| 9 |
|
| 10 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 11 |
class SentimentRequest(BaseModel):
|
| 12 |
items: List[TextItem]
|
| 13 |
|
| 14 |
|
| 15 |
class SentimentResult(BaseModel):
|
| 16 |
id: int
|
| 17 |
-
sentiment: str
|
| 18 |
-
score: float
|
| 19 |
confidence: float
|
| 20 |
|
| 21 |
|
|
@@ -24,6 +32,8 @@ class SentimentResponse(BaseModel):
|
|
| 24 |
model_mode: str
|
| 25 |
|
| 26 |
|
|
|
|
|
|
|
| 27 |
class SummarizeRequest(BaseModel):
|
| 28 |
text: str
|
| 29 |
sentences: int = 3
|
|
@@ -34,6 +44,8 @@ class SummarizeResponse(BaseModel):
|
|
| 34 |
sentences: List[str]
|
| 35 |
|
| 36 |
|
|
|
|
|
|
|
| 37 |
class TopicRequest(BaseModel):
|
| 38 |
items: List[TextItem]
|
| 39 |
num_topics: int = 8
|
|
@@ -52,6 +64,8 @@ class TopicResponse(BaseModel):
|
|
| 52 |
model_mode: str
|
| 53 |
|
| 54 |
|
|
|
|
|
|
|
| 55 |
class SimilarityRequest(BaseModel):
|
| 56 |
items: List[TextItem]
|
| 57 |
threshold: float = 0.3
|
|
@@ -65,3 +79,56 @@ class SimilarityPair(BaseModel):
|
|
| 65 |
|
| 66 |
class SimilarityResponse(BaseModel):
|
| 67 |
pairs: List[SimilarityPair]
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
"""Pydantic schemas untuk request/response."""
|
| 2 |
+
from typing import List, Dict, Optional
|
| 3 |
from pydantic import BaseModel
|
| 4 |
|
| 5 |
|
|
|
|
| 8 |
text: str
|
| 9 |
|
| 10 |
|
| 11 |
+
# === Shared request ===
|
| 12 |
+
|
| 13 |
+
class TextItemsRequest(BaseModel):
|
| 14 |
+
items: List[TextItem]
|
| 15 |
+
|
| 16 |
+
|
| 17 |
+
# === Sentiment ===
|
| 18 |
+
|
| 19 |
class SentimentRequest(BaseModel):
|
| 20 |
items: List[TextItem]
|
| 21 |
|
| 22 |
|
| 23 |
class SentimentResult(BaseModel):
|
| 24 |
id: int
|
| 25 |
+
sentiment: str
|
| 26 |
+
score: float
|
| 27 |
confidence: float
|
| 28 |
|
| 29 |
|
|
|
|
| 32 |
model_mode: str
|
| 33 |
|
| 34 |
|
| 35 |
+
# === Summarize ===
|
| 36 |
+
|
| 37 |
class SummarizeRequest(BaseModel):
|
| 38 |
text: str
|
| 39 |
sentences: int = 3
|
|
|
|
| 44 |
sentences: List[str]
|
| 45 |
|
| 46 |
|
| 47 |
+
# === Topics ===
|
| 48 |
+
|
| 49 |
class TopicRequest(BaseModel):
|
| 50 |
items: List[TextItem]
|
| 51 |
num_topics: int = 8
|
|
|
|
| 64 |
model_mode: str
|
| 65 |
|
| 66 |
|
| 67 |
+
# === Similarity ===
|
| 68 |
+
|
| 69 |
class SimilarityRequest(BaseModel):
|
| 70 |
items: List[TextItem]
|
| 71 |
threshold: float = 0.3
|
|
|
|
| 79 |
|
| 80 |
class SimilarityResponse(BaseModel):
|
| 81 |
pairs: List[SimilarityPair]
|
| 82 |
+
|
| 83 |
+
|
| 84 |
+
# === Emotion ===
|
| 85 |
+
|
| 86 |
+
class EmotionResult(BaseModel):
|
| 87 |
+
id: int
|
| 88 |
+
emotions: Dict[str, float]
|
| 89 |
+
dominant_emotion: str
|
| 90 |
+
dominant_score: float
|
| 91 |
+
|
| 92 |
+
|
| 93 |
+
class EmotionResponse(BaseModel):
|
| 94 |
+
results: List[EmotionResult]
|
| 95 |
+
|
| 96 |
+
|
| 97 |
+
# === Framing ===
|
| 98 |
+
|
| 99 |
+
class FramingResult(BaseModel):
|
| 100 |
+
id: int
|
| 101 |
+
frames: Dict[str, float]
|
| 102 |
+
dominant_frame: str
|
| 103 |
+
score: float
|
| 104 |
+
|
| 105 |
+
|
| 106 |
+
class FramingResponse(BaseModel):
|
| 107 |
+
results: List[FramingResult]
|
| 108 |
+
|
| 109 |
+
|
| 110 |
+
# === Fake Score ===
|
| 111 |
+
|
| 112 |
+
class FakeScoreResult(BaseModel):
|
| 113 |
+
id: int
|
| 114 |
+
score: int
|
| 115 |
+
level: str
|
| 116 |
+
reasons: List[str]
|
| 117 |
+
|
| 118 |
+
|
| 119 |
+
class FakeScoreResponse(BaseModel):
|
| 120 |
+
results: List[FakeScoreResult]
|
| 121 |
+
|
| 122 |
+
|
| 123 |
+
# === Opinion vs Fact ===
|
| 124 |
+
|
| 125 |
+
class OpinionFactResult(BaseModel):
|
| 126 |
+
id: int
|
| 127 |
+
classification: str
|
| 128 |
+
opinion_pct: int
|
| 129 |
+
fact_pct: int
|
| 130 |
+
confidence: float
|
| 131 |
+
|
| 132 |
+
|
| 133 |
+
class OpinionFactResponse(BaseModel):
|
| 134 |
+
results: List[OpinionFactResult]
|