amosnbn commited on
Commit
c5f0f42
Β·
1 Parent(s): 9031ea7

fix: session cookie secure false + full prenorm + Supabase hash

Browse files
Files changed (2) hide show
  1. app.py +103 -168
  2. frontend/index.html +33 -171
app.py CHANGED
@@ -1,83 +1,56 @@
1
- # app.py β€” PapuaTranslate (Flask 3 + SQLAlchemy + Supabase + mT5-LoRA + prenorm)
 
2
  import os, re, logging, threading
3
  from datetime import datetime, timezone
4
  from functools import wraps
5
-
6
  from flask import (
7
  Flask, render_template, request, redirect, url_for,
8
  session, jsonify, flash
9
  )
 
10
 
11
  logging.basicConfig(level=logging.INFO, format="%(asctime)s | %(levelname)s | %(message)s")
12
- log = logging.getLogger("papua-app")
13
 
 
14
  app = Flask(__name__, template_folder="frontend", static_folder="static")
15
- app.config["SECRET_KEY"] = os.getenv("SECRET_KEY", "dev-secret-change-me")
16
- # cookie settings β€” aman untuk Spaces
17
  app.config["SESSION_COOKIE_SAMESITE"] = "Lax"
18
- app.config["SESSION_COOKIE_SECURE"] = True # Spaces via HTTPS β†’ kirim cookie
 
19
 
20
- # ================= DB =================
21
  from sqlalchemy import create_engine, Column, Integer, Text, DateTime, ForeignKey, func
22
  from sqlalchemy.orm import declarative_base, sessionmaker, scoped_session, relationship
23
 
24
- DATABASE_URL = os.getenv("DATABASE_URL") or os.getenv("DB_URL") or "sqlite:////tmp/app.db"
25
- if DATABASE_URL.startswith("postgresql") and "sslmode=" not in DATABASE_URL:
26
- DATABASE_URL += ("&" if "?" in DATABASE_URL else "?") + "sslmode=require"
27
 
28
  engine = create_engine(DATABASE_URL, pool_pre_ping=True)
29
- SessionLocal = scoped_session(sessionmaker(bind=engine, autoflush=False, autocommit=False))
30
  Base = declarative_base()
31
 
32
  class User(Base):
33
  __tablename__ = "users"
34
  id = Column(Integer, primary_key=True)
35
  email = Column(Text, unique=True, nullable=False)
36
- # DUA kolom agar kompatibel dengan skema lamamu
37
- pass_hash = Column(Text, nullable=True) # kolom baru (target utama)
38
- password = Column(Text, nullable=True) # kolom lama (legacy)
39
  created_at = Column(DateTime(timezone=True), server_default=func.now())
40
 
41
  class Translation(Base):
42
  __tablename__ = "translations"
43
  id = Column(Integer, primary_key=True)
44
- user_id = Column(Integer, ForeignKey("users.id"), nullable=False)
45
- src = Column(Text, nullable=False)
46
- mt = Column(Text, nullable=False)
47
  created_at = Column(DateTime(timezone=True), server_default=func.now())
48
- user = relationship("User")
49
-
50
- try:
51
- Base.metadata.create_all(engine)
52
- log.info("[DB] Ready: %s", DATABASE_URL)
53
- except Exception as e:
54
- log.exception("[DB] init error: %s", e)
55
-
56
- # ================= Auth =================
57
- from werkzeug.security import generate_password_hash, check_password_hash
58
-
59
- def _set_hash_fields(u: User, raw: str):
60
- h = generate_password_hash(raw)
61
- u.pass_hash = h
62
- u.password = h # isi juga kolom lama supaya tidak NULL
63
-
64
- def _get_hash(u: User) -> str | None:
65
- # utamakan pass_hash, fallback ke password
66
- return u.pass_hash or u.password
67
 
68
- def _verify(u: User, raw: str) -> bool:
69
- h = _get_hash(u)
70
- return check_password_hash(h, raw) if h else False
71
-
72
- def login_required(fn):
73
- @wraps(fn)
74
- def wrap(*a, **kw):
75
- if not session.get("uid"):
76
- return redirect(url_for("login_get"))
77
- return fn(*a, **kw)
78
- return wrap
79
 
