Navid Arabi commited on
Commit
bc1cd44
·
1 Parent(s): 0fb23b8
app.py CHANGED
@@ -5,9 +5,11 @@ from pathlib import Path
5
  from utils.logger import Logger
6
  from components.login_page import LoginPage
7
  from components.dashboard_page import DashboardPage
 
8
  from config import conf
9
 
10
  log = Logger()
 
11
  CSS_FILE = Path(__file__).parent / "assets" / "styles.css"
12
  custom_css = CSS_FILE.read_text(encoding="utf-8")
13
 
 
5
  from utils.logger import Logger
6
  from components.login_page import LoginPage
7
  from components.dashboard_page import DashboardPage
8
+ from utils.database import initialize_database
9
  from config import conf
10
 
11
  log = Logger()
12
+ initialize_database()
13
  CSS_FILE = Path(__file__).parent / "assets" / "styles.css"
14
  custom_css = CSS_FILE.read_text(encoding="utf-8")
15
 
components/header.py CHANGED
@@ -8,7 +8,6 @@ class Header:
8
  def __init__(self):
9
  with gr.Row(variant="panel", elem_classes="header-row") as self.container:
10
  self.welcome = gr.Markdown()
11
-
12
  self.logout_btn = gr.Button("Log out", scale=0, min_width=90)
13
 
14
  # ---------------- wiring ----------------
@@ -19,6 +18,7 @@ class Header:
19
  outputs=[
20
  login_page.container,
21
  dashboard_page.container,
 
22
  login_page.message,
23
  ],
24
  )
 
8
  def __init__(self):
9
  with gr.Row(variant="panel", elem_classes="header-row") as self.container:
10
  self.welcome = gr.Markdown()
 
11
  self.logout_btn = gr.Button("Log out", scale=0, min_width=90)
12
 
13
  # ---------------- wiring ----------------
 
18
  outputs=[
19
  login_page.container,
20
  dashboard_page.container,
21
+ self.welcome,
22
  login_page.message,
23
  ],
24
  )
components/login_page.py CHANGED
@@ -31,10 +31,11 @@ class LoginPage:
31
  self.login_btn.click(
32
  fn=AuthService.login,
33
  inputs=[self.username, self.password, session_state],
34
- outputs=[self.message, self.container, dashboard_page.container],
 
 
 
 
 
35
  concurrency_limit=10,
36
- ).then(
37
- lambda s: f"### User: {s.get('user', '')}",
38
- inputs=session_state,
39
- outputs=header.welcome,
40
- )
 
31
  self.login_btn.click(
32
  fn=AuthService.login,
33
  inputs=[self.username, self.password, session_state],
34
+ outputs=[
35
+ self.message, # پیام خطا یا None
36
+ self.container, # فرم لاگین (hide/show)
37
+ dashboard_page.container,# داشبورد (show/hide)
38
+ header.welcome, # متن هدر ← NEW
39
+ ],
40
  concurrency_limit=10,
41
+ )
 
 
 
 
create_new_user.py ADDED
@@ -0,0 +1,23 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from utils.database import get_db
2
+ from data.repository.annotator_repo import AnnotatorRepo
3
+ from utils.logger import Logger
4
+
5
+ log = Logger()
6
+
7
+ try:
8
+ with get_db() as session:
9
+ repo = AnnotatorRepo(session)
10
+
11
+ # ---------------------- Add new user ---------------------- #
12
+ new_user = repo.add_new_user("navid", "123")
13
+ log.info(
14
+ f"User created successfully (id={new_user.id}, username='{new_user.name}')"
15
+ )
16
+
17
+ new_user = repo.add_new_user("vargha", "111")
18
+ log.info(
19
+ f"User created successfully (id={new_user.id}, username='{new_user.name}')"
20
+ )
21
+
22
+ except Exception as exc:
23
+ log.error(f"An error occurred in user operations: {exc}")
data/models.py CHANGED
@@ -1,38 +1,99 @@
1
- from sqlalchemy import Column, Integer, String, Float, Boolean, DateTime, ForeignKey
 
 
 
 
 
 
 
 
 
2
  from sqlalchemy.orm import relationship, declarative_base
3
 
4
  Base = declarative_base()
5
 
