Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions ISL_Web
Submodule ISL_Web added at a750fa
141 changes: 92 additions & 49 deletions app_3.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,12 +7,29 @@
import math
from pathlib import Path
from typing import Optional, List, Iterator, Tuple, Dict

from fastapi.responses import RedirectResponse
from starlette.middleware.sessions import SessionMiddleware
from fastapi import FastAPI, HTTPException, Query, Request
from fastapi.responses import HTMLResponse, StreamingResponse, FileResponse, JSONResponse
from fastapi.middleware.cors import CORSMiddleware
from passlib.context import CryptContext

# Use PBKDF2-SHA256 for password hashing to avoid bcrypt backend issues
pwd_context = CryptContext(schemes=["pbkdf2_sha256"], deprecated="auto")

# ---------------- PASSWORD HELPERS ----------------

def verify_password(plain_password: str, hashed_password: str) -> bool:
return pwd_context.verify(plain_password, hashed_password)


def require_login(request: Request) -> str:
"""Return the role of the logged-in user, or raise 401."""
role = request.session.get("role")
if not role:
raise HTTPException(status_code=401, detail="Not logged in")
return role


APP_DIR = Path(__file__).parent.resolve()
DB_PATH = APP_DIR / "annotations.db"
Expand Down Expand Up @@ -55,16 +72,11 @@

app.add_middleware(
SessionMiddleware,
secret_key="isl_annotation_secret_987654",

# ✅ Explicit cookie name
secret_key="isl_ann_2026_xK9#mP2@qR7", # changed → forces all old sessions to expire immediately
session_cookie="isl_session",

# ✅ Works well for normal browser navigation
same_site="lax",

# ✅ Set True only if you use HTTPS
https_only=False,
max_age=28800, # 8 hours — sessions expire automatically
)