80
- # ================= Prenorm =================
81
  PAPUA_MAP = {
82
  r"\bsa\b": "saya",
83
  r"\bko\b": "kamu",
@@ -89,77 +62,71 @@ PAPUA_MAP = {
89
  r"\bkong\b": "kemudian",
90
  }
91
  def prenorm(t: str) -> str:
92
- t = t.strip()
93
- t = re.sub(r"\s+", " ", t)
94
- for pat, repl in PAPUA_MAP.items():
95
- t = re.sub(pat, repl, t, flags=re.IGNORECASE)
96
  return t
97
 
98
- # ================= Model (lazy LoRA) =================
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
99
  from transformers import AutoTokenizer, AutoModelForSeq2SeqLM
100
  from peft import PeftModel
101
 
102
  BASE_MODEL_ID = os.getenv("BASE_MODEL_ID", "amosnbn/cendol-mt5-base-inst")
103
- ADAPTER_ID = os.getenv("ADAPTER_ID", "amosnbn/papua-lora-ckpt-168")
104
- DEVICE = os.getenv("DEVICE", "cpu")
105
- TOK = None
106
- MODEL = None
107
- _LOCK = threading.Lock()
108
-
109
- def _load_model():
110
- global TOK, MODEL
111
- log.info("[MODEL] loading base=%s adapter=%s", BASE_MODEL_ID, ADAPTER_ID)
112
- TOK = AutoTokenizer.from_pretrained(BASE_MODEL_ID)
113
- base = AutoModelForSeq2SeqLM.from_pretrained(BASE_MODEL_ID)
114
- MODEL = PeftModel.from_pretrained(base, ADAPTER_ID)
115
- MODEL.eval()
116
- if DEVICE == "cpu":
117
- MODEL.to("cpu")
118
- log.info("[MODEL] ready")
119
 
120
  def get_model():
121
- global MODEL
122
- if MODEL is None:
123
- with _LOCK:
124
- if MODEL is None:
125
- _load_model()
126
- return TOK, MODEL
127
-
128
- def translate_core(text: str, max_new: int = 48) -> str:
129
- tok, m = get_model()
130
- inp = tok([text], return_tensors="pt")
131
- out = m.generate(
132
- **inp,
133
- max_new_tokens=max_new,
134
- num_beams=4,
135
- length_penalty=0.9,
136
- no_repeat_ngram_size=3,
137
- early_stopping=True,
138
- )
139
- return tok.decode(out[0], skip_special_tokens=True)
140
-
141
- # ================ Utils & Hooks ================
142
  @app.before_request
143
- def _log_req():
144
- p = request.path
145
- if p not in ("/health", "/ping", "/favicon.ico"):
146
- log.info("[REQ] %s %s", request.method, p)
147
-
148
- @app.get("/health")
149
- @app.get("/ping")
150
- def health():
151
- return jsonify(ok=True, time=datetime.now(timezone.utc).isoformat())
152
 
153
- @app.get("/debug/session")
154
- def debug_session():
155
- return jsonify({
156
- "uid": session.get("uid"),
157
- "email": session.get("email"),
158
- "cookie_secure": app.config["SESSION_COOKIE_SECURE"],
159
- "cookie_samesite": app.config["SESSION_COOKIE_SAMESITE"],
160
- })
161
 
162
- # ================ Auth routes ================
163
  @app.get("/login")
164
  def login_get():
165
  return render_template("login.html")
@@ -167,18 +134,20 @@ def login_get():
167
  @app.post("/login")
168
  def login_post():
169
  email = (request.form.get("email") or "").strip().lower()
170
- pwd = request.form.get("password") or request.form.get("pass") or ""
171
- log.info("[AUTH] login attempt email=%s len(pwd)=%d", email, len(pwd))
172
  if not email or not pwd:
173
- flash("Isi email dan password", "error")
174
  return redirect(url_for("login_get"))
 
175
  with SessionLocal() as s:
176
  u = s.query(User).filter_by(email=email).first()
177
  if not u or not _verify(u, pwd):
178
- flash("Email atau password salah", "error")
179
  return redirect(url_for("login_get"))
180
  session["uid"] = u.id
181
  session["email"] = u.email
 
182
  return redirect(url_for("index"))
183
 
184
  @app.get("/register")
@@ -187,24 +156,22 @@ def register_get():
187
 
188
  @app.post("/register")
189
  def register_post():
190
- # TERIMA beberapa kemungkinan nama field agar match file HTML-mu
191
  email = (request.form.get("email") or "").strip().lower()
192
- pwd = (request.form.get("password") or request.form.get("pass") or "").strip()
193
- log.info("[AUTH] register email=%s len(pwd)=%d", email, len(pwd))
194
  if not email or not pwd:
195
- flash("Isi email dan password", "error")
196
  return redirect(url_for("register_get"))
 
197
  with SessionLocal() as s:
198
  if s.query(User).filter_by(email=email).first():
199
  flash("Email sudah terdaftar", "error")
200
  return redirect(url_for("login_get"))
201
  u = User(email=email)
202
- _set_hash_fields(u, pwd) # isi pass_hash & password
203
- s.add(u); s.commit(); s.refresh(u)
204
- log.info("[AUTH] user created id=%s pass_hash_set=%s password_set=%s",
205
- u.id, bool(u.pass_hash), bool(u.password))
206
- session["uid"] = u.id
207
- session["email"] = u.email
208
  return redirect(url_for("index"))
209
 
210
  @app.get("/logout")
@@ -212,56 +179,24 @@ def logout():
212
  session.clear()
213
  return redirect(url_for("login_get"))
214
 
215
- # ================ App routes ================
216
- @app.get("/")
217
- @login_required
218
- def index():
219
- device = DEVICE
220
- with SessionLocal() as s:
221
- uid = session["uid"]
222
- items = (
223
- s.query(Translation)
224
- .filter(Translation.user_id == uid)
225
- .order_by(Translation.id.desc())
226
- .limit(10).all()
227
- )
228
- recent = [{"src": it.src, "mt": it.mt, "created_at": it.created_at} for it in items]
229
- return render_template("index.html", logged_in=True, device=device, recent=recent)
230
-
231
  @app.post("/translate")
232
- def api_translate():
233
- if not session.get("uid"): # keamanan
234
- return jsonify({"error": "Unauthorized"}), 401
235
  data = request.get_json(silent=True) or {}
236
  text = (data.get("text") or "").strip()
237
- maxn = int(data.get("max_new_tokens", 48))
238
  if not text:
239
- return jsonify({"error":"Empty text"}), 400
240
- try:
241
- clean = prenorm(text)
242
- mt = translate_core(clean, max_new=maxn)
243
- with SessionLocal() as s:
244
- s.add(Translation(user_id=session["uid"], src=text, mt=mt))
245
- s.commit()
246
- return jsonify({"mt": mt})
247
- except Exception as e:
248
- log.exception("translate error: %s", e)
249
- return jsonify({"error":"server error"}), 500
250
-
251
- @app.get("/history")
252
- def api_history():
253
- if not session.get("uid"):
254
- return jsonify({"items": []})
255
  with SessionLocal() as s:
256
- uid = session["uid"]
257
- items = (
258
- s.query(Translation)
259
- .filter(Translation.user_id == uid)
260
- .order_by(Translation.id.desc())
261
- .limit(10).all()
262
- )
263
- out = [{"src": i.src, "mt": i.mt, "created_at": i.created_at.strftime("%Y-%m-%d %H:%M")} for i in items]
264
- return jsonify({"items": out})
265
 
266
  if __name__ == "__main__":
267
- app.run(host="0.0.0.0", port=int(os.getenv("PORT", "7860")), debug=True)
 
1
+ # app.py β€” PapuaTranslate (HuggingFace Spaces + Supabase + mT5-LoRA + prenorm)
2
+
3
  import os, re, logging, threading
4
  from datetime import datetime, timezone
5
  from functools import wraps
 
6
  from flask import (
7
  Flask, render_template, request, redirect, url_for,
8
  session, jsonify, flash
9
  )
10
+ from werkzeug.security import generate_password_hash, check_password_hash
11
 
12
  logging.basicConfig(level=logging.INFO, format="%(asctime)s | %(levelname)s | %(message)s")
13
+ log = logging.getLogger("papuatranslate")
14
 
15
+ # ================= APP CONFIG =================
16
  app = Flask(__name__, template_folder="frontend", static_folder="static")
17
+ app.config["SECRET_KEY"] = os.getenv("SECRET_KEY", "dev-secret-key")
 
18
  app.config["SESSION_COOKIE_SAMESITE"] = "Lax"
19
+ app.config["SESSION_COOKIE_SECURE"] = False # penting di Spaces biar cookie disimpan
20
+ app.config["PERMANENT_SESSION_LIFETIME"] = 3600 * 24
21
 
22
+ # ================= DATABASE =================
23
  from sqlalchemy import create_engine, Column, Integer, Text, DateTime, ForeignKey, func
24
  from sqlalchemy.orm import declarative_base, sessionmaker, scoped_session, relationship
25
 
26
+ DATABASE_URL = os.getenv("DATABASE_URL", "sqlite:///local.db")
27
+ if DATABASE_URL.startswith("postgresql") and "sslmode" not in DATABASE_URL:
28
+ DATABASE_URL += "?sslmode=require"
29
 
30
  engine = create_engine(DATABASE_URL, pool_pre_ping=True)
31
+ SessionLocal = scoped_session(sessionmaker(bind=engine))
32
  Base = declarative_base()
33
 
34
  class User(Base):
35
  __tablename__ = "users"
36
  id = Column(Integer, primary_key=True)
37
  email = Column(Text, unique=True, nullable=False)
38
+ pass_hash = Column(Text, nullable=True)
39
+ password = Column(Text, nullable=True)
 
40
  created_at = Column(DateTime(timezone=True), server_default=func.now())
41
 
42
  class Translation(Base):
43
  __tablename__ = "translations"
44
  id = Column(Integer, primary_key=True)
45
+ user_id = Column(Integer, ForeignKey("users.id"))
46
+ src = Column(Text)
47
+ mt = Column(Text)
48
  created_at = Column(DateTime(timezone=True), server_default=func.now())
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
49
 
50
+ Base.metadata.create_all(engine)
51
+ log.info("[DB] Ready: %s", DATABASE_URL)
 
 
 
 
 
 
 
 
 
52
 
53
+ # ================= PRENORM =================
54
  PAPUA_MAP = {
55
  r"\bsa\b": "saya",
56
  r"\bko\b": "kamu",
 
62
  r"\bkong\b": "kemudian",
63
  }
64
  def prenorm(t: str) -> str:
65
+ t = re.sub(r"\s+", " ", t.strip())
66
+ for pat, rep in PAPUA_MAP.items():
67
+ t = re.sub(pat, rep, t, flags=re.IGNORECASE)
 
68
  return t
69
 
70
+ # ================= AUTH UTILS =================
71
+ def _set_hash(user: User, raw_pwd: str):
72
+ h = generate_password_hash(raw_pwd)
73
+ user.pass_hash = h
74
+ user.password = h
75
+
76
+ def _verify(user: User, raw_pwd: str) -> bool:
77
+ return check_password_hash(user.pass_hash or user.password, raw_pwd)
78
+
79
+ def login_required(f):
80
+ @wraps(f)
81
+ def decorated(*args, **kwargs):
82
+ if not session.get("uid"):
83
+ return redirect(url_for("login_get"))
84
+ return f(*args, **kwargs)
85
+ return decorated
86
+
87
+ # ================= MODEL (lazy-load) =================
88
  from transformers import AutoTokenizer, AutoModelForSeq2SeqLM
89
  from peft import PeftModel
90
 
91
  BASE_MODEL_ID = os.getenv("BASE_MODEL_ID", "amosnbn/cendol-mt5-base-inst")
92
+ ADAPTER_ID = os.getenv("ADAPTER_ID", "amosnbn/papua-lora-ckpt-168")
93
+ DEVICE = os.getenv("DEVICE", "cpu")
94
+ _tok, _model, _lock = None, None, threading.Lock()
 
 
 
 
 
 
 
 
 
 
 
 
 
95
 
96
  def get_model():
97
+ global _tok, _model
98
+ if _model is None:
99
+ with _lock:
100
+ if _model is None:
101
+ log.info("[MODEL] Loading base=%s adapter=%s", BASE_MODEL_ID, ADAPTER_ID)
102
+ _tok = AutoTokenizer.from_pretrained(BASE_MODEL_ID)
103
+ base = AutoModelForSeq2SeqLM.from_pretrained(BASE_MODEL_ID)
104
+ _model = PeftModel.from_pretrained(base, ADAPTER_ID)
105
+ _model.eval()
106
+ if DEVICE == "cpu": _model.to("cpu")
107
+ log.info("[MODEL] Loaded successfully")
108
+ return _tok, _model
109
+
110
+ def translate_mt(text: str, max_new=48) -> str:
111
+ tok, model = get_model()
112
+ inputs = tok([text], return_tensors="pt")
113
+ output = model.generate(**inputs, max_new_tokens=max_new, num_beams=4)
114
+ return tok.decode(output[0], skip_special_tokens=True)
115
+
116
+ # ================= ROUTES =================
 
117
  @app.before_request
118
+ def _req():
119
+ log.info("[REQ] %s %s", request.method, request.path)
 
 
 
 
 
 
 
120
 
121
+ @app.get("/")
122
+ @login_required
123
+ def index():
124
+ with SessionLocal() as s:
125
+ uid = session["uid"]
126
+ hist = s.query(Translation).filter(Translation.user_id == uid).all()
127
+ data = [{"src": h.src, "mt": h.mt} for h in hist]
128
+ return render_template("index.html", user=session.get("email"), data=data)
129
 
 
130
  @app.get("/login")
131
  def login_get():
132
  return render_template("login.html")
 
134
  @app.post("/login")
135
  def login_post():
136
  email = (request.form.get("email") or "").strip().lower()
137
+ pwd = request.form.get("password") or ""
138
+ log.info("[AUTH] login attempt %s", email)
139
  if not email or not pwd:
140
+ flash("Email atau password kosong", "error")
141
  return redirect(url_for("login_get"))
142
+
143
  with SessionLocal() as s:
144
  u = s.query(User).filter_by(email=email).first()
145
  if not u or not _verify(u, pwd):
146
+ flash("Email/password salah", "error")
147
  return redirect(url_for("login_get"))
148
  session["uid"] = u.id
149
  session["email"] = u.email
150
+ log.info("[AUTH] Login OK uid=%s", u.id)
151
  return redirect(url_for("index"))
152
 
153
  @app.get("/register")
 
156
 
157
  @app.post("/register")
158
  def register_post():
 
159
  email = (request.form.get("email") or "").strip().lower()
160
+ pwd = request.form.get("password") or ""
161
+ log.info("[AUTH] register %s", email)
162
  if not email or not pwd:
163
+ flash("Lengkapi data", "error")
164
  return redirect(url_for("register_get"))
165
+
166
  with SessionLocal() as s:
167
  if s.query(User).filter_by(email=email).first():
168
  flash("Email sudah terdaftar", "error")
169
  return redirect(url_for("login_get"))
170
  u = User(email=email)
171
+ _set_hash(u, pwd)
172
+ s.add(u); s.commit()
173
+ log.info("[AUTH] created id=%s", u.id)
174
+ session["uid"], session["email"] = u.id, u.email
 
 
175
  return redirect(url_for("index"))
176
 
177
  @app.get("/logout")
 
179
  session.clear()
180
  return redirect(url_for("login_get"))
181
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
182
  @app.post("/translate")
183
+ def translate_api():
184
+ if not session.get("uid"):
185
+ return jsonify({"error": "not logged in"}), 401
186
  data = request.get_json(silent=True) or {}
187
  text = (data.get("text") or "").strip()
 
188
  if not text:
189
+ return jsonify({"error": "empty text"}), 400
190
+ norm = prenorm(text)
191
+ result = translate_mt(norm)
 
 
 
 
 
 
 
 
 
 
 
 
 
192
  with SessionLocal() as s:
193
+ s.add(Translation(user_id=session["uid"], src=text, mt=result))
194
+ s.commit()
195
+ return jsonify({"mt": result})
196
+
197
+ @app.get("/debug")
198
+ def debug():
199
+ return jsonify(dict(session=session, cookies=request.cookies))
 
 
200
 
201
  if __name__ == "__main__":
202
+ app.run(host="0.0.0.0", port=7860, debug=True)
frontend/index.html CHANGED
@@ -1,180 +1,42 @@
1
  <!DOCTYPE html>
2
  <html lang="id">
3
  <head>
4
- <meta charset="UTF-8"/>
5
- <meta name="viewport" content="width=device-width, initial-scale=1.0"/>
6
- <title>PapuaTranslate β€” Translasi Dialek Papua</title>
7
- <style>
8
- *{margin:0;padding:0;box-sizing:border-box;font-family:'Segoe UI', Tahoma, Geneva, Verdana, sans-serif}
9
- body{background:#f8f9fa;color:#333;line-height:1.6}
10
- .container{max-width:1000px;margin:0 auto;padding:0 20px}
11
- header{background:#fff;padding:15px 0;box-shadow:0 2px 10px rgba(0,0,0,.1)}
12
- .header-content{display:flex;justify-content:space-between;align-items:center}
13
- .logo h1{font-size:24px;font-weight:700;color:#000}
14
- nav ul{display:flex;list-style:none;gap:12px}
15
- nav a{color:#000;text-decoration:none;font-weight:500;transition:.2s;padding:6px 10px;border:1px solid #000;border-radius:6px}
16
- nav a:hover{background:#000;color:#fff}
17
- .hero{background:#fff;padding:40px 0 28px;text-align:center;margin-bottom:24px;border-bottom:1px solid #eee}
18
- .hero h2{font-size:28px;margin-bottom:10px;color:#000}
19
- .translation,.info{background:#fff;padding:24px;border-radius:8px;box-shadow:0 4px 12px rgba(0,0,0,.08);margin-bottom:24px}
20
- .translation h3,.info h3{font-size:20px;margin-bottom:16px;color:#000;text-align:center}
21
- .input-group{margin-bottom:16px}
22
- .input-group label{display:block;margin-bottom:8px;color:#333;font-weight:600;font-size:15px}
23
- .input-group textarea{width:100%;padding:14px;border:1px solid #ddd;border-radius:8px;font-size:16px;background:#f9f9f9;min-height:130px;resize:vertical}
24
- .input-group textarea:focus{border-color:#444;outline:none;background:#fff}
25
- .language-label{display:flex;justify-content:space-between;margin-bottom:6px}
26
- .language-label span{font-size:13px;color:#666}
27
- .translate-button{width:100%;padding:13px;background:#000;color:#fff;border:none;border-radius:8px;font-size:16px;font-weight:600;cursor:pointer;transition:.2s;margin:8px 0 16px}
28
- .translate-button:hover{background:#333}
29
- .translate-button:disabled{background:#666;cursor:not-allowed}
30
- .result{background:#f9f9f9;padding:16px;border-radius:8px;border:1px solid #eee;min-height:90px}
31
- .result h4{font-size:14px;color:#666;margin-bottom:6px}
32
- .result-text{font-size:16px;line-height:1.6;color:#000;font-weight:500;white-space:pre-wrap}
33
- .loading{color:#666;font-style:italic}
34
- footer{background:#fff;color:#000;padding:24px 0;border-top:1px solid #eee;margin-top:24px}
35
- .footer-content{display:flex;justify-content:space-between;gap:16px;flex-wrap:wrap;margin-bottom:12px}
36
- .footer-section{flex:1;min-width:260px}
37
- .footer-section h3{font-size:16px;margin-bottom:10px;border-bottom:2px solid #000;padding-bottom:4px;display:inline-block}
38
- .footer-bottom{text-align:center;padding-top:14px;border-top:1px solid #eee;font-size:13px}
39
- .pill{display:inline-block;background:#000;color:#fff;border-radius:999px;padding:4px 10px;margin-left:8px}
40
- .history-list li{margin-left:18px;margin-bottom:6px}
41
- .note{font-size:13px;color:#666;margin-top:8px}
42
- .auth-badge{font-size:13px;color:#444}
43
- @media (max-width:768px){.header-content{flex-direction:column;gap:12px}}
44
- </style>
45
  </head>
46
  <body>
47
- <header>
48
- <div class="container header-content">
49
- <div class="logo"><h1>PapuaTranslate</h1></div>
50
- <nav>
51
- <ul>
52
- <li><a href="/">Home</a></li>
53
- <li><a href="/about">About</a></li>
54
- <li><a href="/#history">History</a></li>
55
- <li><a href="/logout">Logout</a></li>
56
- </ul>
57
- </nav>
58
- </div>
59
- </header>
60
-
61
- <section class="hero">
62
- <div class="container">
63
- <h2>Translasi Dialek Papua β†’ Bahasa Indonesia Baku</h2>
64
- <p>mT5 + LoRA (CENDOL) dengan prenorm & n-best reranking ringan.</p>
65
- </div>
66
- </section>
67
-
68
- <section class="translation">
69
- <div class="container">
70
- <h3>Alat Translasi</h3>
71
- <div class="input-group">
72
- <div class="language-label">
73
- <label for="papua-input">Bahasa Papua</label>
74
- <span>Masukkan teks berlogat Papua</span>
75
- </div>
76
- <textarea id="papua-input" placeholder="Contoh: 'sa tra tau' β†’ 'Saya tidak tahu.'"></textarea>
77
- </div>
78
- <button class="translate-button" onclick="translateText()" id="translate-btn">Terjemahkan</button>
79
- <div class="input-group">
80
- <div class="language-label">
81
- <label for="indonesia-output">Bahasa Indonesia Baku</label>
82
- <span>Hasil terjemahan</span>
83
- </div>
84
- <div class="result">
85
- <h4>Terjemahan:</h4>
86
- <div id="indonesia-output" class="result-text">Hasil terjemahan akan ditampilkan di sini...</div>
87
- </div>
88
- <p class="note">Device: {{ device }}</p>
89
- </div>
90
- </div>
91
- </section>
92
-
93
- <section id="history" class="info">
94
- <div class="container">
95
- <h3>Riwayat Terakhir</h3>
96
- <ul class="history-list" id="history-list">
97
- {% for it in recent %}
98
- <li><b>{{ it.src }}</b> β†’ {{ it.mt }} <small>({{ it.created_at }})</small></li>
99
- {% endfor %}
100
- {% if not recent %}
101
- <li class="note">(Belum ada riwayat.)</li>
102
- {% endif %}
103
- </ul>
104
- </div>
105
- </section>
106
-
107
- <footer>
108
- <div class="container">
109
- <div class="footer-content">
110
- <div class="footer-section">
111
- <h3>PapuaTranslate</h3>
112
- <p>Yogotak Hubuluk, Motok Hanorogo.</p>
113
- </div>
114
- <div class="footer-section">
115
- <h3>Info</h3>
116
- <p>Model: mT5 Base + LoRA</p>
117
- <p>Device: {{ device }}</p>
118
- </div>
119
- </div>
120
- <div class="footer-bottom">
121
- <p>&copy; 2025 PapuaTranslate.</p>
122
- </div>
123
- </div>
124
- </footer>
125
-
126
  <script>
127
- async function translateText() {
128
- const inputText = document.getElementById('papua-input').value.trim();
129
- const outputElement = document.getElementById('indonesia-output');
130
- const translateBtn = document.getElementById('translate-btn');
131
- if (!inputText) { outputElement.textContent = "Silakan masukkan teks."; return; }
132
- outputElement.innerHTML = '<span class="loading">Menerjemahkan...</span>';
133
- translateBtn.disabled = true; translateBtn.textContent = 'Menerjemahkan...';
134
- try {
135
- const r = await fetch('/translate', {
136
- method: 'POST',
137
- headers: { 'Content-Type': 'application/json' },
138
- credentials: 'same-origin',
139
- body: JSON.stringify({ text: inputText, max_new_tokens: 48 })
140
- });
141
- const data = await r.json();
142
- if (r.ok) {
143
- outputElement.textContent = data.mt;
144
- loadHistory();
145
- } else {
146
- outputElement.textContent = 'Error: ' + (data.error || 'Terjadi kesalahan');
147
- }
148
- } catch(e) {
149
- outputElement.textContent = 'Gagal terhubung ke server';
150
- } finally {
151
- translateBtn.disabled = false; translateBtn.textContent = 'Terjemahkan';
152
- }
153
- }
154
-
155
- async function loadHistory() {
156
- try {
157
- const r = await fetch('/history', { credentials: 'same-origin' });
158
- if (!r.ok) return;
159
- const j = await r.json();
160
- const ul = document.getElementById('history-list');
161
- ul.innerHTML = '';
162
- if (!j.items || j.items.length === 0) {
163
- ul.innerHTML = '<li class="note">(Belum ada riwayat.)</li>';
164
- return;
165
- }
166
- for (const it of j.items) {
167
- const li = document.createElement('li');
168
- li.innerHTML = `<b>${it.src}</b> β†’ ${it.mt} <small>(${it.created_at})</small>`;
169
- ul.appendChild(li);
170
- }
171
- } catch(e) {}
172
- }
173
-
174
- // Ctrl/Cmd + Enter untuk translate
175
- document.getElementById('papua-input').addEventListener('keydown', (e) => {
176
- if (e.key === 'Enter' && (e.ctrlKey || e.metaKey)) translateText();
177
- });
178
  </script>
179
  </body>
180
  </html>
 
1
  <!DOCTYPE html>
2
  <html lang="id">
3
  <head>
4
+ <meta charset="UTF-8">
5
+ <meta name="viewport" content="width=device-width,initial-scale=1.0">
6
+ <title>Dashboard β€” PapuaTranslate</title>
7
+ <style>
8
+ body{font-family:'Segoe UI',sans-serif;background:#fafafa;margin:0;padding:0}
9
+ header{background:#fff;box-shadow:0 2px 10px rgba(0,0,0,.1);padding:15px}
10
+ h1{margin:0}
11
+ .container{max-width:800px;margin:40px auto;background:#fff;padding:24px;border-radius:10px;box-shadow:0 2px 10px rgba(0,0,0,.05)}
12
+ textarea{width:100%;height:120px;padding:10px;border-radius:6px;border:1px solid #ccc;resize:none}
13
+ button{background:#000;color:#fff;border:0;padding:10px 16px;border-radius:6px;cursor:pointer;margin-top:10px}
14
+ .history{margin-top:24px}
15
+ .item{background:#f8f9fa;padding:10px;border-radius:6px;margin-bottom:10px}
16
+ </style>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
17
  </head>
18
  <body>
19
+ <header><h1>PapuaTranslate</h1><p><a href="/logout">Logout</a></p></header>
20
+ <div class="container">
21
+ <h2>Selamat datang, {{ user }}</h2>
22
+ <textarea id="src" placeholder="Masukkan teks Papua..."></textarea>
23
+ <button onclick="goTranslate()">Terjemahkan</button>
24
+ <pre id="res"></pre>
25
+ <div class="history">
26
+ <h3>Riwayat</h3>
27
+ {% for h in data %}
28
+ <div class="item"><b>{{ h.src }}</b><br>{{ h.mt }}</div>
29
+ {% endfor %}
30
+ </div>
31
+ </div>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
32
  <script>
33
+ async function goTranslate(){
34
+ let t=document.getElementById("src").value.trim();
35
+ if(!t)return;
36
+ let r=await fetch("/translate",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({text:t})});
37
+ let j=await r.json();
38
+ document.getElementById("res").innerText=j.mt||j.error;
39
+ }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
40
  </script>
41
  </body>
42
  </html>