Spaces:
Running
Running
Upload 2 files
Browse files- app.py +93 -28
- index.html +8 -2
app.py
CHANGED
|
@@ -347,23 +347,66 @@ def save_payments_db(db):
|
|
| 347 |
|
| 348 |
def hp(p): return hashlib.sha256(p.encode()).hexdigest()
|
| 349 |
|
| 350 |
-
#
|
| 351 |
-
|
| 352 |
-
|
| 353 |
-
|
| 354 |
-
|
| 355 |
-
|
| 356 |
-
|
| 357 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 358 |
now = time.time()
|
| 359 |
-
with
|
| 360 |
-
|
| 361 |
-
|
| 362 |
-
if
|
| 363 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 364 |
return False
|
| 365 |
times.append(now)
|
| 366 |
-
|
| 367 |
return True
|
| 368 |
|
| 369 |
ADJ = ['Red','Blue','Gold','Star','Sky','Fire','Moon','Cool','Ice','Dark','Neon','Wild']
|
|
@@ -518,7 +561,7 @@ GEMINI_MODELS_TRANSCRIPT = [
|
|
| 518 |
'gemini-2.5-flash', # primary
|
| 519 |
'gemini-3-flash', # fallback
|
| 520 |
]
|
| 521 |
-
# โโ Caption: 2.5-flash-lite primary โ 3.1-flash-lite fallback
|
| 522 |
GEMINI_MODELS_CAPTION = [
|
| 523 |
'gemini-2.5-flash-lite-preview-06-17', # primary
|
| 524 |
'gemini-3.1-flash-lite', # fallback
|
|
@@ -529,15 +572,14 @@ _mdl_cap_idx = 0
|
|
| 529 |
_mdl_lock = threading.Lock()
|
| 530 |
|
| 531 |
def next_model(purpose='transcript'):
|
| 532 |
-
"""Lucky spinner โ
|
| 533 |
models = GEMINI_MODELS_TRANSCRIPT if purpose == 'transcript' else GEMINI_MODELS_CAPTION
|
| 534 |
return list(models)
|
| 535 |
|
| 536 |
def call_api(msgs, api='Gemini', purpose='transcript'):
|
| 537 |
"""
|
| 538 |
-
purpose='transcript' โ
|
| 539 |
-
purpose='caption' โ
|
| 540 |
-
API keys: random shuffle (lucky spinner) every call.
|
| 541 |
"""
|
| 542 |
if api == 'DeepSeek':
|
| 543 |
keys, base = DEEPSEEK_KEYS, 'https://api.deepseek.com'
|
|
@@ -547,8 +589,7 @@ def call_api(msgs, api='Gemini', purpose='transcript'):
|
|
| 547 |
models = next_model(purpose) # spinner: returns rotation-ordered list
|
| 548 |
valid = [(i+1, k) for i, k in enumerate(keys) if k]
|
| 549 |
if not valid: raise Exception('No API Key available')
|
| 550 |
-
#
|
| 551 |
-
random.shuffle(valid)
|
| 552 |
last_err = None
|
| 553 |
for n, k in valid:
|
| 554 |
for mdl in models:
|
|
@@ -1010,20 +1051,44 @@ def sitemap():
|
|
| 1010 |
@app.route('/api/login', methods=['POST'])
|
| 1011 |
def api_login():
|
| 1012 |
try:
|
| 1013 |
-
|
| 1014 |
-
|
| 1015 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1016 |
except Exception as e:
|
| 1017 |
return jsonify(ok=False, msg=str(e))
|
| 1018 |
|
| 1019 |
@app.route('/api/register', methods=['POST'])
|
| 1020 |
def api_register():
|
| 1021 |
try:
|
| 1022 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1023 |
uname = (d.get('username') or '').strip() or gen_uname()
|
| 1024 |
-
pw
|
| 1025 |
-
db
|
| 1026 |
-
if uname in db['users']:
|
|
|
|
| 1027 |
db['users'][uname] = {'password': hp(pw) if pw else '', 'coins': 1,
|
| 1028 |
'created_at': datetime.now().isoformat(), 'last_login': None,
|
| 1029 |
'total_transcripts': 0, 'total_videos': 0,
|
|
|
|
| 347 |
|
| 348 |
def hp(p): return hashlib.sha256(p.encode()).hexdigest()
|
| 349 |
|
| 350 |
+
# โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
|
| 351 |
+
# SECURITY: Brute-force & rate-limit protection
|
| 352 |
+
# (1) Wrong login โ fail counter per IP โ lock 5 min after 5 fails
|
| 353 |
+
# (2) Register โ max 1 account per IP per 24 hours
|
| 354 |
+
# โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
|
| 355 |
+
import threading as _thr
|
| 356 |
+
|
| 357 |
+
_pw_fails = {} # ip -> {'count':int, 'locked_until':float}
|
| 358 |
+
_pw_lock = _thr.Lock()
|
| 359 |
+
PW_MAX_FAILS = 5
|
| 360 |
+
PW_LOCK_SECS = 300 # 5 minutes
|
| 361 |
+
|
| 362 |
+
_reg_log = {} # ip -> [ts, ...]
|
| 363 |
+
_reg_lock = _thr.Lock()
|
| 364 |
+
REG_WINDOW = 86400 # 24 hours
|
| 365 |
+
REG_MAX = 1 # accounts per IP per day
|
| 366 |
+
|
| 367 |
+
def _ip(req):
|
| 368 |
+
return (req.headers.get('X-Forwarded-For') or req.remote_addr or '').split(',')[0].strip()
|
| 369 |
+
|
| 370 |
+
def _pw_check(ip):
|
| 371 |
+
"""(allowed:bool, wait_secs:int)"""
|
| 372 |
now = time.time()
|
| 373 |
+
with _pw_lock:
|
| 374 |
+
rec = _pw_fails.get(ip, {})
|
| 375 |
+
lu = rec.get('locked_until', 0)
|
| 376 |
+
if lu > now:
|
| 377 |
+
return False, int(lu - now)
|
| 378 |
+
return True, 0
|
| 379 |
+
|
| 380 |
+
def _pw_fail(ip):
|
| 381 |
+
"""Record one failed attempt; lock if threshold reached."""
|
| 382 |
+
now = time.time()
|
| 383 |
+
with _pw_lock:
|
| 384 |
+
rec = _pw_fails.get(ip, {'count': 0, 'locked_until': 0})
|
| 385 |
+
if rec.get('locked_until', 0) < now: # not currently locked
|
| 386 |
+
rec['count'] = rec.get('count', 0) + 1
|
| 387 |
+
if rec['count'] >= PW_MAX_FAILS:
|
| 388 |
+
rec['locked_until'] = now + PW_LOCK_SECS
|
| 389 |
+
rec['count'] = 0
|
| 390 |
+
_pw_fails[ip] = rec
|
| 391 |
+
|
| 392 |
+
def _pw_remaining(ip):
|
| 393 |
+
with _pw_lock:
|
| 394 |
+
return max(0, PW_MAX_FAILS - _pw_fails.get(ip, {}).get('count', 0))
|
| 395 |
+
|
| 396 |
+
def _pw_clear(ip):
|
| 397 |
+
with _pw_lock:
|
| 398 |
+
_pw_fails.pop(ip, None)
|
| 399 |
+
|
| 400 |
+
def _reg_check(ip):
|
| 401 |
+
"""True = allowed to register."""
|
| 402 |
+
now = time.time()
|
| 403 |
+
with _reg_lock:
|
| 404 |
+
times = [t for t in _reg_log.get(ip, []) if now - t < REG_WINDOW]
|
| 405 |
+
if len(times) >= REG_MAX:
|
| 406 |
+
_reg_log[ip] = times
|
| 407 |
return False
|
| 408 |
times.append(now)
|
| 409 |
+
_reg_log[ip] = times
|
| 410 |
return True
|
| 411 |
|
| 412 |
ADJ = ['Red','Blue','Gold','Star','Sky','Fire','Moon','Cool','Ice','Dark','Neon','Wild']
|
|
|
|
| 561 |
'gemini-2.5-flash', # primary
|
| 562 |
'gemini-3-flash', # fallback
|
| 563 |
]
|
| 564 |
+
# โโ Caption: 2.5-flash-lite primary โ 3.1-flash-lite fallback
|
| 565 |
GEMINI_MODELS_CAPTION = [
|
| 566 |
'gemini-2.5-flash-lite-preview-06-17', # primary
|
| 567 |
'gemini-3.1-flash-lite', # fallback
|
|
|
|
| 572 |
_mdl_lock = threading.Lock()
|
| 573 |
|
| 574 |
def next_model(purpose='transcript'):
|
| 575 |
+
"""Lucky spinner โ 2.5-flash first, 3-flash fallback."""
|
| 576 |
models = GEMINI_MODELS_TRANSCRIPT if purpose == 'transcript' else GEMINI_MODELS_CAPTION
|
| 577 |
return list(models)
|
| 578 |
|
| 579 |
def call_api(msgs, api='Gemini', purpose='transcript'):
|
| 580 |
"""
|
| 581 |
+
purpose='transcript' โ GEMINI_MODELS_TRANSCRIPT round-robin (gemini-3-flash โ gemini-2.5-flash)
|
| 582 |
+
purpose='caption' โ GEMINI_MODELS_CAPTION round-robin (gemini-3.1-flash-lite โ gemini-2.5-flash-lite)
|
|
|
|
| 583 |
"""
|
| 584 |
if api == 'DeepSeek':
|
| 585 |
keys, base = DEEPSEEK_KEYS, 'https://api.deepseek.com'
|
|
|
|
| 589 |
models = next_model(purpose) # spinner: returns rotation-ordered list
|
| 590 |
valid = [(i+1, k) for i, k in enumerate(keys) if k]
|
| 591 |
if not valid: raise Exception('No API Key available')
|
| 592 |
+
random.shuffle(valid) # lucky spinner โ random key order every call
|
|
|
|
| 593 |
last_err = None
|
| 594 |
for n, k in valid:
|
| 595 |
for mdl in models:
|
|
|
|
| 1051 |
@app.route('/api/login', methods=['POST'])
|
| 1052 |
def api_login():
|
| 1053 |
try:
|
| 1054 |
+
ip = _ip(request)
|
| 1055 |
+
# โโ (1) Check lockout โโ
|
| 1056 |
+
allowed, wait = _pw_check(ip)
|
| 1057 |
+
if not allowed:
|
| 1058 |
+
m, s = divmod(wait, 60)
|
| 1059 |
+
return jsonify(ok=False,
|
| 1060 |
+
msg=f'โ {PW_MAX_FAILS} แแผแญแแบ แแพแฌแธแแฑแฌแแผแฑแฌแแทแบ {m} แแญแแ
แบ {s} แ
แแนแแแทแบ แ
แฑแฌแแทแบแแซแ'), 429
|
| 1061 |
+
d = request.get_json(force=True) or {}
|
| 1062 |
+
uname = d.get('username', '')
|
| 1063 |
+
pw = d.get('password', '')
|
| 1064 |
+
ok, msg, coins = login_user(uname, pw)
|
| 1065 |
+
if ok:
|
| 1066 |
+
_pw_clear(ip) # reset on success
|
| 1067 |
+
else:
|
| 1068 |
+
_pw_fail(ip)
|
| 1069 |
+
rem = _pw_remaining(ip)
|
| 1070 |
+
if rem == 0:
|
| 1071 |
+
msg = f'โ {PW_MAX_FAILS} แแผแญแแบ แแพแฌแธแแฑแฌแแผแฑแฌแแทแบ 5 แแญแแ
แบ แ
แฑแฌแแทแบแแซแ'
|
| 1072 |
+
else:
|
| 1073 |
+
msg = f'โ Username (แแญแฏแท) Password แแพแฌแธแแแบแ ({rem} แแผแญแแบ แแปแแบแแแบ)'
|
| 1074 |
+
return jsonify(ok=ok, msg=msg, coins=coins, is_admin=(uname==ADMIN_U and ok))
|
| 1075 |
except Exception as e:
|
| 1076 |
return jsonify(ok=False, msg=str(e))
|
| 1077 |
|
| 1078 |
@app.route('/api/register', methods=['POST'])
|
| 1079 |
def api_register():
|
| 1080 |
try:
|
| 1081 |
+
ip = _ip(request)
|
| 1082 |
+
# โโ (2) 1 account per IP per day โโ
|
| 1083 |
+
if not _reg_check(ip):
|
| 1084 |
+
return jsonify(ok=False,
|
| 1085 |
+
msg='โ แแ
แบแแแบแแฝแแบ account แแ
แบแแฏแแฌ แแแบแแฎแธแแญแฏแแบแแแบแ แแแแบแแผแแบ แแแบแแผแญแฏแธแ
แฌแธแแซแ'), 429
|
| 1086 |
+
d = request.get_json(force=True) or {}
|
| 1087 |
uname = (d.get('username') or '').strip() or gen_uname()
|
| 1088 |
+
pw = d.get('password', '')
|
| 1089 |
+
db = load_db()
|
| 1090 |
+
if uname in db['users']:
|
| 1091 |
+
return jsonify(ok=False, msg='โ Already exists')
|
| 1092 |
db['users'][uname] = {'password': hp(pw) if pw else '', 'coins': 1,
|
| 1093 |
'created_at': datetime.now().isoformat(), 'last_login': None,
|
| 1094 |
'total_transcripts': 0, 'total_videos': 0,
|
index.html
CHANGED
|
@@ -1639,7 +1639,10 @@ async function doLogin(){
|
|
| 1639 |
sam(d.msg||'โ Login failed',false);
|
| 1640 |
if(btn){btn.disabled=false;btn.textContent='Login';}
|
| 1641 |
}
|
| 1642 |
-
}catch{
|
|
|
|
|
|
|
|
|
|
| 1643 |
}
|
| 1644 |
async function doReg(){
|
| 1645 |
const u=document.getElementById('r-u').value.trim(),p=document.getElementById('r-p').value;
|
|
@@ -1655,7 +1658,10 @@ async function doReg(){
|
|
| 1655 |
sam(d.msg||'โ Failed',false);
|
| 1656 |
if(btn){btn.disabled=false;btn.textContent='Register';}
|
| 1657 |
}
|
| 1658 |
-
}catch{
|
|
|
|
|
|
|
|
|
|
| 1659 |
}
|
| 1660 |
async function fetchAndUpdateCoins(){
|
| 1661 |
if(!U||ISADM)return;
|
|
|
|
| 1639 |
sam(d.msg||'โ Login failed',false);
|
| 1640 |
if(btn){btn.disabled=false;btn.textContent='Login';}
|
| 1641 |
}
|
| 1642 |
+
} catch{
|
| 1643 |
+
sam('โ Connection error',false);
|
| 1644 |
+
if(btn){btn.disabled=false;btn.textContent='Login';}
|
| 1645 |
+
}
|
| 1646 |
}
|
| 1647 |
async function doReg(){
|
| 1648 |
const u=document.getElementById('r-u').value.trim(),p=document.getElementById('r-p').value;
|
|
|
|
| 1658 |
sam(d.msg||'โ Failed',false);
|
| 1659 |
if(btn){btn.disabled=false;btn.textContent='Register';}
|
| 1660 |
}
|
| 1661 |
+
} catch{
|
| 1662 |
+
sam('โ Connection error',false);
|
| 1663 |
+
if(btn){btn.disabled=false;btn.textContent='Register';}
|
| 1664 |
+
}
|
| 1665 |
}
|
| 1666 |
async function fetchAndUpdateCoins(){
|
| 1667 |
if(!U||ISADM)return;
|