amosnbn commited on
Commit
57cffd2
Β·
1 Parent(s): f7bc2d9
Files changed (1) hide show
  1. app.py +66 -46
app.py CHANGED
@@ -1,25 +1,24 @@
1
  # app.py
2
- import os, re, json, logging, threading
 
3
  from datetime import datetime, timezone
4
  from functools import wraps
5
- from typing import Optional, Tuple, List
6
  from flask import Flask, render_template, request, redirect, url_for, session, jsonify, flash
7
 
 
8
  logging.basicConfig(level=logging.INFO, format="%(asctime)s | %(levelname)s | %(message)s")
9
  log = logging.getLogger("papua-app")
10
 
11
- # ==== GANTI: pakai folder 'templates' (atau pastikan file HTML ada di 'frontend') ====
12
  app = Flask(__name__, template_folder="templates", static_folder="static")
13
-
14
- # cookie aman di HF (HTTPS). Untuk lokal bisa set False via ENV.
15
- SESSION_SECURE = os.getenv("SESSION_COOKIE_SECURE", "true").lower() in ("1","true","yes")
16
  app.config.update(
17
  SECRET_KEY=os.getenv("SECRET_KEY", "dev-secret-change-me"),
18
  SESSION_COOKIE_SAMESITE="Lax",
19
  SESSION_COOKIE_SECURE=SESSION_SECURE,
20
  )
21
 
22
- # ================= DB =================
23
  from sqlalchemy import create_engine, Column, Integer, Text, DateTime, ForeignKey, func
24
  from sqlalchemy.orm import declarative_base, sessionmaker, scoped_session, relationship
25
 
@@ -28,7 +27,7 @@ if not DATABASE_URL:
28
  DATABASE_URL = "sqlite:////tmp/app.db"
29
  log.warning("[DB] DATABASE_URL tidak diset; pakai SQLite /tmp/app.db")
30
  else:
31
- # normalisasi skema lama
32
  if DATABASE_URL.startswith("postgres://"):
33
  DATABASE_URL = DATABASE_URL.replace("postgres://", "postgresql+psycopg2://", 1)
34
  elif DATABASE_URL.startswith("postgresql://"):
@@ -60,11 +59,11 @@ class Translation(Base):
60
 
61
  try:
62
  Base.metadata.create_all(engine)
63
- log.info("[DB] Ready: %s", DATABASE_URL)
64
  except Exception as e:
65
  log.exception("[DB] init error: %s", e)
66
 
67
- # ================= Auth =================
68
  from werkzeug.security import generate_password_hash, check_password_hash
69
  def set_password(user: User, raw: str): user.pass_hash = generate_password_hash(raw)
70
  def verify_password(user: User, raw: str) -> bool:
@@ -79,25 +78,28 @@ def login_required(fn):
79
  return fn(*args, **kwargs)
80
  return _wrap
81
 
82
- # ================= Prenorm =================
83
  PAPUA_MAP = {
84
  r"\bsa\b": "saya", r"\bko\b": "kamu", r"\btra\b": "tidak", r"\bndak\b": "tidak",
85
  r"\bmo\b": "mau", r"\bpu\b": "punya", r"\bsu\b": "sudah", r"\bkong\b": "kemudian",
86
  }
87
  def prenorm(text: str) -> str:
88
  t = re.sub(r"\s+", " ", text.strip())
89
- t = t.replace("…","...").replace("–","-").replace("β€”","-")
90
  for pat, repl in PAPUA_MAP.items(): t = re.sub(pat, repl, t, flags=re.IGNORECASE)
91
  return t
92
 
93
- # ================= Model (lazy) =================
94
  from transformers import AutoTokenizer, AutoModelForSeq2SeqLM
95
  from peft import PeftModel
96
 
97
  BASE_MODEL_ID = os.getenv("BASE_MODEL_ID", "amosnbn/cendol-mt5-base-inst")
98
  ADAPTER_ID = os.getenv("ADAPTER_ID", "amosnbn/papua-lora-ckpt-168")
99
- DEVICE = "cuda" if os.getenv("DEVICE","cpu") == "cuda" else "cpu"
100
- TOK = None; MODEL = None; _MODEL_LOCK = threading.Lock()
 
 
 
101
 
102
  def _load_model():
103
  global TOK, MODEL
@@ -112,19 +114,24 @@ def get_model():
112
  global MODEL
113
  if MODEL is None:
114
  with _MODEL_LOCK:
115
- if MODEL is None: _load_model()
 
116
  return TOK, MODEL
117
 
118
  def translate_with_model(text: str, max_new_tokens: int = 48) -> str:
119
  tok, m = get_model()
120
  inputs = tok([text], return_tensors="pt").to(DEVICE)
121
  outputs = m.generate(
122
- **inputs, max_new_tokens=max_new_tokens, num_beams=4,
123
- length_penalty=0.9, no_repeat_ngram_size=3, early_stopping=True,
 
 
 
 
124
  )
125
  return tok.decode(outputs[0], skip_special_tokens=True)
126
 
127
- # ================= Utilities =================
128
  @app.before_request
129
  def _log_req():
130
  if request.path not in ("/health", "/ping", "/favicon.ico"):
@@ -132,48 +139,58 @@ def _log_req():
132
 
133
  @app.errorhandler(Exception)
134
  def _err(e):
135
- # supaya 500 kelihatan jelas di Logs HF
136
  log.exception("Unhandled error")
137
  return "Internal Server Error", 500
138
 
139
- # ================= Routes =================
140
- @app.get("/health"); @app.get("/ping")
141
- def health(): return jsonify({"ok": True, "time": datetime.now(timezone.utc).isoformat()})
 
 
142
 
143
  @app.get("/login")
144
- def login_get(): return render_template("login.html")
 
145
 
146
  @app.post("/login")
147
  def login_post():
148
  email = (request.form.get("email") or "").strip().lower()
149
  pwd = request.form.get("password") or ""
150
  if not email or not pwd:
151
- flash("Isi email dan password", "error"); return redirect(url_for("login_get"))
 
152
  with SessionLocal() as s:
153
  u = s.query(User).filter_by(email=email).first()
154
  if not u or not verify_password(u, pwd):
155
- flash("Email atau password salah", "error"); return redirect(url_for("login_get"))
 
156
  session["uid"], session["email"] = u.id, u.email
157
- return redirect(url_for("index"))
158
 
159
  @app.get("/register")
160
- def register_get(): return render_template("register.html")
 
161
 
162
  @app.post("/register")
163
  def register_post():
164
  email = (request.form.get("email") or "").strip().lower()
165
  pwd = request.form.get("password") or ""
166
  if not email or not pwd:
167
- flash("Isi email dan password", "error"); return redirect(url_for("register_get"))
 
168
  with SessionLocal() as s:
169
  if s.query(User).filter_by(email=email).first():
170
- flash("Email sudah terdaftar", "error"); return redirect(url_for("register_get"))
171
- u = User(email=email); set_password(u, pwd); s.add(u); s.commit()
 
 
172
  session["uid"], session["email"] = u.id, u.email
173
  return redirect(url_for("index"))
174
 
175
  @app.get("/logout")
176
- def logout(): session.clear(); return redirect(url_for("login_get"))
 
 
177
 
178
  @app.get("/")
179
  @login_required
@@ -181,33 +198,36 @@ def index():
181
  with SessionLocal() as s:
182
  uid = session["uid"]
183
  items = (s.query(Translation)
184
- .filter(Translation.user_id == uid)
185
- .order_by(Translation.id.desc()).limit(10).all())
 
186
  data = [{"src": it.src, "mt": it.mt, "created_at": it.created_at} for it in items]
187
- # ==== GANTI: kirim variabel yang dipakai template ====
188
- return render_template("index.html",
189
- user=session.get("email"),
190
- data=data,
191
- device=DEVICE)
192
 
193
  @app.get("/about")
194
- def about_page(): return render_template("about.html")
 
195
 
196
  @app.post("/translate")
197
  def api_translate():
198
- if not session.get("uid"): return jsonify({"error": "Unauthorized"}), 401
 
199
  payload = request.get_json(silent=True) or {}
200
  text = (payload.get("text") or "").strip()
201
  max_new = int(payload.get("max_new_tokens", 48))
202
- if not text: return jsonify({"error": "Empty text"}), 400
 
203
  try:
204
- clean = prenorm(text); mt = translate_with_model(clean, max_new_tokens=max_new)
 
205
  with SessionLocal() as s:
206
- s.add(Translation(user_id=session["uid"], src=text, mt=mt)); s.commit()
 
207
  return jsonify({"mt": mt})
208
- except Exception as e:
209
  log.exception("translate error")
210
  return jsonify({"error": "server error"}), 500