app.add_middleware(
Expand Down Expand Up @@ -97,7 +109,7 @@ def init_db():
conn = db()
cur = conn.cursor()

# videos table
# videos table
cur.execute("""
CREATE TABLE IF NOT EXISTS videos (
id INTEGER PRIMARY KEY AUTOINCREMENT,
Expand All @@ -108,7 +120,7 @@ def init_db():
)
""")

# annotations table (double annotation)
# annotations table (double annotation)
cur.execute("""
CREATE TABLE IF NOT EXISTS annotations (
video_id INTEGER,
Expand All @@ -120,6 +132,15 @@ def init_db():
)
""")

# users table for login
cur.execute("""
CREATE TABLE IF NOT EXISTS users (
username TEXT PRIMARY KEY,
password_hash TEXT NOT NULL,
role TEXT NOT NULL
)
""")

# indexes
cur.execute("CREATE INDEX IF NOT EXISTS idx_videos_word_user ON videos(word, user_id)")
cur.execute("CREATE INDEX IF NOT EXISTS idx_videos_user ON videos(user_id)")
Expand Down Expand Up @@ -513,16 +534,9 @@ def startup():

@app.get("/isl/", response_class=HTMLResponse)
def index(request: Request):

# ✅ If not logged in → go to login page
return RedirectResponse(url="/isl/login")

# ✅ If logged in → load annotation tool
html = APP_DIR / "index_5.html"
if not html.exists():
return HTMLResponse("<h3>index.html not found</h3>", status_code=404)

return HTMLResponse(html.read_text(encoding="utf-8"))
if not request.session.get("role"):
return RedirectResponse(url="/isl/login")
return RedirectResponse(url="/isl/tool")

@app.get("/isl/admin", response_class=HTMLResponse)
def admin_page(request: Request):
Expand All @@ -538,18 +552,22 @@ def admin_page(request: Request):
return HTMLResponse(html.read_text(encoding="utf-8"))

@app.get("/isl/tool", response_class=HTMLResponse)
@app.get("/isl/tools", response_class=HTMLResponse)
def tool(request: Request):

# must be logged in
if not request.session.get("role"):
return RedirectResponse(url="/isl/login")

html = APP_DIR / "index_5.html"
return HTMLResponse(html.read_text(encoding="utf-8"))


@app.post("/isl/api/logout")
def logout(request: Request):
request.session.clear()
return {"ok": True}

@app.get("/isl/login", response_class=HTMLResponse)

@app.get("/isl/login_page", response_class=HTMLResponse)
def login_page():
return HTMLResponse("""
<html>
Expand Down Expand Up @@ -579,11 +597,12 @@ def login_page():
border-radius:8px;
border:none;
outline:none;
margin-bottom:10px;
text-align:center;
}
button{
padding:10px 20px;
margin-top:15px;
margin-top:10px;
border:none;
border-radius:10px;
cursor:pointer;
Expand All @@ -595,33 +614,33 @@ def login_page():
<body>
<div class="box">
<h2>Annotation Tool Login</h2>
<p>Enter key: <b>user_A1 ... user_U2</b> or <b>user_Z</b></p>

<input id="keyBox" placeholder="user_A"/>
<input id="usernameBox" placeholder="Username" />
<input id="passwordBox" type="password" placeholder="Password" />

<br/>
<button onclick="doLogin()">Sign In</button>

<p id="msg"></p>
</div>

<script>
async function doLogin(){
const key = document.getElementById("keyBox").value.trim();

// ✅ Correct API path (must include /isl/)
const username = document.getElementById("usernameBox").value.trim();
const password = document.getElementById("passwordBox").value.trim();

const res = await fetch("/isl/api/login", {
method:"POST",
headers: {"Content-Type":"application/json"},
body: JSON.stringify({key})
body: JSON.stringify({username, password})
});

if(res.ok){
// ✅ Redirect into annotation tool
window.location.href = "/isl/tool";
} else {
const data = await res.json();
document.getElementById("msg").innerText =
"❌ Invalid Key!";
"❌ " + (data.detail || "Login failed");
}
}
</script>
Expand All @@ -630,31 +649,49 @@ def login_page():
""")


# Simple alias so that both /isl/login and /isl/login_page work
@app.get("/isl/login", response_class=HTMLResponse)
def login_alias():
return login_page()


# ------------------------------------

@app.post("/isl/api/login")
async def login(data: dict, request: Request):

key = data.get("key", "").strip()
username = data.get("username", "").strip()
password = data.get("password", "").strip()

if not key.startswith("user_"):
raise HTTPException(status_code=401, detail="Invalid login key")
if not username or not password:
raise HTTPException(status_code=400, detail="Username and password required")

role = key.replace("user_", "").upper()
conn = db()
cur = conn.cursor()

# Admin
if role == "Z":
request.session["role"] = "Z"
return {"ok": True, "role": "Z"}
cur.execute("""
SELECT password_hash, role
FROM users
WHERE username = ?
""", (username,))

# Normal roles A–T
if role not in ROLE_RANGES:
raise HTTPException(status_code=401, detail="Invalid login key")
row = cur.fetchone()
conn.close()

request.session["role"] = role
return {"ok": True, "role": role}
if not row:
raise HTTPException(status_code=401, detail="Invalid username or password")

stored_hash = row["password_hash"]
role = row["role"]

if not verify_password(password, stored_hash):
raise HTTPException(status_code=401, detail="Invalid username or password")

# ✅ Store session
request.session["username"] = username
request.session["role"] = role

return {"ok": True, "role": role}


# ---------------- WHO AM I (ROLE CHECK) ----------------
Expand All @@ -672,7 +709,8 @@ def me(request: Request):
# ---------------- USER API ----------------

@app.get("/isl/api/users")
def list_users():
def list_users(request: Request):
require_login(request)
conn = db()
cur = conn.cursor()
cur.execute("SELECT DISTINCT user_id FROM videos ORDER BY user_id")
Expand Down Expand Up @@ -732,8 +770,10 @@ def user_word_detail(user_id: str, word: str, request: Request):

words = get_reference_words()

# Restrict access for normal users
# Restrict access for normal users
if role != "Z":
if role not in ROLE_RANGES:
raise HTTPException(status_code=403, detail="Invalid role")
start, end = ROLE_RANGES[role]
allowed_words = words[start:end]

Expand Down Expand Up @@ -1148,7 +1188,8 @@ def admin_review_summary(request: Request):
# ---------------- FRAMES API + SERVE ----------------

@app.get("/isl/api/frames")
def frames_api(path: str = Query(...)):
def frames_api(request: Request, path: str = Query(...)):
require_login(request)
p = Path(path)
if not p.exists() or not p.is_file():
raise HTTPException(status_code=404, detail="File not found")
Expand All @@ -1164,7 +1205,8 @@ def frames_api(path: str = Query(...)):
raise HTTPException(status_code=500, detail=f"Frame extraction failed: {e}")

@app.get("/isl/frames/{key}/{name}")
def frames_serve(key: str, name: str):
def frames_serve(request: Request, key: str, name: str):
require_login(request)
p = (FRAMES_DIR / key / name).resolve()
if not str(p).startswith(str((FRAMES_DIR / key).resolve())):
raise HTTPException(status_code=403, detail="Invalid path")
Expand All @@ -1176,6 +1218,7 @@ def frames_serve(key: str, name: str):

@app.get("/isl/media")
def media(request: Request, path: str = Query(...)):
require_login(request)
p = Path(path)
if not p.exists() or not p.is_file():
raise HTTPException(status_code=404, detail="File not found")
Expand Down
33 changes: 32 additions & 1 deletion build_mapping_multiuser.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,21 @@
"005": [
Path(r"/mnt/9a528fe4-4fe8-4dff-9a0c-8b1a3cf3d7ba/ISL_DATA_USER005/All_clips_19-01-2026"),
],
"006": [
Path(r"/mnt/9a528fe4-4fe8-4dff-9a0c-8b1a3cf3d7ba/ISL_DATA_USER006/user006_output_5-02-2026/clips"),
],
"007": [
Path(r"/mnt/9a528fe4-4fe8-4dff-9a0c-8b1a3cf3d7ba/ISL_DATA_USER007/user007_output_05-02-2026/chopped"),
],
"008": [
Path(r"/mnt/9a528fe4-4fe8-4dff-9a0c-8b1a3cf3d7ba/ISL_DATA_USER008/user008/user_008_06-02-2026/output_user008_06-02-2026"),
],
"009": [
Path(r"/mnt/9a528fe4-4fe8-4dff-9a0c-8b1a3cf3d7ba/ISL_DATA_USER009/User_009_06-02-2026/Clips"),
],
"010": [
Path(r"/mnt/9a528fe4-4fe8-4dff-9a0c-8b1a3cf3d7ba/ISL_DATA_USER010/User010_06-02-2026/Clips"),
],
}

