amosnbn commited on
Commit
7a604fd
·
1 Parent(s): 9961081
Files changed (1) hide show
  1. app.py +76 -31
app.py CHANGED
@@ -1,35 +1,41 @@
1
  # app.py
2
- # PapuaTranslate — Flask + SQLAlchemy (Supabase/SQLite) + mT5-LoRA (lazy) + diag + preload
3
- import os, re, logging, threading, traceback
 
 
4
  from datetime import datetime, timezone, timedelta
5
  from functools import wraps
6
  from flask import Flask, render_template, request, redirect, url_for, session, jsonify, flash
7
  from werkzeug.middleware.proxy_fix import ProxyFix
 
8
 
9
- # ===== Logging =====
10
  logging.basicConfig(level=logging.INFO, format="%(asctime)s | %(levelname)s | %(message)s")
11
  log = logging.getLogger("papua-app")
12
 
13
- # ===== Flask / Template =====
14
  app = Flask(__name__, template_folder="frontend", static_folder="static")
 
 
15
  app.wsgi_app = ProxyFix(app.wsgi_app, x_for=1, x_proto=1, x_host=1)
16
 
 
17
  app.config.update(
18
  SECRET_KEY=os.getenv("SECRET_KEY", "dev-secret-change-me"),
19
  SESSION_COOKIE_NAME="hfspace_session",
20
- SESSION_COOKIE_SAMESITE="None",
21
- SESSION_COOKIE_SECURE=True,
22
  SESSION_COOKIE_HTTPONLY=True,
23
  SESSION_COOKIE_PATH="/",
24
  PREFERRED_URL_SCHEME="https",
25
  )
26
  app.permanent_session_lifetime = timedelta(hours=8)
27
 
28
- # ===== Feature flags via ENV =====
29
- PRELOAD_MODEL = os.getenv("PRELOAD_MODEL", "true").lower() in ("1","true","yes")
30
- FALLBACK_TRANSLATE = os.getenv("FALLBACK_TRANSLATE", "false").lower() in ("1","true","yes")
31
 
32
- # ===== DB: SQLAlchemy =====
33
  from sqlalchemy import create_engine, Column, Integer, Text, DateTime, ForeignKey, func
34
  from sqlalchemy.orm import declarative_base, sessionmaker, scoped_session, relationship
35
 
@@ -38,10 +44,12 @@ if not DATABASE_URL:
38
  DATABASE_URL = "sqlite:////tmp/app.db"
39
  log.warning("[DB] DATABASE_URL tidak diset; pakai SQLite /tmp/app.db")
40
  else:
 
41
  if DATABASE_URL.startswith("postgres://"):
42
  DATABASE_URL = DATABASE_URL.replace("postgres://", "postgresql+psycopg2://", 1)
43
  elif DATABASE_URL.startswith("postgresql://"):
44
  DATABASE_URL = DATABASE_URL.replace("postgresql://", "postgresql+psycopg2://", 1)
 
45
  if DATABASE_URL.startswith("postgresql+psycopg2") and "sslmode=" not in DATABASE_URL:
46
  sep = "&" if "?" in DATABASE_URL else "?"
47
  DATABASE_URL = f"{DATABASE_URL}{sep}sslmode=require"
@@ -72,7 +80,7 @@ try:
72
  except Exception as e:
73
  log.exception("[DB] init error: %s", e)
74
 
75
- # ===== Auth helpers =====
76
  from werkzeug.security import generate_password_hash, check_password_hash
77
  def set_password(user: User, raw: str): user.pass_hash = generate_password_hash(raw)
78
  def verify_password(user: User, raw: str) -> bool:
@@ -87,7 +95,7 @@ def login_required(fn):
87
  return fn(*args, **kwargs)
88
  return _wrap
89
 