211
 
212
  if __name__ == "__main__":
213
- app.run(host="0.0.0.0", port=int(os.getenv("PORT","7860")), debug=False)
 
1
  # app.py
2
+ # PapuaTranslate β€” Flask + SQLAlchemy + Supabase + mT5-LoRA (lazy load)
3
+ import os, re, logging, threading
4
  from datetime import datetime, timezone
5
  from functools import wraps
 
6
  from flask import Flask, render_template, request, redirect, url_for, session, jsonify, flash
7
 
8
+ # ===== Logging =====
9
  logging.basicConfig(level=logging.INFO, format="%(asctime)s | %(levelname)s | %(message)s")
10
  log = logging.getLogger("papua-app")
11
 
12
+ # ===== Flask (templates di folder 'templates') =====
13
  app = Flask(__name__, template_folder="templates", static_folder="static")
14
+ SESSION_SECURE = os.getenv("SESSION_COOKIE_SECURE", "true").lower() in ("1", "true", "yes")
 
 
15
  app.config.update(
16
  SECRET_KEY=os.getenv("SECRET_KEY", "dev-secret-change-me"),
17
  SESSION_COOKIE_SAMESITE="Lax",
18
  SESSION_COOKIE_SECURE=SESSION_SECURE,
19
  )
20
 
21
+ # ===== DB: SQLAlchemy (Supabase Postgres / SQLite fallback) =====
22
  from sqlalchemy import create_engine, Column, Integer, Text, DateTime, ForeignKey, func
23
  from sqlalchemy.orm import declarative_base, sessionmaker, scoped_session, relationship
24
 
 
27
  DATABASE_URL = "sqlite:////tmp/app.db"
28
  log.warning("[DB] DATABASE_URL tidak diset; pakai SQLite /tmp/app.db")
29
  else:
30
+ # normalisasi skema lama β†’ psycopg2 driver
31
  if DATABASE_URL.startswith("postgres://"):
32
  DATABASE_URL = DATABASE_URL.replace("postgres://", "postgresql+psycopg2://", 1)
33
  elif DATABASE_URL.startswith("postgresql://"):
 
59
 
60
  try:
61
  Base.metadata.create_all(engine)
62
+ log.info("[DB] Ready")
63
  except Exception as e:
64
  log.exception("[DB] init error: %s", e)
65
 
66
+ # ===== Auth helpers =====
67
  from werkzeug.security import generate_password_hash, check_password_hash
68
  def set_password(user: User, raw: str): user.pass_hash = generate_password_hash(raw)
69
  def verify_password(user: User, raw: str) -> bool:
 
78
  return fn(*args, **kwargs)
79
  return _wrap
80
 
81
+ # ===== Prenorm (heuristik ringan) =====
82
  PAPUA_MAP = {
83
  r"\bsa\b": "saya", r"\bko\b": "kamu", r"\btra\b": "tidak", r"\bndak\b": "tidak",
84
  r"\bmo\b": "mau", r"\bpu\b": "punya", r"\bsu\b": "sudah", r"\bkong\b": "kemudian",
85
  }
86
  def prenorm(text: str) -> str:
87
  t = re.sub(r"\s+", " ", text.strip())
88
+ t = t.replace("…", "...").replace("–", "-").replace("β€”", "-")
89
  for pat, repl in PAPUA_MAP.items(): t = re.sub(pat, repl, t, flags=re.IGNORECASE)
90
  return t
91
 
92
+ # ===== Model (lazy-load LoRA) =====
93
  from transformers import AutoTokenizer, AutoModelForSeq2SeqLM
94
  from peft import PeftModel
95
 
96
  BASE_MODEL_ID = os.getenv("BASE_MODEL_ID", "amosnbn/cendol-mt5-base-inst")
97
  ADAPTER_ID = os.getenv("ADAPTER_ID", "amosnbn/papua-lora-ckpt-168")
98
+ DEVICE = "cuda" if os.getenv("DEVICE", "cpu") == "cuda" else "cpu"
99
+
100
+ TOK = None
101
+ MODEL = None
102
+ _MODEL_LOCK = threading.Lock()
103
 
104
  def _load_model():
105
  global TOK, MODEL
 
114
  global MODEL
115
  if MODEL is None:
116
  with _MODEL_LOCK:
117
+ if MODEL is None:
118
+ _load_model()
119
  return TOK, MODEL
120
 
121
  def translate_with_model(text: str, max_new_tokens: int = 48) -> str:
122
  tok, m = get_model()
123
  inputs = tok([text], return_tensors="pt").to(DEVICE)
124
  outputs = m.generate(
125
+ **inputs,
126
+ max_new_tokens=max_new_tokens,
127
+ num_beams=4,
128
+ length_penalty=0.9,
129
+ no_repeat_ngram_size=3,
130
+ early_stopping=True,
131
  )
132
  return tok.decode(outputs[0], skip_special_tokens=True)
133
 
134
+ # ===== Little utils / logging =====
135
  @app.before_request
136
  def _log_req():
137
  if request.path not in ("/health", "/ping", "/favicon.ico"):
 
139
 
140
  @app.errorhandler(Exception)
141
  def _err(e):
 
142
  log.exception("Unhandled error")
143
  return "Internal Server Error", 500
144
 
145
+ # ===== Routes =====
146
+ @app.get("/health")
147
+ @app.get("/ping")
148
+ def health():
149
+ return jsonify({"ok": True, "time": datetime.now(timezone.utc).isoformat()})
150
 
151
  @app.get("/login")
152
+ def login_get():
153
+ return render_template("login.html")
154
 
155
  @app.post("/login")
156
  def login_post():
157
  email = (request.form.get("email") or "").strip().lower()
158
  pwd = request.form.get("password") or ""
159
  if not email or not pwd:
160
+ flash("Isi email dan password", "error")
161
+ return redirect(url_for("login_get"))
162
  with SessionLocal() as s:
163
  u = s.query(User).filter_by(email=email).first()
164
  if not u or not verify_password(u, pwd):
165
+ flash("Email atau password salah", "error")
166
+ return redirect(url_for("login_get"))
167
  session["uid"], session["email"] = u.id, u.email
168
+ return redirect(url_for("index"))
169
 
170
  @app.get("/register")
171
+ def register_get():
172
+ return render_template("register.html")
173
 
174
  @app.post("/register")
175
  def register_post():
176
  email = (request.form.get("email") or "").strip().lower()
177
  pwd = request.form.get("password") or ""
178
  if not email or not pwd:
179
+ flash("Isi email dan password", "error")
180
+ return redirect(url_for("register_get"))
181
  with SessionLocal() as s:
182
  if s.query(User).filter_by(email=email).first():
183
+ flash("Email sudah terdaftar", "error")
184
+ return redirect(url_for("register_get"))
185
+ u = User(email=email); set_password(u, pwd)
186
+ s.add(u); s.commit()
187
  session["uid"], session["email"] = u.id, u.email
188
  return redirect(url_for("index"))
189
 
190
  @app.get("/logout")
191
+ def logout():
192
+ session.clear()
193
+ return redirect(url_for("login_get"))
194
 
195
  @app.get("/")
196
  @login_required
 
198
  with SessionLocal() as s:
199
  uid = session["uid"]
200
  items = (s.query(Translation)
201
+ .filter(Translation.user_id == uid)
202
+ .order_by(Translation.id.desc())
203
+ .limit(10).all())
204
  data = [{"src": it.src, "mt": it.mt, "created_at": it.created_at} for it in items]
205
+ # index.html kamu memakai 'user' & 'data'
206
+ return render_template("index.html", user=session.get("email"), data=data, device=DEVICE)
 
 
 
207
 
208
  @app.get("/about")
209
+ def about_page():
210
+ return render_template("about.html")
211
 
212
  @app.post("/translate")
213
  def api_translate():
214
+ if not session.get("uid"):
215
+ return jsonify({"error": "Unauthorized"}), 401
216
  payload = request.get_json(silent=True) or {}
217
  text = (payload.get("text") or "").strip()
218
  max_new = int(payload.get("max_new_tokens", 48))
219
+ if not text:
220
+ return jsonify({"error": "Empty text"}), 400
221
  try:
222
+ clean = prenorm(text)
223
+ mt = translate_with_model(clean, max_new_tokens=max_new)
224
  with SessionLocal() as s:
225
+ s.add(Translation(user_id=session["uid"], src=text, mt=mt))
226
+ s.commit()
227
  return jsonify({"mt": mt})
228
+ except Exception:
229
  log.exception("translate error")
230
  return jsonify({"error": "server error"}), 500
231
 
232
  if __name__ == "__main__":
233
+ app.run(host="0.0.0.0", port=int(os.getenv("PORT", "7860")), debug=False)