6
-
 
 
7
  class TTSData(Base):
8
  __tablename__ = "tts_data"
9
 
10
  id = Column(Integer, primary_key=True)
11
- filename = Column(String, nullable=False, unique=True)
12
- sentence = Column(String, nullable=False)
13
 
14
 
 
 
 
15
  class Annotator(Base):
16
  __tablename__ = "annotators"
17
 
18
  id = Column(Integer, primary_key=True)
19
- name = Column(String, nullable=False, unique=True)
20
- password = Column(String, nullable=False)
21
  last_login = Column(DateTime)
22
  is_active = Column(Boolean, default=True)
23
- annotations = relationship("Annotation", backref="annotator")
24
- validators = relationship("Validator", backref="annotator")
25
- annotation_intervals = relationship("AnnotationInterval", backref="annotator")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
26
 
27
 
 
 
 
28
  class Validator(Base):
29
  __tablename__ = "validators"
30
 
31
  id = Column(Integer, primary_key=True)
32
- annotator_id = Column(Integer, ForeignKey("annotators.id"))
33
- validator_id = Column(Integer, ForeignKey("annotators.id"))
 
 
 
 
 
 
 
 
 
 
 
 
 
 
34
 
35
 
 
 
 
36
  class AnnotationInterval(Base):
37
  __tablename__ = "annotation_intervals"
38
 
@@ -41,21 +102,33 @@ class AnnotationInterval(Base):
41
  start_index = Column(Integer, nullable=True)
42
  end_index = Column(Integer, nullable=True)
43
 
 
 
44
 
 
 
 
45
  class Annotation(Base):
46
  __tablename__ = "annotations"
47
 
48
  id = Column(Integer, primary_key=True)
49
- annotated_sentence = Column(String, nullable=False)
50
  validated = Column(Boolean, nullable=False)
51
  annotated_at = Column(DateTime, nullable=False)
52
  annotator_id = Column(Integer, ForeignKey("annotators.id"))
53
- audio_trims = relationship("AudioTrim", cascade="all, delete", backref="annotation")
 
 
 
 
54
  validations = relationship(
55
- "Validation", cascade="all, delete", backref="annotation"
56
  )
57
 
58
 
 
 
 
59
  class AudioTrim(Base):
60
  __tablename__ = "audio_trims"
61
 
@@ -64,7 +137,12 @@ class AudioTrim(Base):
64
  end = Column(Float, nullable=False)
65
  annotation_id = Column(Integer, ForeignKey("annotations.id"))
66
 
 
67
 
 
 
 
 
68
  class Validation(Base):
69
  __tablename__ = "validations"
70
 
@@ -72,5 +150,8 @@ class Validation(Base):
72
  annotation_id = Column(Integer, ForeignKey("annotations.id"))
73
  validator_id = Column(Integer, ForeignKey("validators.id"))
74
  validated = Column(Boolean, nullable=False)
75
- description = Column(String, nullable=True)
76
  validated_at = Column(DateTime, nullable=False)
 
 
 
 
1
+ from sqlalchemy import (
2
+ Column,
3
+ Integer,
4
+ String,
5
+ Float,
6
+ Boolean,
7
+ DateTime,
8
+ ForeignKey,
9
+ Text,
10
+ )
11
  from sqlalchemy.orm import relationship, declarative_base
12
 
13
  Base = declarative_base()
14
 
15
+ # --------------------------------------------------------------------------- #
16
+ # TTSData #
17
+ # --------------------------------------------------------------------------- #
18
  class TTSData(Base):
19
  __tablename__ = "tts_data"
20
 
21
  id = Column(Integer, primary_key=True)
22
+ filename = Column(String(255), nullable=False, unique=True)
23
+ sentence = Column(Text, nullable=False)
24
 
25
 
26
+ # --------------------------------------------------------------------------- #
27
+ # Annotator #
28
+ # --------------------------------------------------------------------------- #
29
  class Annotator(Base):
30
  __tablename__ = "annotators"
31
 
32
  id = Column(Integer, primary_key=True)
33
+ name = Column(String(100), nullable=False, unique=True)
34
+ password = Column(String(255), nullable=False)
35
  last_login = Column(DateTime)
36
  is_active = Column(Boolean, default=True)
