nuguri01's picture
Add app.py
c77e919 verified
Raw
History Blame Contribute Delete
5.96 kB
from fastapi import FastAPI, HTTPException
from pydantic import BaseModel
from transformers import AutoTokenizer, AutoModelForSequenceClassification
import torch
import numpy as np
import os
app = FastAPI(title="PGOS Emotion Agent API", version="1.0.0")
# ── μ„€μ • ───────────────────────────────────────────
HF_USERNAME = "nuguri01"
HF_TOKEN = os.environ.get("HF_TOKEN", "") # Spaces Secret으둜 μ£Όμž…
MAJOR_CLASSES = ["뢄노계", "μŠ¬ν””κ³„", "λΆˆμ•ˆκ³„", "μƒμ²˜κ³„", "당황계", "기쁨계"]
MINOR_STRUCTURE = {
"뢄노계": {"labels": ["λΆ„λ…Έ", "쒌절", "짜증"], "repo": "pgos-emotion-stage2-anger"},
"μŠ¬ν””κ³„": {"labels": ["μŠ¬ν””", "우울", "ν›„νšŒ"], "repo": "pgos-emotion-stage2-sadness"},
"λΆˆμ•ˆκ³„": {"labels": ["λΆˆμ•ˆ", "κ±±μ •"], "repo": "pgos-emotion-stage2-anxiety"},
"μƒμ²˜κ³„": {"labels": ["μƒμ²˜", "μ–΅μšΈ", "질투"], "repo": "pgos-emotion-stage2-hurt"},
"당황계": {"labels": ["μˆ˜μΉ˜μ‹¬", "죄책감", "λ‹Ήν™©"], "repo": "pgos-emotion-stage2-embarrass"},
"기쁨계": {"labels": ["기쁨", "μ•ˆλ„"], "repo": "pgos-emotion-stage2-joy"},
}
PGOS_CLASSES = [
"λΆ„λ…Έ", "쒌절", "짜증", "μŠ¬ν””", "우울", "ν›„νšŒ",
"λΆˆμ•ˆ", "κ±±μ •", "μƒμ²˜", "μ–΅μšΈ", "질투",
"μˆ˜μΉ˜μ‹¬", "죄책감", "λ‹Ήν™©", "기쁨", "μ•ˆλ„"
]
label2id = {l: i for i, l in enumerate(PGOS_CLASSES)}
device = "cuda" if torch.cuda.is_available() else "cpu"
# ── λͺ¨λΈ λ‘œλ“œ (μ„œλ²„ μ‹œμž‘ μ‹œ ν•œ 번만) ──────────────
print("πŸ“₯ λͺ¨λΈ λ‘œλ”© 쀑...")
tokenizer = AutoTokenizer.from_pretrained(
f"{HF_USERNAME}/pgos-emotion-stage1-major",
token=HF_TOKEN
)
model_major = AutoModelForSequenceClassification.from_pretrained(
f"{HF_USERNAME}/pgos-emotion-stage1-major",
token=HF_TOKEN
).to(device)
model_major.eval()
minor_models = {}
for major_label, config in MINOR_STRUCTURE.items():
model_sub = AutoModelForSequenceClassification.from_pretrained(
f"{HF_USERNAME}/{config['repo']}",
token=HF_TOKEN,
num_labels=len(config["labels"])
).to(device)
model_sub.eval()
minor_models[major_label] = model_sub
print("βœ… 전체 λͺ¨λΈ λ‘œλ“œ μ™„λ£Œ")
# ── μš”μ²­/응닡 μŠ€ν‚€λ§ˆ ───────────────────────────────
class EmotionRequest(BaseModel):
text: str
top_k: int = 3 # λ°˜ν™˜ν•  감정 수 (κΈ°λ³Έ 3개)
class EmotionResult(BaseModel):
emotion: str
score: float
rank: int
class EmotionResponse(BaseModel):
text: str
primary_emotion: str
primary_score: float
top_emotions: list[EmotionResult]
pgos_message: str # λ‚΄λ‹΄μžμ—κ²Œ λŒλ €μ€„ 멘트
# ── 감정 λΆ„λ₯˜ ν•¨μˆ˜ ─────────────────────────────────
def predict_emotions(text: str, top_k: int = 3):
# 1단계: λŒ€λΆ„λ₯˜
enc = tokenizer(text, max_length=128, padding=True,
truncation=True, return_tensors="pt").to(device)
with torch.no_grad():
major_probs = torch.softmax(
model_major(**enc).logits, dim=-1
).cpu().numpy()[0]
# 2단계: μ†ŒλΆ„λ₯˜ + ν™•λ₯  κ²°ν•©
final_probs = np.zeros(len(PGOS_CLASSES))
for major_idx, major_label in enumerate(MAJOR_CLASSES):
config = MINOR_STRUCTURE[major_label]
minor_labels = config["labels"]
model_sub = minor_models[major_label]
with torch.no_grad():
sub_probs = torch.softmax(
model_sub(**enc).logits, dim=-1
).cpu().numpy()[0]
for minor_idx, minor_label in enumerate(minor_labels):
final_probs[label2id[minor_label]] = (
major_probs[major_idx] * sub_probs[minor_idx]
)
# Top-K μΆ”μΆœ
top_k_ids = np.argsort(final_probs)[-top_k:][::-1]
top_emotions = [
EmotionResult(
emotion=PGOS_CLASSES[idx],
score=round(float(final_probs[idx]), 4),
rank=rank + 1
)
for rank, idx in enumerate(top_k_ids)
]
# PGOS 멘트 생성
primary = top_emotions[0].emotion
subs = [e.emotion for e in top_emotions[1:] if e.score > 0.05]
sub_str = f", {'κ³Ό '.join(subs)} 감정도 ν•¨κ»˜ λŠκ»΄μ§€λŠ” 것 κ°™μ•„μš”" if subs else ""
message = f"'{primary}' 감정이 주둜 λŠκ»΄μ§€λ©°{sub_str}. μ§€κΈˆ 이 감정을 천천히 λ“€μ—¬λ‹€λ³ΌκΉŒμš”?"
return top_emotions, primary, float(final_probs[top_k_ids[0]]), message
# ── API μ—”λ“œν¬μΈνŠΈ ─────────────────────────────────
@app.get("/")
def root():
return {
"service": "PGOS Emotion Agent API",
"version": "1.0.0",
"description": "계측적 2단계 감정 λΆ„λ₯˜κΈ° (16클래슀, Top-3 Hit Rate 0.870)",
"endpoints": {
"POST /predict": "감정 뢄석",
"GET /health": "μ„œλ²„ μƒνƒœ 확인"
}
}
@app.get("/health")
def health():
return {"status": "ok", "device": device, "models_loaded": len(minor_models) + 1}
@app.post("/predict", response_model=EmotionResponse)
def predict(req: EmotionRequest):
if not req.text.strip():
raise HTTPException(status_code=400, detail="ν…μŠ€νŠΈλ₯Ό μž…λ ₯ν•΄μ£Όμ„Έμš”.")
if len(req.text) > 500:
raise HTTPException(status_code=400, detail="ν…μŠ€νŠΈλŠ” 500자 μ΄ν•˜λ‘œ μž…λ ₯ν•΄μ£Όμ„Έμš”.")
top_k = max(1, min(req.top_k, 5)) # 1~5 μ‚¬μ΄λ‘œ μ œν•œ
top_emotions, primary, primary_score, message = predict_emotions(req.text, top_k)
return EmotionResponse(
text=req.text,
primary_emotion=primary,
primary_score=primary_score,
top_emotions=top_emotions,
pgos_message=message
)