File size: 5,959 Bytes
c77e919
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
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
    )