Hoang Kha commited on
Commit
2a8d890
·
1 Parent(s): 1ba8a76

upload source

Browse files
Files changed (6) hide show
  1. Dockerfile +12 -0
  2. main.py +181 -0
  3. requirements.txt +6 -0
  4. static/main.js +88 -0
  5. static/style.css +117 -0
  6. templates/index.html +87 -0
Dockerfile ADDED
@@ -0,0 +1,12 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ FROM python:3.10-slim
2
+
3
+ RUN apt-get update && apt-get install -y git
4
+
5
+ WORKDIR /app
6
+ COPY . /app
7
+
8
+ RUN pip install --no-cache-dir -r requirements.txt
9
+
10
+ EXPOSE 7860
11
+
12
+ CMD ["python", "main.py"]
main.py ADDED
@@ -0,0 +1,181 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+ from flask import Flask, render_template, request, jsonify
3
+ from langdetect import detect
4
+ import torch
5
+ import torch.nn.functional as F
6
+ from transformers import AutoTokenizer, AutoModelForSequenceClassification, pipeline
7
+
8
+ app = Flask(__name__)
9
+
10
+ # --------- Models ----------
11
+ VI_MODEL_NAME = "wonrax/phobert-base-vietnamese-sentiment"
12
+ EN_MODEL_NAME = "distilbert-base-uncased-finetuned-sst-2-english"
13
+
14
+ device = "cuda" if torch.cuda.is_available() else "cpu"
15
+
16
+ # Vietnamese model
17
+ # vi_tokenizer = AutoTokenizer.from_pretrained(VI_MODEL_NAME, use_fast=False)
18
+ # vi_model = AutoModelForSequenceClassification.from_pretrained(VI_MODEL_NAME).to(device)
19
+ # vi_model.eval()
20
+ vi_tokenizer = AutoTokenizer.from_pretrained(VI_MODEL_NAME, use_fast=False)
21
+ vi_model = AutoModelForSequenceClassification.from_pretrained(VI_MODEL_NAME)
22
+ vi_model.eval()
23
+ sentiment_pipeline = pipeline("sentiment-analysis", model=vi_model, tokenizer=vi_tokenizer)
24
+
25
+
26
+ # English model
27
+ en_tokenizer = AutoTokenizer.from_pretrained(EN_MODEL_NAME)
28
+ en_model = AutoModelForSequenceClassification.from_pretrained(EN_MODEL_NAME).to(device)
29
+ en_model.eval()
30
+
31
+ # Label mapping cho PhoBERT
32
+ vi_label_map = {
33
+ 0: ("NEGATIVE", "Tiêu cực"),
34
+ 1: ("NEUTRAL", "Trung tính"),
35
+ 2: ("POSITIVE", "Tích cực")
36
+ }
37
+
38
+ # Label mapping cho tiếng Anh
39
+ en_label_map = {
40
+ 0: ("NEGATIVE", "Negative"),
41
+ 1: ("POSITIVE", "Positive")
42
+ }
43
+
44
+
45
+ # -----------------------------
46
+ # Ngôn ngữ nhận diện
47
+ # -----------------------------
48
+ def detect_lang(text: str) -> str:
49
+ try:
50
+ lang = detect(text)
51
+ if lang.startswith("vi"):
52
+ return "vi"
53
+ elif lang.startswith("en"):
54
+ return "en"
55
+ else:
56
+ if any(ch in text for ch in "ăâđêôơưáàạảãấầậẩẫắằặẳẵéèẹẻẽếềệểễóòọỏõốồộổỗớờợởỡíìịỉĩúùụủũứừựửữýỳỵỷỹ"):
57
+ return "vi"
58
+ return "en"
59
+ except Exception:
60
+ if any(ch in text for ch in "ăâđêôơưáàạảãấầậẩẫắằặẳẵéèẹẻẽếềệểễóòọỏõốồộổỗớờợởỡíìịỉĩúùụủũứừựửữýỳỵỷỹ"):
61
+ return "vi"
62
+ return "en"
63
+
64
+
65
+ # -----------------------------
66
+ # Phân tích tiếng Việt (PhoBERT)
67
+ # -----------------------------
68
+ # def analyze_vi(text: str):
69
+ # inputs = vi_tokenizer(text, return_tensors="pt", truncation=True, padding=True).to(device)
70
+ # with torch.no_grad():
71
+ # outputs = vi_model(**inputs)
72
+ # logits = outputs.logits.squeeze(0)
73
+ # probs = torch.softmax(logits, dim=-1)
74
+
75
+ # label_idx = int(torch.argmax(probs).item())
76
+ # eng_label, vi_label = vi_label_map[label_idx]
77
+ # confidence = float(probs[label_idx].item())
78
+
79
+ # scores = {
80
+ # vi_label_map[i][1]: round(float(probs[i].item()), 3) for i in range(3)
81
+ # }
82
+
83
+ # return {
84
+ # "language": "vi",
85
+ # "label": vi_label,
86
+ # "english_label": eng_label,
87
+ # "score": round(confidence, 3),
88
+ # "scores": scores
89
+ # }
90
+
91
+ def analyze_vi(text: str):
92
+ if not text.strip():
93
+ return {"error": "Text is empty."}
94
+
95
+ # Dùng pipeline của transformers
96
+ result = sentiment_pipeline(text)[0]
97
+ label = result["label"]
98
+ score = round(result["score"], 3)
99
+
100
+ # Map nhãn tiếng Việt
101
+ label_map = {
102
+ "POS": "Tích cực",
103
+ "NEG": "Tiêu cực",
104
+ "NEU": "Trung tính"
105
+ }
106
+
107
+ vi_label = label_map.get(label, label)
108
+
109
+ # Trả kết quả tương thích với frontend
110
+ return {
111
+ "language": "vi",
112
+ "label": vi_label,
113
+ "english_label": label, # Giữ nhãn gốc POS/NEG/NEU
114
+ "score": score,
115
+ "scores": {
116
+ "Tích cực": score if label == "POS" else 0.0,
117
+ "Trung tính": score if label == "NEU" else 0.0,
118
+ "Tiêu cực": score if label == "NEG" else 0.0
119
+ }
120
+ }
121
+ # -----------------------------
122
+ # Phân tích tiếng Anh
123
+ # -----------------------------
124
+ def analyze_en(text: str):
125
+ inputs = en_tokenizer(text, return_tensors="pt", truncation=True, padding=True).to(device)
126
+ with torch.no_grad():
127
+ outputs = en_model(**inputs)
128
+ logits = outputs.logits.squeeze(0)
129
+ probs = torch.softmax(logits, dim=-1)
130
+
131
+ label_idx = int(torch.argmax(probs).item())
132
+ eng_label, vi_label = en_label_map[label_idx]
133
+ confidence = float(probs[label_idx].item())
134
+
135
+ scores = {
136
+ en_label_map[i][1]: round(float(probs[i].item()), 3) for i in range(2)
137
+ }
138
+
139
+ return {
140
+ "language": "en",
141
+ "label": vi_label, # Giữ English, có thể đổi sang tiếng Việt nếu muốn
142
+ "english_label": eng_label,
143
+ "score": round(confidence, 3),
144
+ "scores": scores
145
+ }
146
+
147
+
148
+ # -----------------------------
149
+ # Flask routes
150
+ # -----------------------------
151
+ @app.route("/", methods=["GET"])
152
+ def home():
153
+ return render_template("index.html")
154
+
155
+
156
+ @app.route("/analyze", methods=["POST"])
157
+ def analyze():
158
+ data = request.get_json(force=True)
159
+ text = (data.get("text") or "").strip()
160
+ lang = (data.get("lang") or "auto").lower()
161
+ if not text:
162
+ return jsonify({"error": "Text is empty."}), 400
163
+
164
+ if lang == "auto":
165
+ lang = detect_lang(text)
166
+
167
+ if lang == "vi":
168
+ result = analyze_vi(text)
169
+ else:
170
+ result = analyze_en(text)
171
+
172
+ return jsonify({
173
+ "ok": True,
174
+ "input": {"text": text, "lang": lang},
175
+ "result": result
176
+ })
177
+
178
+
179
+ if __name__ == "__main__":
180
+ port = int(os.environ.get("PORT", 7860))
181
+ app.run(host="0.0.0.0", port=port)
requirements.txt ADDED
@@ -0,0 +1,6 @@
 
 
 
 
 
 
 
