amosnbn commited on
Commit
56bbfa7
·
1 Parent(s): 6703c65

add app.py, Dockerfile, requirements, frontend, static

Browse files
Files changed (4) hide show
  1. Dockerfile +9 -0
  2. app.py +465 -0
  3. frontend/index.html +16 -0
  4. requirements.txt +11 -0
Dockerfile ADDED
@@ -0,0 +1,9 @@
 
 
 
 
 
 
 
 
 
 
1
+ FROM python:3.11-slim
2
+ ENV DEBIAN_FRONTEND=noninteractive PIP_DISABLE_PIP_VERSION_CHECK=1 PYTHONDONTWRITEBYTECODE=1 PYTHONUNBUFFERED=1
3
+ RUN apt-get update && apt-get install -y --no-install-recommends build-essential git curl libpq-dev && rm -rf /var/lib/apt/lists/*
4
+ WORKDIR /app
5
+ COPY requirements.txt .
6
+ RUN pip install --no-cache-dir -r requirements.txt
7
+ COPY . .
8
+ ENV PORT=7860
9
+ CMD ["bash","-lc","exec gunicorn -w 1 -k gthread -t 180 -b 0.0.0.0:$PORT app:app"]
app.py ADDED
@@ -0,0 +1,465 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # app.py
2
+ import os, re
3
+ from typing import Optional, List, Tuple
4
+
5
+ from flask import (
6
+ Flask, request, jsonify, redirect, url_for, render_template
7
+ )
8
+ from flask_login import (
9
+ LoginManager, UserMixin, login_user, logout_user,
10
+ current_user, login_required
11
+ )
12
+ from werkzeug.security import generate_password_hash, check_password_hash
13
+
14
+ # ====== DB (PostgreSQL via SQLAlchemy) ======
15
+ from sqlalchemy import create_engine, Column, Integer, String, Text, DateTime, ForeignKey, func
16
+ from sqlalchemy.orm import declarative_base, sessionmaker, relationship, scoped_session
17
+
18
+ # ====== Model MT ======
19
+ import torch
20
+ from transformers import AutoTokenizer, AutoModelForSeq2SeqLM
21
+ from peft import PeftModel
22
+
23
+ # ---------------------------------------------------------
24
+ # KONFIG APLIKASI (gunakan ENV saat deploy)
25
+ # ---------------------------------------------------------
26
+ BASE_MODEL_ID = os.getenv("BASE_MODEL_ID", "amosnbn/cendol-mt5-base-inst")
27
+ ADAPTER_ID = os.getenv("ADAPTER_ID", "amosnbn/papua-lora-ckpt-168") # kosongkan "" bila tanpa adapter
28
+ HF_TOKEN = os.getenv("HF_TOKEN") or None # JANGAN hardcode token di sini
29
+
30
+ DATABASE_URL = os.getenv(
31
+ "DATABASE_URL",
32
+ "postgresql+psycopg2://papua_user:nababan04@localhost:5432/papua_db"
33
+ )
34
+ SECRET_KEY = os.getenv("SECRET_KEY", "dev-secret-please-change")
35
+ DEVICE = "cuda" if torch.cuda.is_available() else "cpu"
36
+
37
+ # Templating & static
38
+ app = Flask(
39
+ __name__,
40
+ template_folder="frontend", # tempat about.html, index.html, login.html, register.html
41
+ static_folder="static" # tempat /static/...
42
+ )
43
+ app.secret_key = SECRET_KEY
44
+ app.config.update(SESSION_COOKIE_SAMESITE="Lax", SESSION_COOKIE_SECURE=False)
45
+
46
+ # ---------------------------------------------------------
47
+ # DB SETUP
48
+ # ---------------------------------------------------------
49
+ engine = create_engine(DATABASE_URL, pool_pre_ping=True)
50
+ SessionLocal = scoped_session(sessionmaker(bind=engine, autoflush=False, autocommit=False))
51
+ Base = declarative_base()
52
+
53
+ class User(Base, UserMixin):
54
+ __tablename__ = "users"
55
+ id = Column(Integer, primary_key=True)
56
+ email = Column(String(255), unique=True, nullable=False, index=True)
57
+ pass_hash = Column(String(255), nullable=False)
58
+ created_at = Column(DateTime, server_default=func.now())
59
+ histories = relationship("History", back_populates="user", cascade="all,delete")
60
+
61
+ class History(Base):
62
+ __tablename__ = "history"
63
+ id = Column(Integer, primary_key=True)
64
+ user_id = Column(Integer, ForeignKey("users.id"), nullable=True, index=True)
65
+ src = Column(Text, nullable=False)
66
+ mt = Column(Text, nullable=False)
67
+ created_at = Column(DateTime, server_default=func.now())
68
+ user = relationship("User", back_populates="histories")
69
+
70
+ def init_db():
71
+ Base.metadata.create_all(bind=engine)
72
+ print(f"[DB] Ready: {DATABASE_URL}")
73
+
74
+ def db():
75
+ return SessionLocal()
76
+
77
+ # ---------------------------------------------------------
78
+ # LOGIN MANAGER
79
+ # ---------------------------------------------------------
80
+ login_manager = LoginManager()
81
+ login_manager.login_view = "login_get"
82
+ login_manager.init_app(app)
83
+
84
+ @login_manager.user_loader
85
+ def load_user(user_id: str) -> Optional[User]:
86
+ s = db()
87
+ try:
88
+ return s.get(User, int(user_id))
89
+ finally:
90
+ s.close()
91
+
92
+ # ---------------------------------------------------------
93
+ # UTIL RESPON
94
+ # ---------------------------------------------------------
95
+ def wants_json_response() -> bool:
96
+ if request.is_json:
97
+ return True
98
+ accept = request.headers.get("Accept", "")
99
+ return "application/json" in accept.lower()
100
+
101
+ # ---------------------------------------------------------
102
+ # TRANSLATION UTIL (prenorm, prompt, rerank)
103
+ # ---------------------------------------------------------
104
+ def strip_spaces(s: str) -> str:
105
+ return re.sub(r"\s+", " ", s).strip()
106
+
107
+ def make_prompt(src: str) -> str:
108
+ return f"Instruksi: Terjemahkan ke bahasa Indonesia baku.\nTeks: {src}\nKeluaran:"
109
+
110
+ PRE_NORM = [
111
+ (r"\bngana\b", "kamu"),
112
+ (r"\bkoi\b", "kamu"),
113
+ (r"\bdorang\b", "mereka"),
114
+ (r"\btong\b", "kita"),
115
+ (r"\bgosi\b", "kemaluan pria"),
116
+ (r"\bpace\b", "pace"),
117
+ (r"\bde\b", "dia"),
118
+ (r"\bsa\b", "saya"),
119
+ (r"\bko\b", "kamu"),
120
+ (r"\bkam\b", "kalian"),
121
+ (r"\bmo\b", "mau"),
122
+ (r"\bsu\b", "sudah"),
123
+ (r"\btra\b", "tidak"),
124
+ (r"\btrada\b", "tidak ada"),
125
+ (r"\btrakan\b", "tidak akan"),
126
+ (r"\bbikin\b", "buat"),
127
+ (r"\bpigi\b", "pergi"),
128
+ (r"\bdeng\b", "dengan"),
129
+ (r"\bdimana\b", "di mana"),
130
+ (r"\bsampe\b", "sampai"),
131
+ (r"\bstlelan\b", "setelan"),
132
+ (r"\bpipi\s+congka\b", "lesung pipi"),
133
+ (r"\btoki\b", "ikuti"),
134
+ (r"\b([A-Za-zÀ-ÖØ-öø-ÿ]+)\s+e\b", r"\1"),
135
+ ]
136
+
137
+ def prenorm(text: str) -> str:
138
+ out = text
139
+ for pat, rep in PRE_NORM:
140
+ out = re.sub(pat, rep, out, flags=re.IGNORECASE)
141
+ return strip_spaces(out)
142
+
143
+ def possessive_rewrite(src: str) -> str:
144
+ pat = re.compile(r"\b(sa|saya|ko|kamu|kita|kam|kalian|dia|mereka)\s+p(?:u|ung)\s+([A-Za-zÀ-ÖØ-öø-ÿ]+)(?:\s+(.*))?", flags=re.IGNORECASE)
145
+ def repl(m):
146
+ pron = m.group(1).lower()
147
+ noun = m.group(2)
148
+ tail = m.group(3) or ""
149
+ owner = {
150
+ "sa": "saya", "saya": "saya", "ko": "kamu", "kamu": "kamu",
151
+ "kita": "kita", "kam": "kalian", "kalian": "kalian",
152
+ "dia": "dia", "mereka": "mereka",
153
+ }.get(pron, pron)
154
+ out = f"{noun} {owner}".strip()
155
+ if tail: out += " " + tail
156
+ return out
157
+ return re.sub(pat, repl, src)
158
+
159
+ BAD_PHRASES = [
160
+ "berikut", "artikel", "seri ini", "pelajaran", "pengantar", "menurut",
161
+ "dalam perjalananku", "cerita", "kisah", "bab", "paragraf",
162
+ "air anggur", "alkohol", "anggur", "pesta",
163
+ "jawab", "jawaban", "berkata", "katanya", "mengatakan", "mengucap", "lalu", "bahwa",
164
+ "keluar", "keluaran", "keluarannya", "keluari",
165
+ "Pak ", "Bu ", "Tuan ", "Nyonya ",
166
+ "Terima kasih", "Maaf",
167
+ ]
168
+ PREFERRED_START = ["saya","aku","kamu","anda","dia","kita","kalian","mereka","apakah","tidak","ada","jangan","seperti"]
169
+
170
+ def make_bad_words_ids(tok) -> List[List[int]]:
171
+ ids = []
172
+ for phrase in BAD_PHRASES:
173
+ toks = tok(phrase, add_special_tokens=False).input_ids
174
+ if toks: ids.append(toks)
175
+ return ids
176
+
177
+ def clean_after_decode(t: str) -> str:
178
+ t = re.sub(r"\bKeluaran\b\s*:?", "", t, flags=re.IGNORECASE)
179
+ t = re.sub(r"^\s*(Terjemahan\s+Bahasa\s+Indonesia\s*:)\s*", "", t, flags=re.IGNORECASE)
180
+ return strip_spaces(t)
181
+
182
+ def first_sentence(s: str, max_words: int = 20) -> str:
183
+ s = strip_spaces(s)
184
+ s = re.split(r'\b(kata|jawab|berkata|katanya|mengatakan|mengucap|lalu|bahwa)\b', s, flags=re.IGNORECASE)[0]
185
+ s = re.split(r'[""()]', s)[0]
186
+ m = re.search(r"[.!?…]|。|?|!", s)
187
+ if m: s = s[:m.end()]
188
+ words = s.split()
189
+ if len(words) > max_words:
190
+ s = " ".join(words[:max_words])
191
+ if not re.search(r"[.!?…]$", s): s += "."
192
+ s = re.sub(r"\bkeluar\w*\b", "", s, flags=re.IGNORECASE)
193
+ return strip_spaces(s)
194
+
195
+ def score_candidate(src_raw: str, cand: str) -> Tuple[int,int,int,int]:
196
+ low = cand.lower()
197
+ bad_hits = sum(1 for w in BAD_PHRASES if w.lower() in low)
198
+ length = len(low.split())
199
+ pref_bonus = -2 if any(low.startswith(p + " ") for p in PREFERRED_START) else 0
200
+ quote_pen = 1 if any(ch in cand for ch in ['"', '(', ')', ':', ';']) else 0
201
+ return (bad_hits, length, pref_bonus, quote_pen)
202
+
203
+ def rerank_nbest(src_raw: str, cands: List[str]) -> str:
204
+ pruned = [first_sentence(clean_after_decode(c)) for c in cands]
205
+ scored = sorted(pruned, key=lambda x: score_candidate(src_raw, x))
206
+ out = scored[0].strip()
207
+ if out and out[0].islower(): out = out[0].upper() + out[1:]
208
+ if out and out[-1].isalnum(): out += "."
209
+ return out.replace("..", ".")
210
+
211
+ # ---------------------------------------------------------
212
+ # LOAD MODEL
213
+ # ---------------------------------------------------------
214
+ TOK: Optional[AutoTokenizer] = None
215
+ MODEL: Optional[AutoModelForSeq2SeqLM] = None
216
+
217
+ def fix_special_tokens(model, tok):
218
+ if getattr(model.config, "decoder_start_token_id", None) is None:
219
+ model.config.decoder_start_token_id = tok.pad_token_id
220
+ if getattr(model.config, "eos_token_id", None) is None:
221
+ model.config.eos_token_id = tok.eos_token_id
222
+ if getattr(model.config, "pad_token_id", None) is None:
223
+ model.config.pad_token_id = tok.pad_token_id
224
+ model.config.forced_bos_token_id = None
225
+ model.config.forced_eos_token_id = None
226
+ model.config.use_cache = True
227
+
228
+ def load_models():
229
+ global TOK, MODEL
230
+ print(f"[INFO] Using device: {DEVICE}")
231
+ print(f"[INFO] Base model: {BASE_MODEL_ID}")
232
+ print(f"[INFO] Adapter : {ADAPTER_ID or '(none)'}")
233
+
234
+ tok = AutoTokenizer.from_pretrained(BASE_MODEL_ID, use_fast=True, token=HF_TOKEN)
235
+ base = AutoModelForSeq2SeqLM.from_pretrained(BASE_MODEL_ID, token=HF_TOKEN).to(DEVICE).eval()
236
+ fix_special_tokens(base, tok)
237
+ for p in base.parameters(): p.requires_grad = False
238
+
239
+ if ADAPTER_ID:
240
+ model = PeftModel.from_pretrained(base, ADAPTER_ID, token=HF_TOKEN).to(DEVICE).eval()
241
+ fix_special_tokens(model, tok)
242
+ for p in model.parameters(): p.requires_grad = False
243
+ else:
244
+ model = base
245
+
246
+ TOK, MODEL = tok, model
247
+ print("[INFO] Model loaded!")
248
+
249
+ def generate_nbest(source: str, *, beams=6, nbest=6, max_new=24,
250
+ rep_penalty=1.12, no_repeat=4, bad_ids=None) -> List[str]:
251
+ prompt = make_prompt(source)
252
+ enc = TOK([prompt], return_tensors="pt", padding=True, truncation=True, max_length=256).to(DEVICE)
253
+ with torch.inference_mode():
254
+ out = MODEL.generate(
255
+ **enc,
256
+ num_beams=beams,
257
+ num_return_sequences=nbest,
258
+ do_sample=False,
259
+ no_repeat_ngram_size=no_repeat,
260
+ repetition_penalty=rep_penalty,
261
+ min_new_tokens=1,
262
+ max_new_tokens=max_new,
263
+ eos_token_id=TOK.eos_token_id,
264
+ pad_token_id=TOK.pad_token_id,
265
+ bad_words_ids=bad_ids,
266
+ )
267
+ return TOK.batch_decode(out, skip_special_tokens=True)
268
+
269
+ def translate_one(raw_text: str, max_new: int = 24) -> str:
270
+ src0 = possessive_rewrite(raw_text)
271
+ src = prenorm(src0)
272
+ bad_ids = make_bad_words_ids(TOK)
273
+ cands = generate_nbest(src, beams=6, nbest=6, max_new=max_new,
274
+ rep_penalty=1.12, no_repeat=4, bad_ids=bad_ids)
275
+ out = rerank_nbest(raw_text, cands)
276
+ if raw_text.strip().endswith("?") and not out.endswith("?"):
277
+ out = out.rstrip(".") + "?"
278
+ return out
279
+
280
+ # ---------------------------------------------------------
281
+ # ROUTES
282
+ # ---------------------------------------------------------
283
+ @app.before_request
284
+ def force_login_first():
285
+ """Kalau belum login, paksa ke /login kecuali beberapa route publik."""
286
+ public_paths = {"/login", "/register", "/about", "/health"}
287
+ # izinkan akses file statis
288
+ if request.path.startswith("/static/"):
289
+ return
290
+ if request.path not in public_paths and not current_user.is_authenticated:
291
+ return redirect(url_for("login_get"))
292
+
293
+ @app.get("/")
294
+ def home():
295
+ recent = []
296
+ if current_user.is_authenticated:
297
+ s = db()
298
+ try:
299
+ rows = (
300
+ s.query(History)
301
+ .filter(History.user_id == current_user.id)
302
+ .order_by(History.created_at.desc())
303
+ .limit(10).all()
304
+ )
305
+ recent = [
306
+ {"src": h.src, "mt": h.mt, "created_at": h.created_at.strftime("%Y-%m-%d %H:%M")}
307
+ for h in rows
308
+ ]
309
+ finally:
310
+ s.close()
311
+ return render_template(
312
+ "index.html",
313
+ device=DEVICE,
314
+ logged_in=current_user.is_authenticated,
315
+ user_email=(current_user.email if current_user.is_authenticated else ""),
316
+ recent=recent
317
+ )
318
+
319
+ @app.get("/about")
320
+ def about():
321
+ return render_template("about.html", device=DEVICE)
322
+
323
+ # ---------- AUTH ----------
324
+ @app.get("/login")
325
+ def login_get():
326
+ return render_template("login.html")
327
+
328
+ @app.post("/login")
329
+ def login_post():
330
+ data = request.get_json(silent=True) or request.form
331
+ email = (data.get("email") or "").strip().lower()
332
+ password = (data.get("password") or "").strip()
333
+ if not email or not password:
334
+ if wants_json_response(): return jsonify({"error": "Email & password wajib diisi"}), 400
335
+ return redirect(url_for("login_get"))
336
+
337
+ s = db()
338
+ try:
339
+ u = s.query(User).filter_by(email=email).first()
340
+ if not u or not check_password_hash(u.pass_hash, password):
341
+ if wants_json_response(): return jsonify({"error": "Email atau password salah"}), 401
342
+ return redirect(url_for("login_get"))
343
+ login_user(u)
344
+ finally:
345
+ s.close()
346
+
347
+ if wants_json_response(): return jsonify({"ok": True})
348
+ return redirect(url_for("home"))
349
+
350
+ @app.get("/register")
351
+ def register_get():
352
+ return render_template("register.html")
353
+
354
+ @app.post("/register")
355
+ def register_post():
356
+ data = request.get_json(silent=True) or request.form
357
+ email = (data.get("email") or "").strip().lower()
358
+ password = (data.get("password") or "").strip()
359
+ if not email or not password:
360
+ if wants_json_response(): return jsonify({"error": "Email & password wajib diisi"}), 400
361
+ return redirect(url_for("register_get"))
362
+
363
+ s = db()
364
+ try:
365
+ exists = s.query(User).filter_by(email=email).first()
366
+ if exists:
367
+ if wants_json_response(): return jsonify({"error": "Email sudah terdaftar"}), 400
368
+ return redirect(url_for("login_get"))
369
+ u = User(email=email, pass_hash=generate_password_hash(password))
370
+ s.add(u); s.commit()
371
+ login_user(u)
372
+ finally:
373
+ s.close()
374
+
375
+ if wants_json_response(): return jsonify({"ok": True})
376
+ return redirect(url_for("home"))
377
+
378
+ @app.get("/logout")
379
+ def logout():
380
+ if current_user.is_authenticated:
381
+ logout_user()
382
+ return redirect(url_for("login_get"))
383
+
384
+ # ---------- API ----------
385
+ @app.post("/translate")
386
+ @login_required
387
+ def translate_api():
388
+ if TOK is None or MODEL is None:
389
+ return jsonify({"error": "Model belum siap"}), 503
390
+
391
+ data = request.get_json(force=True, silent=True) or {}
392
+ text = (data.get("text") or "").strip()
393
+ if not text:
394
+ return jsonify({"error": "field 'text' wajib diisi"}), 400
395
+
396
+ max_new = int(data.get("max_new_tokens", 24))
397
+ max_new = max(8, min(64, max_new))
398
+
399
+ try:
400
+ mt = translate_one(text, max_new=max_new)
401
+ except Exception as e:
402
+ return jsonify({"error": f"gagal generate: {e}"}), 500
403
+
404
+ s = db()
405
+ try:
406
+ s.add(History(user_id=current_user.id, src=text, mt=mt))
407
+ s.commit()
408
+ finally:
409
+ s.close()
410
+
411
+ return jsonify({"src": text, "mt": mt})
412
+
413
+ @app.get("/history")
414
+ @login_required
415
+ def history_api():
416
+ s = db()
417
+ try:
418
+ items = (
419
+ s.query(History)
420
+ .filter(History.user_id == current_user.id)
421
+ .order_by(History.created_at.desc())
422
+ .limit(50).all()
423
+ )
424
+ out = [{"src": h.src, "mt": h.mt, "created_at": h.created_at.strftime("%Y-%m-%d %H:%M")} for h in items]
425
+ return jsonify({"items": out})
426
+ finally:
427
+ s.close()
428
+
429
+ @app.get("/health")
430
+ def health():
431
+ return jsonify({
432
+ "status": "ok",
433
+ "device": DEVICE,
434
+ "db": DATABASE_URL,
435
+ "model_base": BASE_MODEL_ID,
436
+ "adapter": ADAPTER_ID,
437
+ "logged_in": current_user.is_authenticated
438
+ })
439
+
440
+ # ---------------------------------------------------------
441
+ # STARTUP (untuk server WSGI & lokal)
442
+ # ---------------------------------------------------------
443
+ @app.before_first_request
444
+ def _warmup():
445
+ # Inisialisasi DB & model saat request pertama (cocok di Gunicorn)
446
+ try:
447
+ init_db()
448
+ except Exception as e:
449
+ print(f"[DB] init error: {e}")
450
+ try:
451
+ if (globals().get("TOK") is None) or (globals().get("MODEL") is None):
452
+ load_models()
453
+ except Exception as e:
454
+ print(f"[MODEL] load error: {e}")
455
+
456
+ if __name__ == "__main__":
457
+ print("============================================================")
458
+ print("PapuaTranslate (Flask + PostgreSQL + LoRA)")
459
+ print("============================================================")
460
+ init_db()
461
+ load_models()
462
+ print("Server: http://localhost:8000")
463
+ print("Templates dir:", os.path.abspath(app.template_folder))
464
+ print("Static dir :", os.path.abspath(app.static_folder))
465
+ app.run(host="0.0.0.0", port=8000, debug=True)
frontend/index.html ADDED
@@ -0,0 +1,16 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <!doctype html><html><body>
2
+ <h1>PapuaTranslate</h1>
3
+ <form id="f">
4
+ <textarea name="text" rows="4" cols="60" placeholder="Tulis teks..."></textarea><br/>
5
+ <button type="submit">Translate</button>
6
+ </form>
7
+ <pre id="out"></pre>
8
+ <script>
9
+ document.getElementById("f").onsubmit = async (e) => {
10
+ e.preventDefault();
11
+ const text = e.target.text.value;
12
+ const r = await fetch("/translate",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({text})});
13
+ document.getElementById("out").textContent = JSON.stringify(await r.json(), null, 2);
14
+ };
15
+ </script>
16
+ </body></html>
requirements.txt ADDED
@@ -0,0 +1,11 @@
 
 
 
 
 
 
 
 
 
 
 
 
1
+ flask==3.0.0
2
+ flask-login==0.6.3
3
+ werkzeug==3.0.1
4
+ sqlalchemy==2.0.30
5
+ psycopg2-binary==2.9.9
6
+ gunicorn==21.2.0
7
+
8
+ torch
9
+ transformers==4.44.2
10
+ peft==0.11.1
11
+ accelerate==0.34.2