37
+
38
+ # رابطه با Annotation
39
+ annotations = relationship(
40
+ "Annotation",
41
+ back_populates="annotator",
42
+ cascade="all, delete-orphan",
43
+ )
44
+
45
+ # رابطه با Validator ـ نقش «کسی که کارش ارزیابی می‌شود»
46
+ validation_records = relationship(
47
+ "Validator",
48
+ back_populates="annotated_annotator",
49
+ foreign_keys="Validator.annotator_id",
50
+ cascade="all, delete-orphan",
51
+ )
52
+
53
+ # رابطه با Validator ـ نقش «کسی که اعتبارسنجی می‌کند»
54
+ validations_done = relationship(
55
+ "Validator",
56
+ back_populates="validator_user",
57
+ foreign_keys="Validator.validator_id",
58
+ cascade="all, delete-orphan",
59
+ )
60
+
61
+ # بازه‌های واگذاری کار
62
+ annotation_intervals = relationship(
63
+ "AnnotationInterval",
64
+ back_populates="annotator",
65
+ cascade="all, delete-orphan",
66
+ )
67
 
68
 
69
+ # --------------------------------------------------------------------------- #
70
+ # Validator #
71
+ # --------------------------------------------------------------------------- #
72
  class Validator(Base):
73
  __tablename__ = "validators"
74
 
75
  id = Column(Integer, primary_key=True)
76
+ # کسی که جمله را حاشیه‌نویسی کرده
77
+ annotator_id = Column(Integer, ForeignKey("annotators.id"), nullable=False)
78
+ # کسی که آن را اعتبارسنجی کرده
79
+ validator_id = Column(Integer, ForeignKey("annotators.id"), nullable=False)
80
+
81
+ # رابطه‌ها
82
+ annotated_annotator = relationship(
83
+ "Annotator",
84
+ foreign_keys=[annotator_id],
85
+ back_populates="validation_records",
86
+ )
87
+ validator_user = relationship(
88
+ "Annotator",
89
+ foreign_keys=[validator_id],
90
+ back_populates="validations_done",
91
+ )
92
 
93
 
94
+ # --------------------------------------------------------------------------- #
95
+ # AnnotationInterval #
96
+ # --------------------------------------------------------------------------- #
97
  class AnnotationInterval(Base):
98
  __tablename__ = "annotation_intervals"
99
 
 
102
  start_index = Column(Integer, nullable=True)
103
  end_index = Column(Integer, nullable=True)
104
 
105
+ annotator = relationship("Annotator", back_populates="annotation_intervals")
106
+
107
 
108
+ # --------------------------------------------------------------------------- #
109
+ # Annotation #
110
+ # --------------------------------------------------------------------------- #
111
  class Annotation(Base):
112
  __tablename__ = "annotations"
113
 
114
  id = Column(Integer, primary_key=True)
115
+ annotated_sentence = Column(Text, nullable=False)
116
  validated = Column(Boolean, nullable=False)
117
  annotated_at = Column(DateTime, nullable=False)
118
  annotator_id = Column(Integer, ForeignKey("annotators.id"))
119
+
120
+ annotator = relationship("Annotator", back_populates="annotations")
121
+ audio_trims = relationship(
122
+ "AudioTrim", cascade="all, delete-orphan", back_populates="annotation"
123
+ )
124
  validations = relationship(
125
+ "Validation", cascade="all, delete-orphan", back_populates="annotation"
126
  )
127
 
128
 
129
+ # --------------------------------------------------------------------------- #
130
+ # AudioTrim #
131
+ # --------------------------------------------------------------------------- #
132
  class AudioTrim(Base):
133
  __tablename__ = "audio_trims"
134
 
 
137
  end = Column(Float, nullable=False)
138
  annotation_id = Column(Integer, ForeignKey("annotations.id"))
139
 
140
+ annotation = relationship("Annotation", back_populates="audio_trims")
141
 
142
+
143
+ # --------------------------------------------------------------------------- #
144
+ # Validation #
145
+ # --------------------------------------------------------------------------- #
146
  class Validation(Base):
147
  __tablename__ = "validations"
148
 
 
150
  annotation_id = Column(Integer, ForeignKey("annotations.id"))