1
+ flask==3.0.3
2
+ transformers==4.44.2
3
+ torch==2.2.0+cpu
4
+ accelerate==0.33.0
5
+ langdetect==1.0.9
6
+ --extra-index-url https://download.pytorch.org/whl/cpu
static/main.js ADDED
@@ -0,0 +1,88 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ const $ = (sel) => document.querySelector(sel);
2
+ const analyzeBtn = $("#analyzeBtn");
3
+ const loader = $("#loader");
4
+ const resultBox = $("#result");
5
+ const resLang = $("#resLang");
6
+ const resLabel = $("#resLabel");
7
+ const resScore = $("#resScore");
8
+ const langSelect = $("#lang");
9
+ const textArea = $("#text");
10
+
11
+ function show(el){ el.classList.remove("hidden"); }
12
+ function hide(el){ el.classList.add("hidden"); }
13
+ function percent(x){ return (x*100).toFixed(1) + "%"; }
14
+
15
+ function renderBars(lang, scores){
16
+ const keys = lang === "vi" ? ["NEGATIVE","NEUTRAL","POSITIVE"] : ["NEGATIVE","POSITIVE"];
17
+ keys.forEach(k=>{
18
+ const wrap = document.createElement("div");
19
+ wrap.className = "bar";
20
+ const fill = document.createElement("div");
21
+ fill.className = "bar-fill";
22
+ fill.style.width = "0%";
23
+ const label = document.createElement("div");
24
+ label.className = "bar-label";
25
+ const sc = scores[k] ?? 0;
26
+ label.innerHTML = `<span>${k}</span><span>${percent(sc)}</span>`;
27
+ wrap.appendChild(fill);
28
+ setTimeout(()=>{ fill.style.width = percent(sc); }, 30);
29
+ });
30
+ }
31
+
32
+ langSelect.addEventListener("change", (e) => {
33
+ const lang = e.target.value;
34
+ console.clear();
35
+ console.log("Ngôn ngữ được chọn:", lang);
36
+ });
37
+
38
+ textArea.addEventListener("input", () => {
39
+ console.log("Nội dung hiện tại:", textArea.value.trim());
40
+ });
41
+
42
+ analyzeBtn.addEventListener("click", async ()=>{
43
+ const text = textArea.value.trim();
44
+ const lang = langSelect.value;
45
+ console.log("▶️ Bắt đầu phân tích với:", { text, lang });
46
+
47
+ if (!text) {
48
+ alert("Nhập nội dung trước.");
49
+ return;
50
+ }
51
+
52
+ hide(resultBox);
53
+ show(loader);
54
+
55
+ try{
56
+ const r = await fetch("/analyze", {
57
+ method:"POST",
58
+ headers:{ "Content-Type":"application/json" },
59
+ body: JSON.stringify({ text, lang })
60
+ });
61
+ const data = await r.json();
62
+ hide(loader);
63
+
64
+ if (!r.ok || data.ok !== true){
65
+ alert(data.error || "Có lỗi xảy ra.");
66
+ console.error("Lỗi phản hồi:", data);
67
+ return;
68
+ }
69
+
70
+ const { input, result } = data;
71
+ console.log("Kết quả nhận được:", result);
72
+
73
+ resLang.textContent = input.lang.toUpperCase();
74
+ resLabel.textContent = result.label;
75
+ resLabel.style.borderColor =
76
+ result.label === "POSITIVE" ? "#23d2ac" :
77
+ result.label === "NEUTRAL" ? "#ffd166" : "#ff6b6b";
78
+ resScore.textContent = percent(result.score);
79
+
80
+ renderBars(input.lang, result.scores);
81
+ show(resultBox);
82
+
83
+ }catch(e){
84
+ hide(loader);
85
+ alert("Lỗi mạng hoặc server.");
86
+ console.error("Lỗi khi gửi request:", e);
87
+ }
88
+ });
static/style.css ADDED
@@ -0,0 +1,117 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ :root{
2
+ --bg:#0f1220;
3
+ --panel:#171a2b;
4
+ --panel-2:#1e2236;
5
+ --text:#eef1ff;
6
+ --muted:#9aa3c7;
7
+ --primary:#6c8dff;
8
+ --accent:#23d2ac;
9
+ --danger:#ff6b6b;
10
+ --warning:#ffd166;
11
+ --neutral:#a0aec0;
12
+ --border:#2a3150;
13
+ --shadow: 0 10px 25px rgba(0,0,0,.35);
14
+ }
15
+
16
+ *{box-sizing:border-box}
17
+ html,body{height:100%}
18
+ body{
19
+ margin:0;
20
+ display:flex;
21
+ font-family: system-ui, -apple-system, "Segoe UI", Roboto, Arial, "Helvetica Neue", sans-serif;
22
+ background: linear-gradient(180deg, #0a0d1a, #11162a);
23
+ color:var(--text);
24
+ }
25
+
26
+ /* Sidebar */
27
+ .sidebar{
28
+ width:280px;
29
+ background:var(--panel);
30
+ border-right:1px solid var(--border);
31
+ display:flex; flex-direction:column;
32
+ position:sticky; top:0; height:100vh;
33
+ box-shadow: var(--shadow);
34
+ }
35
+ .brand{display:flex; gap:12px; justify-content: center ;padding:20px; align-items:center; border-bottom:1px solid var(--border)}
36
+ .logo{width:40px; height:40px; border-radius:12px; display:grid; place-items:center; background:var(--panel-2); font-size:22px}
37
+ .brand-text h1{margin:0; font-size:18px}
38
+ .brand-text p{margin:2px 0 0; font-size:12px; color:var(--muted)}
39
+ .nav{padding:8px}
40
+ .nav-item{
41
+ display:block; padding:12px 16px; margin:6px 10px; border-radius:10px;
42
+ color:var(--text); text-decoration:none; background:transparent; border:1px solid transparent;
43
+ }
44
+ .nav-item:hover{background:var(--panel-2)}
45
+ .nav-item.active{border-color:var(--primary); background:rgba(108,141,255,.08)}
46
+ .nav-item.disabled{opacity:.6; cursor:not-allowed}
47
+ .sidebar-footer{margin-top:auto; padding:16px; color:var(--muted); border-top:1px solid var(--border)}
48
+
49
+ /* Main */
50
+ .main{flex:1; display:flex; flex-direction:column; min-width:0}
51
+ .header{
52
+ display:flex; align-items:center; justify-content:space-between;
53
+ padding:18px 24px; border-bottom:1px solid var(--border);
54
+ backdrop-filter: blur(6px);
55
+ }
56
+ .header h2{margin:0; font-size:20px}
57
+ .badge{padding:6px 10px; background:var(--panel-2); border:1px solid var(--border); border-radius:999px; font-size:12px; color:var(--muted)}
58
+
59
+ .card{
60
+ background:var(--panel);
61
+ margin:24px; padding:20px; border:1px solid var(--border); border-radius:16px;
62
+ box-shadow: var(--shadow);
63
+ }
64
+
65
+ label{display:block; margin:10px 0 6px; color:#e4e8ff}
66
+ select, textarea{
67
+ width:100%; background:var(--panel-2); color:var(--text);
68
+ border:1px solid var(--border); border-radius:12px; padding:12px 14px; outline:none;
69
+ }
70
+ textarea{resize:vertical}
71
+
72
+ .actions{display:flex; justify-content:flex-end; margin-top:12px}
73
+ button{
74
+ border:1px solid transparent; background:var(--primary); color:white; padding:12px 16px;
75
+ border-radius:12px; cursor:pointer; font-weight:600; transition:.2s transform ease;
76
+ box-shadow: 0 6px 16px rgba(108,141,255,.35);
77
+ }
78
+ button:hover{transform: translateY(-1px)}
79
+ button:active{transform: translateY(0)}
80
+
81
+ .row{display:flex; gap:16px; margin-bottom:8px}
82
+ .col{flex:1}
83
+
84
+ /* Loader */
85
+ .loader{display:flex; align-items:center; gap:12px; margin-top:16px; padding:12px 14px;
86
+ border:1px dashed var(--border); border-radius:12px; background:rgba(255,255,255,.02)}
87
+ .spinner{
88
+ width:20px; height:20px; border-radius:50%;
89
+ border:3px solid rgba(255,255,255,.2); border-top-color:var(--accent);
90
+ animation:spin 0.9s linear infinite;
91
+ }
92
+ @keyframes spin{to{transform:rotate(360deg)}}
93
+ .hidden{display:none}
94
+
95
+ /* Result */
96
+ .result{margin-top:16px}
97
+ .result h3{margin:0 0 12px}
98
+ .result-grid{display:grid; grid-template-columns: repeat(3,1fr); gap:12px}
99
+ .result-item{background:var(--panel-2); border:1px solid var(--border); border-radius:12px; padding:12px}
100
+ .result-item .label{display:block; font-size:12px; color:var(--muted); margin-bottom:6px}
101
+ .value{font-weight:700}
102
+ .badge-lg{padding:6px 10px; border-radius:10px; border:1px solid var(--border); background:rgba(255,255,255,.04)}
103
+ .bars{margin-top:14px}
104
+ .bar{
105
+ background:var(--panel-2); border:1px solid var(--border); border-radius:10px; overflow:hidden; margin:8px 0
106
+ }
107
+ .bar-fill{
108
+ height:14px; background:linear-gradient(90deg, var(--accent), var(--primary));
109
+ width:0%;
110
+ transition: width .5s ease;
111
+ }
112
+ .bar-label{display:flex; justify-content:space-between; font-size:12px; color:var(--muted); margin-top:4px}
113
+
114
+ /* Footer */
115
+ .footer{
116
+ margin: 0 24px 24px; color:var(--muted);
117
+ }
templates/index.html ADDED
@@ -0,0 +1,87 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <!DOCTYPE html>
2
+ <html lang="vi">
3
+ <head>
4
+ <meta charset="utf-8" />
5
+ <meta name="viewport" content="width=device-width,initial-scale=1" />
6
+ <title>Sentiment Analysis (EN + VI)</title>
7
+ <link rel="stylesheet" href="/static/style.css" />
8
+ </head>
9
+ <body>
10
+ <aside class="sidebar">
11
+ <div class="brand">
12
+ <div class="brand-text">
13
+ <h1>Sentiment AI</h1>
14
+ <p>English &amp; Tiếng Việt</p>
15
+ </div>
16
+ </div>
17
+ <nav class="nav">
18
+ <a class="nav-item active" href="#">Phân tích</a>
19
+ <a class="nav-item disabled" href="#" title="Coming soon">Lịch sử</a>
20
+ <a class="nav-item disabled" href="#" title="Coming soon">Cấu hình</a>
21
+ </nav>
22
+ <footer class="sidebar-footer">
23
+ <small>&copy; 2025—Demo</small>
24
+ </footer>
25
+ </aside>
26
+
27
+ <main class="main">
28
+ <header class="header">
29
+ <h2>Phân tích cảm xúc</h2>
30
+ <div class="right">
31
+ <span class="badge">PhoBERT + SST-2</span>
32
+ </div>
33
+ </header>
34
+
35
+ <section class="card">
36
+ <div class="row">
37
+ <div class="col">
38
+ <label for="lang">Ngôn ngữ</label>
39
+ <select id="lang">
40
+ <option value="auto">Tự động</option>
41
+ <option value="vi" selected>Tiếng Việt</option>
42
+ <option value="en">English</option>
43
+ </select>
44
+ </div>
45
+ </div>
46
+
47
+ <label for="text">Nội dung cần phân tích</label>
48
+ <textarea
49
+ id="text"
50
+ rows="6"
51
+ placeholder="Nhập nội dung... Ví dụ: Mình rất thích sản phẩm này! / I absolutely love this!"
52
+ ></textarea>
53
+
54
+ <div class="actions">
55
+ <button id="analyzeBtn">Phân tích</button>
56
+ </div>
57
+
58
+ <div id="loader" class="loader hidden">
59
+ <div class="spinner"></div>
60
+ <p>Đang phân tích...</p>
61
+ </div>
62
+
63
+ <div id="result" class="result hidden">
64
+ <h3>Kết quả</h3>
65
+ <div class="result-grid">
66
+ <div class="result-item">
67
+ <span class="label">Ngôn ngữ</span>
68
+ <span id="resLang" class="value">—</span>
69
+ </div>
70
+ <div class="result-item">
71
+ <span class="label">Nhận định</span>
72
+ <span id="resLabel" class="value badge-lg">—</span>
73
+ </div>
74
+ <div class="result-item">
75
+ <span class="label">Độ tin cậy</span>
76
+ <span id="resScore" class="value">—</span>
77
+ </div>
78
+ </div>
79
+
80
+ <div id="resBars" class="bars"></div>
81
+ </div>
82
+ </section>
83
+ </main>
84
+
85
+ <script src="/static/main.js"></script>
86
+ </body>
87
+ </html>