VIDEO_EXTS = {".mp4", ".mpg", ".mov", ".mkv", ".avi", ".webm"}
Expand Down Expand Up @@ -53,9 +68,25 @@ def word_from_collected_filename(filename: str) -> str:
"""
stem = Path(filename).stem.lower()

# remove common prefixes like user001_, u001_, etc.
# remove common prefixes like user001_, u001_
stem = re.sub(r"^(user|u)\d{1,3}[_-]*", "", stem)

# CASE 1: word__session123__clip001 → word
m = re.match(r"([a-z]+)__", stem)
if m:
return m.group(1)

# CASE 2: something_word.mp4 → word (for user007)
m = re.search(r"_([a-z]+)$", stem)
if m:
return m.group(1)

# CASE 3: word__000001.mp4 → word (user009/010)
m = re.match(r"([a-z]+)__", stem)
if m:
return m.group(1)

# CASE 4: fallback → first letters at start
m = re.match(r"([a-z]+)", stem)
return m.group(1) if m else ""

Expand Down
8 changes: 6 additions & 2 deletions index_5.html
Original file line number Diff line number Diff line change
Expand Up @@ -251,7 +251,7 @@

<input id="wordSearch" type="text" placeholder="Type word + Enter" />

<a class="btn primary" href="/isl/api/export.csv" target="_blank">⬇ Download CSV</a>
<a class="btn primary" href="/isl/api/admin/export_all.csv" target="_blank">⬇ Download CSV</a>

<!-- ✅ Admin Review Button -->
<button class="btn bad" id="adminReviewBtn" style="display:none;">Admin Review</button>
Expand Down Expand Up @@ -347,8 +347,12 @@ <h2>Collected Clips (Selected User)</h2>

async function loadUsers(){

// ✅ STEP 3: Detect Admin Role correctly
// Check authentication — redirect to login if not logged in
const meRes = await fetch(apiUrl("/api/me"));
if (!meRes.ok) {
window.location.href = "/isl/login";
return;
}
const meData = await meRes.json();

if (meData.role === "Z") {
Expand Down
Loading