151
  validator_id = Column(Integer, ForeignKey("validators.id"))
152
  validated = Column(Boolean, nullable=False)
153
+ description = Column(Text, nullable=True)
154
  validated_at = Column(DateTime, nullable=False)
155
+
156
+ annotation = relationship("Annotation", back_populates="validations")
157
+ validator = relationship("Validator")
data/repository/annotator_repo.py ADDED
@@ -0,0 +1,80 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from typing import Optional
2
+ from sqlalchemy.orm import Session
3
+
4
+ from data.models import Annotator
5
+ from utils.logger import Logger
6
+ from utils.security import hash_password
7
+
8
+ log = Logger()
9
+
10
+
11
+ class AnnotatorRepo:
12
+ """
13
+ Data-Access-Layer for Annotator table.
14
+ """
15
+
16
+ def __init__(self, db: Session) -> None:
17
+ self.db = db
18
+
19
+ # ------------------------------------------------------------------ #
20
+ # READ METHODS
21
+ # ------------------------------------------------------------------ #
22
+ def get_user_by_username(self, username: str) -> Optional[Annotator]:
23
+ try:
24
+ return (
25
+ self.db.query(Annotator)
26
+ .filter(Annotator.name == username)
27
+ .first()
28
+ )
29
+ except Exception as exc:
30
+ log.error(f"Unable to fetch user <username={username}> : {exc}")
31
+ raise
32
+
33
+ def get_user_by_id(self, user_id: int) -> Optional[Annotator]:
34
+ try:
35
+ return (
36
+ self.db.query(Annotator)
37
+ .filter(Annotator.id == user_id)
38
+ .first()
39
+ )
40
+ except Exception as exc:
41
+ log.error(f"Unable to fetch user <id={user_id}> : {exc}")
42
+ raise
43
+
44
+ # ------------------------------------------------------------------ #
45
+ # WRITE METHODS
46
+ # ------------------------------------------------------------------ #
47
+ def add_new_user(
48
+ self,
49
+ username: str,
50
+ password: str,
51
+ *,
52
+ is_active: bool = True,
53
+ ) -> Annotator:
54
+ """
55
+ Create a new Annotator with a hashed password.
56
+ Raises:
57
+ ValueError: if username already exists.
58
+ """
59
+ try:
60
+ if self.get_user_by_username(username):
61
+ raise ValueError(f"Username `{username}` already exists.")
62
+
63
+ # ------------------ HASH PASSWORD ------------------ #
64
+ hashed_pass = hash_password(password)
65
+
66
+ user = Annotator(
67
+ name=username,
68
+ password=hashed_pass,
69
+ is_active=is_active,
70
+ )
71
+ self.db.add(user)
72
+ self.db.flush() # Ensure PK generated
73
+ self.db.refresh(user)
74
+
75
+ log.info(f"New user created <id={user.id} username={username}>")
76
+ return user
77
+ except Exception as exc:
78
+ self.db.rollback()
79
+ log.error(f"Unable to create user `{username}` : {exc}")
80
+ raise
requirements.txt CHANGED
@@ -4,4 +4,6 @@ pydantic
4
  mysql-connector-python
5
  soundfile
6
  librosa
7
- pydantic-settings
 
 
 
4
  mysql-connector-python
5
  soundfile
6
  librosa
7
+ pydantic-settings
8
+ pymysql
9
+ bcrypt
utils/auth.py CHANGED
@@ -1,42 +1,65 @@
1
- # utils/auth.py
2
-
3
  import gradio as gr
4
  from utils.logger import Logger
 
 
 
5
 
6
  log = Logger()
7
 
8
 
9
  class AuthService:
10
- _USERS = {"u1": "123", "u2": "234"}
 
 
11
 
12
- # ─────────────────── core actions ────────────────────
13
  @staticmethod
14
  def login(username: str, password: str, session: dict):
15
  log.info(f"Login attempt: username={username}")
16
- if AuthService._USERS.get(username) != password:
17
- log.warning(f"Failed login for username={username}")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
18
  return (
19
- "❌ Wrong username or password!",
20
- gr.update(), # keep login form visible
21
- gr.update(visible=False), # hide dashboard
 
22
  )
23
 