90
- # ===== Prenorm =====
91
  PAPUA_MAP = {
92
  r"\bsa\b": "saya", r"\bko\b": "kamu", r"\btra\b": "tidak", r"\bndak\b": "tidak",
93
  r"\bmo\b": "mau", r"\bpu\b": "punya", r"\bsu\b": "sudah", r"\bkong\b": "kemudian",
@@ -98,9 +106,9 @@ def prenorm(text: str) -> str:
98
  for pat, repl in PAPUA_MAP.items(): t = re.sub(pat, repl, t, flags=re.IGNORECASE)
99
  return t
100
 
101
- # ===== Model (lazy) =====
102
- BASE_MODEL_ID = os.getenv("BASE_MODEL_ID", "google/mt5-small") # kecil dulu untuk uji
103
- ADAPTER_ID = os.getenv("ADAPTER_ID", "") # kosongkan dulu
104
  DEVICE = "cuda" if os.getenv("DEVICE", "cpu") == "cuda" else "cpu"
105
 
106
  TOK = None
@@ -109,20 +117,58 @@ _MODEL_LOCK = threading.Lock()
109
  _MODEL_READY = False
110
  _MODEL_ERROR = None
111
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
112
  def _load_model():
 
113
  global TOK, MODEL, _MODEL_READY, _MODEL_ERROR
114
  try:
115
- log.info("[MODEL] loading base=%s adapter=%s", BASE_MODEL_ID, ADAPTER_ID or "-")
116
- # import di sini agar error import terlihat di /diag
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
117
  import torch
118
  from transformers import AutoTokenizer, AutoModelForSeq2SeqLM
119
  from peft import PeftModel
120
 
121
- TOK = AutoTokenizer.from_pretrained(BASE_MODEL_ID)
122
- base = AutoModelForSeq2SeqLM.from_pretrained(BASE_MODEL_ID)
123
 
124
- if ADAPTER_ID:
125
- MODEL = PeftModel.from_pretrained(base, ADAPTER_ID)
 
 
 
 
126
  else:
127
  MODEL = base
128
 
@@ -149,11 +195,11 @@ def translate_with_model(text: str, max_new_tokens: int = 48) -> str:
149
  if not _MODEL_READY or m is None:
150
  raise RuntimeError(f"Model not ready: {_MODEL_ERROR or 'unknown error'}")
151
 
152
- # guard panjang input agar tidak OOM
153
  enc = tok([text], return_tensors="pt", truncation=True, max_length=256)
154
  enc = {k: v.to(DEVICE) for k, v in enc.items()}
155
 
156
- log.info("[GEN] start (len=%d)", int(enc["input_ids"].shape[-1]))
157
  out = m.generate(
158
  **enc,
159
  max_new_tokens=int(max_new_tokens),
@@ -165,7 +211,7 @@ def translate_with_model(text: str, max_new_tokens: int = 48) -> str:
165
  log.info("[GEN] done")
166
  return tok.decode(out[0], skip_special_tokens=True)
167
 
168
- # preload (opsional) supaya request pertama nggak lama
169
  def _preload_thread():
170
  try:
171
  _load_model()
@@ -175,7 +221,7 @@ def _preload_thread():
175
  if PRELOAD_MODEL:
176
  threading.Thread(target=_preload_thread, daemon=True).start()
177
 
178
- # ===== Utils / logging =====
179
  @app.before_request
180
  def _log_req():
181
  if request.path not in ("/health", "/ping", "/favicon.ico"):
@@ -186,7 +232,7 @@ def _err(e):
186
  log.exception("Unhandled error")
187
  return "Internal Server Error", 500
188
 
189
- # ===== Debug / Diag =====
190
  @app.get("/diag")
191
  def diag():
192
  import sys
@@ -221,7 +267,7 @@ def dbg_set():
221
  def dbg_get():
222
  return {"uid": session.get("uid"), "email": session.get("email")}
223
 
224
- # ===== Routes =====
225
  @app.get("/health")
226
  @app.get("/ping")
227
  def health():
@@ -299,22 +345,21 @@ def api_translate():
299
  try:
300
  clean = prenorm(text)
301
 
302
- # Jika ingin memastikan alur UI/DB dulu tanpa model:
303
  if FALLBACK_TRANSLATE:
304
  mt = f"[FAKE] {clean}"
305
  else:
306
  mt = translate_with_model(clean, max_new_tokens=max_new)
307
 
308
- # simpan riwayat
309
  with SessionLocal() as s:
310
  s.add(Translation(user_id=session["uid"], src=text, mt=mt))
311
  s.commit()
312
 
313
- return jsonify({"ok": True, "result": mt})
 
314
  except Exception as e:
315
  log.error("[API] translate error: %s", e)
316
  log.error(traceback.format_exc())
317
- # kirim error yang lebih informatif
318
  return jsonify({"ok": False, "error": f"{type(e).__name__}: {e}"}), 500
319
 
320
  if __name__ == "__main__":
 
1
  # app.py
2
+ # PapuaTranslate — Flask + SQLAlchemy (Supabase/SQLite) + mT5/LoRA (lazy)
3
+ # Fitur: ProxyFix, cookie Spaces, session permanent, /diag, preload, fallback, strip BOM JSON.
4
+
5
+ import os, re, json, codecs, pathlib, logging, threading, traceback
6
  from datetime import datetime, timezone, timedelta
7
  from functools import wraps
8
  from flask import Flask, render_template, request, redirect, url_for, session, jsonify, flash
9
  from werkzeug.middleware.proxy_fix import ProxyFix
10
+ from huggingface_hub import snapshot_download
11
 
12
+ # ========== Logging ==========
13
  logging.basicConfig(level=logging.INFO, format="%(asctime)s | %(levelname)s | %(message)s")
14
  log = logging.getLogger("papua-app")
15
 
16
+ # ========== Flask ==========
17
  app = Flask(__name__, template_folder="frontend", static_folder="static")
18
+
19
+ # Trust reverse proxy di HF supaya proto/host benar (perlu untuk cookie Secure)
20
  app.wsgi_app = ProxyFix(app.wsgi_app, x_for=1, x_proto=1, x_host=1)
21
 
22
+ # Cookie/session aman untuk iframe (HF Spaces) dan tab langsung hf.space
23
  app.config.update(
24
  SECRET_KEY=os.getenv("SECRET_KEY", "dev-secret-change-me"),
25
  SESSION_COOKIE_NAME="hfspace_session",
26
+ SESSION_COOKIE_SAMESITE="None", # penting: iframe = third-party context
27
+ SESSION_COOKIE_SECURE=True, # wajib True jika SAMESITE=None
28
  SESSION_COOKIE_HTTPONLY=True,
29
  SESSION_COOKIE_PATH="/",
30
  PREFERRED_URL_SCHEME="https",
31
  )
32
  app.permanent_session_lifetime = timedelta(hours=8)
33
 
34
+ # ========== Feature Flags via ENV ==========
35
+ PRELOAD_MODEL = os.getenv("PRELOAD_MODEL", "true").lower() in ("1","true","yes")
36
+ FALLBACK_TRANSLATE = os.getenv("FALLBACK_TRANSLATE", "false").lower() in ("1","true","yes")
37
 
38
+ # ========== Database (SQLAlchemy) ==========
39
  from sqlalchemy import create_engine, Column, Integer, Text, DateTime, ForeignKey, func
40
  from sqlalchemy.orm import declarative_base, sessionmaker, scoped_session, relationship
41
 
 
44
  DATABASE_URL = "sqlite:////tmp/app.db"
45
  log.warning("[DB] DATABASE_URL tidak diset; pakai SQLite /tmp/app.db")
46
  else:
47
+ # normalisasi skema ke psycopg2
48
  if DATABASE_URL.startswith("postgres://"):
49
  DATABASE_URL = DATABASE_URL.replace("postgres://", "postgresql+psycopg2://", 1)
50
  elif DATABASE_URL.startswith("postgresql://"):
51
  DATABASE_URL = DATABASE_URL.replace("postgresql://", "postgresql+psycopg2://", 1)
52
+ # tambahkan sslmode kalau belum ada
53
  if DATABASE_URL.startswith("postgresql+psycopg2") and "sslmode=" not in DATABASE_URL:
54
  sep = "&" if "?" in DATABASE_URL else "?"
55
  DATABASE_URL = f"{DATABASE_URL}{sep}sslmode=require"
 
80
  except Exception as e:
81
  log.exception("[DB] init error: %s", e)
82
 
83
+ # ========== Auth Helpers ==========
84
  from werkzeug.security import generate_password_hash, check_password_hash
85
  def set_password(user: User, raw: str): user.pass_hash = generate_password_hash(raw)
86
  def verify_password(user: User, raw: str) -> bool:
 
95
  return fn(*args, **kwargs)
96
  return _wrap
97
 
98
+ # ========== Prenorm (heuristik ringan) ==========
99
  PAPUA_MAP = {
100
  r"\bsa\b": "saya", r"\bko\b": "kamu", r"\btra\b": "tidak", r"\bndak\b": "tidak",
101
  r"\bmo\b": "mau", r"\bpu\b": "punya", r"\bsu\b": "sudah", r"\bkong\b": "kemudian",
 
106
  for pat, repl in PAPUA_MAP.items(): t = re.sub(pat, repl, t, flags=re.IGNORECASE)
107
  return t
108
 
109
+ # ========== Model (lazy) + Strip BOM ==========
110
+ BASE_MODEL_ID = os.getenv("BASE_MODEL_ID", "google/mt5-small") # mulai kecil dulu untuk uji cepat
111
+ ADAPTER_ID = os.getenv("ADAPTER_ID", "") # kosongkan dulu; isi nanti
112
  DEVICE = "cuda" if os.getenv("DEVICE", "cpu") == "cuda" else "cpu"
113
 
114
  TOK = None
 
117
  _MODEL_READY = False
118
  _MODEL_ERROR = None
119
 
120
+ def _strip_bom_in_dir(root_dir: str):
121
+ """Hapus BOM dari semua *.json (UTF-8-sig) agar tidak memicu JSONDecodeError."""
122
+ root = pathlib.Path(root_dir)
123
+ for p in root.rglob("*.json"):
124
+ try:
125
+ with codecs.open(p, "r", encoding="utf-8-sig") as f:
126
+ data = json.load(f)
127
+ with open(p, "w", encoding="utf-8") as f:
128
+ json.dump(data, f, ensure_ascii=False, indent=2)
129
+ log.info(f"[BOM] stripped: {p}")
130
+ except Exception as e:
131
+ # Jangan gagal hanya karena satu file
132
+ log.warning(f"[BOM] skip {p}: {e}")
133
+
134
  def _load_model():
135
+ """Download ke /tmp, strip BOM, lalu load dari path lokal."""
136
  global TOK, MODEL, _MODEL_READY, _MODEL_ERROR
137
  try:
138
+ log.info("[MODEL] downloading base=%s adapter=%s", BASE_MODEL_ID, ADAPTER_ID or "-")
139
+
140
+ base_dir = snapshot_download(
141
+ repo_id=BASE_MODEL_ID,
142
+ local_dir="/tmp/hf_base",
143
+ local_dir_use_symlinks=False,
144
+ allow_patterns=None,
145
+ )
146
+ _strip_bom_in_dir(base_dir)
147
+
148
+ adapter_dir = None
149
+ if ADAPTER_ID:
150
+ adapter_dir = snapshot_download(
151
+ repo_id=ADAPTER_ID,
152
+ local_dir="/tmp/hf_adapter",
153
+ local_dir_use_symlinks=False,
154
+ allow_patterns=None,
155
+ )
156
+ _strip_bom_in_dir(adapter_dir)
157
+
158
+ # Import di sini agar error versi lib ketangkep di /diag
159
  import torch
160
  from transformers import AutoTokenizer, AutoModelForSeq2SeqLM
161
  from peft import PeftModel
162
 
163
+ log.info("[MODEL] loading tokenizer from %s", base_dir)
164
+ TOK = AutoTokenizer.from_pretrained(base_dir)
165
 
166
+ log.info("[MODEL] loading base model from %s", base_dir)
167
+ base = AutoModelForSeq2SeqLM.from_pretrained(base_dir)
168
+
169
+ if adapter_dir:
170
+ log.info("[MODEL] attaching adapter from %s", adapter_dir)
171
+ MODEL = PeftModel.from_pretrained(base, adapter_dir)
172
  else:
173
  MODEL = base
174
 
 
195
  if not _MODEL_READY or m is None:
196
  raise RuntimeError(f"Model not ready: {_MODEL_ERROR or 'unknown error'}")
197
 
198
+ # Guard panjang input supaya aman
199
  enc = tok([text], return_tensors="pt", truncation=True, max_length=256)
200
  enc = {k: v.to(DEVICE) for k, v in enc.items()}
201
 
202
+ log.info("[GEN] start (tok_len=%d)", int(enc["input_ids"].shape[-1]))
203
  out = m.generate(
204
  **enc,
205
  max_new_tokens=int(max_new_tokens),
 
211
  log.info("[GEN] done")
212
  return tok.decode(out[0], skip_special_tokens=True)
213
 
214
+ # Preload supaya request pertama nggak lama/timeout
215
  def _preload_thread():
216
  try:
217
  _load_model()
 
221
  if PRELOAD_MODEL:
222
  threading.Thread(target=_preload_thread, daemon=True).start()
223
 
224
+ # ========== Utils / Logging ==========
225
  @app.before_request
226
  def _log_req():
227
  if request.path not in ("/health", "/ping", "/favicon.ico"):
 
232
  log.exception("Unhandled error")
233
  return "Internal Server Error", 500
234
 
235
+ # ========== Debug & Diag ==========
236
  @app.get("/diag")
237
  def diag():
238
  import sys
 
267
  def dbg_get():
268
  return {"uid": session.get("uid"), "email": session.get("email")}
269
 
270
+ # ========== Routes ==========
271
  @app.get("/health")
272
  @app.get("/ping")
273
  def health():
 
345
  try:
346
  clean = prenorm(text)
347
 
 
348
  if FALLBACK_TRANSLATE:
349
  mt = f"[FAKE] {clean}"
350
  else:
351
  mt = translate_with_model(clean, max_new_tokens=max_new)
352
 
353
+ # Simpan riwayat
354
  with SessionLocal() as s:
355
  s.add(Translation(user_id=session["uid"], src=text, mt=mt))
356
  s.commit()
357
 
358
+ # Kompatibel dengan frontend kamu (pakai j.mt)
359
+ return jsonify({"ok": True, "mt": mt})
360
  except Exception as e:
361
  log.error("[API] translate error: %s", e)
362
  log.error(traceback.format_exc())
 
363
  return jsonify({"ok": False, "error": f"{type(e).__name__}: {e}"}), 500
364
 
365
  if __name__ == "__main__":