24
- # success
25
- session["user"] = username
26
- log.info(f"User '{username}' logged in successfully.")
27
- return (
28
- None,
29
- gr.update(visible=False), # hide login form
30
- gr.update(visible=True), # show dashboard
31
- )
32
-
33
  @staticmethod
34
  def logout(session: dict):
35
- username = session.get("user", "unknown")
36
  session.clear()
37
  log.info(f"User '{username}' logged out.")
38
  return (
39
- gr.update(visible=True), # show login page
40
- gr.update(visible=False), # hide dashboard
41
- gr.update(value=""), # clear old messages
 
42
  )
 
 
 
1
  import gradio as gr
2
  from utils.logger import Logger
3
+ from utils.database import get_db
4
+ from data.repository.annotator_repo import AnnotatorRepo
5
+ from utils.security import verify_password
6
 
7
  log = Logger()
8
 
9
 
10
  class AuthService:
11
+ """
12
+ Authenticate users against DB and drive Gradio UI states.
13
+ """
14
 
15
+ # --------------- LOGIN --------------- #
16
  @staticmethod
17
  def login(username: str, password: str, session: dict):
18
  log.info(f"Login attempt: username={username}")
19
+
20
+ with get_db() as db:
21
+ repo = AnnotatorRepo(db)
22
+ user = repo.get_user_by_username(username)
23
+
24
+ if user is None or not user.is_active:
25
+ log.warning(
26
+ f"Failed login for username='{username}' (not found / inactive)."
27
+ )
28
+ return (
29
+ "❌ Wrong username or password!",
30
+ gr.update(),
31
+ gr.update(visible=False),
32
+ gr.update(value=""),
33
+ )
34
+
35
+ if not verify_password(password, user.password):
36
+ log.warning(f"Failed login; bad password for '{username}'.")
37
+ return (
38
+ "❌ Wrong username or password!",
39
+ gr.update(),
40
+ gr.update(visible=False),
41
+ gr.update(value=""),
42
+ )
43
+
44
+ session["user_id"] = user.id
45
+ session["username"] = user.name
46
+ log.info(f"User '{username}' logged in successfully.")
47
  return (
48
+ None,
49
+ gr.update(visible=False),
50
+ gr.update(visible=True),
51
+ gr.update(value=f"👋 Welcome, {user.name}!"),
52
  )
53
 
54
+ # --------------- LOGOUT --------------- #
 
 
 
 
 
 
 
 
55
  @staticmethod
56
  def logout(session: dict):
57
+ username = session.get("username", "unknown")
58
  session.clear()
59
  log.info(f"User '{username}' logged out.")
60
  return (
61
+ gr.update(visible=True),
62
+ gr.update(visible=False),
63
+ gr.update(value=""),
64
+ gr.update(value=""),
65
  )
utils/database.py CHANGED
@@ -1,3 +1,5 @@
 
 
1
  from sqlalchemy import create_engine
2
  from sqlalchemy.orm import sessionmaker
3
  from contextlib import contextmanager
 
1
+ # utils/database.py
2
+
3
  from sqlalchemy import create_engine
4
  from sqlalchemy.orm import sessionmaker
5
  from contextlib import contextmanager
utils/security.py ADDED
@@ -0,0 +1,29 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import bcrypt
2
+ from utils.logger import Logger
3
+
4
+ log = Logger()
5
+
6
+
7
+ def hash_password(plain_password: str) -> str:
8
+ """
9
+ Hash a plaintext password using bcrypt.
10
+ """
11
+ try:
12
+ hashed = bcrypt.hashpw(plain_password.encode("utf-8"), bcrypt.gensalt())
13
+ return hashed.decode("utf-8")
14
+ except Exception as exc:
15
+ log.error(f"Password hashing failed: {exc}")
16
+ raise
17
+
18
+
19
+ def verify_password(plain_password: str, hashed_password: str) -> bool:
20
+ """
21
+ Verify a plaintext password against its bcrypt hash.
22
+ """
23
+ try:
24
+ return bcrypt.checkpw(
25
+ plain_password.encode("utf-8"), hashed_password.encode("utf-8")
26
+ )
27
+ except Exception as exc:
28
+ log.error(f"Password verification failed: {exc}")
29
+ return False