Spaces:
Running
Running
Navid Arabi
commited on
Commit
·
e1df50c
1
Parent(s):
d52b7fa
load annotation data done
Browse files- app.py +22 -13
- components/dashboard_page.py +136 -7
- components/login_page.py +8 -1
- data/__init__.py +0 -0
- data/models.py +9 -7
- data/repository/__init__.py +0 -0
- data/repository/annotation_interval_repo.py +88 -0
- data/repository/annotator_repo.py +17 -17
- data/repository/annotator_workload_repo.py +58 -0
- data/repository/tts_data_repo.py +19 -0
- scripts/__init__.py +0 -0
- scripts/assign_interval_to_annotator.py +33 -0
- create_new_user.py → scripts/create_new_annotator.py +2 -8
- test/__init__.py +0 -0
- utils/auth.py +105 -18
app.py
CHANGED
@@ -1,4 +1,3 @@
|
|
1 |
-
# app.py
|
2 |
import gradio as gr
|
3 |
from pathlib import Path
|
4 |
|
@@ -10,30 +9,40 @@ 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 |
|
16 |
|
17 |
def build_app() -> gr.Blocks:
|
18 |
-
|
19 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
20 |
|
21 |
gr.Markdown(f"### {conf.APP_TITLE}")
|
22 |
|
23 |
-
|
|
|
24 |
dashboard_page = DashboardPage()
|
25 |
|
26 |
-
|
27 |
-
|
|
|
28 |
|
29 |
-
|
30 |
-
|
|
|
31 |
return demo
|
32 |
|
33 |
|
34 |
if __name__ == "__main__":
|
35 |
-
try:
|
36 |
-
|
37 |
-
|
38 |
-
except Exception as err:
|
39 |
-
|
|
|
|
|
1 |
import gradio as gr
|
2 |
from pathlib import Path
|
3 |
|
|
|
9 |
|
10 |
log = Logger()
|
11 |
initialize_database()
|
12 |
+
|
13 |
CSS_FILE = Path(__file__).parent / "assets" / "styles.css"
|
14 |
custom_css = CSS_FILE.read_text(encoding="utf-8")
|
15 |
|
16 |
|
17 |
def build_app() -> gr.Blocks:
|
18 |
+
"""
|
19 |
+
میسازد و wiring کل اپلیکیشن را انجام میدهد.
|
20 |
+
"""
|
21 |
+
demo = gr.Blocks(title=conf.APP_TITLE, css=custom_css)
|
22 |
+
|
23 |
+
with demo:
|
24 |
+
# حالت سراسری برنامه بهصورت gr.State
|
25 |
+
session_state = gr.State({})
|
26 |
|
27 |
gr.Markdown(f"### {conf.APP_TITLE}")
|
28 |
|
29 |
+
# صفحات
|
30 |
+
login_page = LoginPage()
|
31 |
dashboard_page = DashboardPage()
|
32 |
|
33 |
+
# اتصال رویدادها
|
34 |
+
login_page.register_callbacks(dashboard_page, session_state)
|
35 |
+
dashboard_page.register_callbacks(login_page, session_state, demo)
|
36 |
|
37 |
+
# صف پردازش گرادیو
|
38 |
+
demo.queue(default_concurrency_limit=50)
|
39 |
+
log.info("App Started.")
|
40 |
return demo
|
41 |
|
42 |
|
43 |
if __name__ == "__main__":
|
44 |
+
# try:
|
45 |
+
log.info("Launching App ...")
|
46 |
+
build_app().launch()
|
47 |
+
# except Exception as err:
|
48 |
+
# log.error(err)
|
components/dashboard_page.py
CHANGED
@@ -1,5 +1,4 @@
|
|
1 |
# components/dashboard_page.py
|
2 |
-
|
3 |
import gradio as gr
|
4 |
from components.header import Header
|
5 |
|
@@ -7,15 +6,145 @@ from components.header import Header
|
|
7 |
class DashboardPage:
|
8 |
"""UI elements + event wiring for the post-login dashboard."""
|
9 |
|
10 |
-
|
|
|
11 |
with gr.Column(visible=False) as self.container:
|
12 |
-
#
|
13 |
self.header = Header()
|
14 |
|
15 |
-
#
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
16 |
|
17 |
-
# ─────────────────── event wiring ────────────────────
|
18 |
-
def register_callbacks(self, login_page, session_state):
|
19 |
|
20 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
21 |
self.header.register_callbacks(login_page, self, session_state)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
# components/dashboard_page.py
|
|
|
2 |
import gradio as gr
|
3 |
from components.header import Header
|
4 |
|
|
|
6 |
class DashboardPage:
|
7 |
"""UI elements + event wiring for the post-login dashboard."""
|
8 |
|
9 |
+
# ───────── ساخت UI ───────── #
|
10 |
+
def __init__(self) -> None:
|
11 |
with gr.Column(visible=False) as self.container:
|
12 |
+
# هدر بالا (نام کاربر + خروج)
|
13 |
self.header = Header()
|
14 |
|
15 |
+
# اطلاعات فایل صوتی و انوتیشن
|
16 |
+
with gr.Row():
|
17 |
+
with gr.Column():
|
18 |
+
self.tts_id = gr.Textbox(
|
19 |
+
label="tts_data.id", interactive=False
|
20 |
+
)
|
21 |
+
self.filename = gr.Textbox(
|
22 |
+
label="tts_data.filename", interactive=False
|
23 |
+
)
|
24 |
+
self.sentence = gr.Textbox(
|
25 |
+
label="tts_data.sentence", interactive=False
|
26 |
+
)
|
27 |
+
self.ann_sentence = gr.Textbox(
|
28 |
+
label="annotations.annotated_sentence",
|
29 |
+
interactive=False,
|
30 |
+
)
|
31 |
+
self.ann_at = gr.Textbox(
|
32 |
+
label="annotations.annotated_at",
|
33 |
+
interactive=False,
|
34 |
+
)
|
35 |
+
self.validated = gr.Checkbox(
|
36 |
+
label="annotations.validated",
|
37 |
+
interactive=False,
|
38 |
+
)
|
39 |
+
|
40 |
+
# دکمههای پیمایش
|
41 |
+
with gr.Row():
|
42 |
+
self.btn_prev = gr.Button("⬅️ Previous")
|
43 |
+
self.btn_next = gr.Button("Next ➡️")
|
44 |
+
|
45 |
+
# stateهای مخفی
|
46 |
+
self.items_state = gr.State([]) # list[dict]
|
47 |
+
self.idx_state = gr.State(0) # اندیس فعلی
|
48 |
+
|
49 |
|
|
|
|
|
50 |
|
51 |
+
# ───────── wiring رویدادها ───────── #
|
52 |
+
def register_callbacks(
|
53 |
+
self,
|
54 |
+
login_page, # برای اجازهی logout در Header
|
55 |
+
session_state: gr.State, # gr.State شامل دیکشنری نشست
|
56 |
+
root_blocks: gr.Blocks # بلوک ریشهی برنامه
|
57 |
+
) -> None:
|
58 |
+
|
59 |
+
# ۱) رویداد خروج (در Header)
|
60 |
self.header.register_callbacks(login_page, self, session_state)
|
61 |
+
|
62 |
+
# ---------- توابع کمکی ---------- #
|
63 |
+
def show_current(items: list, idx: int):
|
64 |
+
"""رکورد idx را روی فیلدها میریزد؛ در صورت خالی بودن لیست مقادیر تهی."""
|
65 |
+
if not items:
|
66 |
+
return ["", "", "", "", "", False]
|
67 |
+
|
68 |
+
data = items[idx]
|
69 |
+
return [
|
70 |
+
data["id"],
|
71 |
+
data["filename"],
|
72 |
+
data["sentence"],
|
73 |
+
data.get("annotated_sentence", ""),
|
74 |
+
data.get("annotated_at", ""),
|
75 |
+
bool(data.get("validated", False)),
|
76 |
+
]
|
77 |
+
|
78 |
+
def next_idx(items: list, idx: int):
|
79 |
+
return min(idx + 1, max(len(items) - 1, 0))
|
80 |
+
|
81 |
+
def prev_idx(items: list, idx: int):
|
82 |
+
return max(idx - 1, 0)
|
83 |
+
|
84 |
+
# ---------- بارگذاری اولیه (یک بار در شروع) ---------- #
|
85 |
+
def load_items(sess: dict):
|
86 |
+
items = sess.get("dashboard_items", [])
|
87 |
+
return (
|
88 |
+
items, # → items_state
|
89 |
+
0, # → idx_state
|
90 |
+
*show_current(items, 0), # → شش فیلد
|
91 |
+
)
|
92 |
+
|
93 |
+
root_blocks.load(
|
94 |
+
fn=load_items,
|
95 |
+
inputs=[session_state],
|
96 |
+
outputs=[
|
97 |
+
self.items_state,
|
98 |
+
self.idx_state,
|
99 |
+
self.tts_id,
|
100 |
+
self.filename,
|
101 |
+
self.sentence,
|
102 |
+
self.ann_sentence,
|
103 |
+
self.ann_at,
|
104 |
+
self.validated,
|
105 |
+
],
|
106 |
+
)
|
107 |
+
|
108 |
+
# ---------- دکمه «قبلی» ----------
|
109 |
+
(
|
110 |
+
self.btn_prev
|
111 |
+
.click(
|
112 |
+
fn=prev_idx,
|
113 |
+
inputs=[self.items_state, self.idx_state],
|
114 |
+
outputs=self.idx_state,
|
115 |
+
)
|
116 |
+
.then(
|
117 |
+
fn=show_current,
|
118 |
+
inputs=[self.items_state, self.idx_state],
|
119 |
+
outputs=[
|
120 |
+
self.tts_id,
|
121 |
+
self.filename,
|
122 |
+
self.sentence,
|
123 |
+
self.ann_sentence,
|
124 |
+
self.ann_at,
|
125 |
+
self.validated,
|
126 |
+
],
|
127 |
+
)
|
128 |
+
)
|
129 |
+
|
130 |
+
# ---------- دکمه «بعدی» ----------
|
131 |
+
(
|
132 |
+
self.btn_next
|
133 |
+
.click(
|
134 |
+
fn=next_idx,
|
135 |
+
inputs=[self.items_state, self.idx_state],
|
136 |
+
outputs=self.idx_state,
|
137 |
+
)
|
138 |
+
.then(
|
139 |
+
fn=show_current,
|
140 |
+
inputs=[self.items_state, self.idx_state],
|
141 |
+
outputs=[
|
142 |
+
self.tts_id,
|
143 |
+
self.filename,
|
144 |
+
self.sentence,
|
145 |
+
self.ann_sentence,
|
146 |
+
self.ann_at,
|
147 |
+
self.validated,
|
148 |
+
],
|
149 |
+
)
|
150 |
+
)
|
components/login_page.py
CHANGED
@@ -42,8 +42,15 @@ class LoginPage:
|
|
42 |
self.container,
|
43 |
dashboard_page.container,
|
44 |
header.welcome,
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
45 |
],
|
46 |
-
concurrency_limit=10,
|
47 |
)
|
48 |
.then(
|
49 |
fn=lambda: gr.update(value="Login", interactive=True),
|
|
|
42 |
self.container,
|
43 |
dashboard_page.container,
|
44 |
header.welcome,
|
45 |
+
dashboard_page.items_state,
|
46 |
+
dashboard_page.idx_state,
|
47 |
+
dashboard_page.tts_id,
|
48 |
+
dashboard_page.filename,
|
49 |
+
dashboard_page.sentence,
|
50 |
+
dashboard_page.ann_sentence,
|
51 |
+
dashboard_page.ann_at,
|
52 |
+
dashboard_page.validated,
|
53 |
],
|
|
|
54 |
)
|
55 |
.then(
|
56 |
fn=lambda: gr.update(value="Login", interactive=True),
|
data/__init__.py
ADDED
File without changes
|
data/models.py
CHANGED
@@ -22,6 +22,11 @@ class TTSData(Base):
|
|
22 |
filename = Column(String(255), nullable=False, unique=True)
|
23 |
sentence = Column(Text, nullable=False)
|
24 |
|
|
|
|
|
|
|
|
|
|
|
25 |
|
26 |
# --------------------------------------------------------------------------- #
|
27 |
# Annotator #
|
@@ -35,14 +40,12 @@ class Annotator(Base):
|
|
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",
|
@@ -50,7 +53,6 @@ class Annotator(Base):
|
|
50 |
cascade="all, delete-orphan",
|
51 |
)
|
52 |
|
53 |
-
# رابطه با Validator ـ نقش «کسی که اعتبارسنجی میکند»
|
54 |
validations_done = relationship(
|
55 |
"Validator",
|
56 |
back_populates="validator_user",
|
@@ -58,7 +60,6 @@ class Annotator(Base):
|
|
58 |
cascade="all, delete-orphan",
|
59 |
)
|
60 |
|
61 |
-
# بازههای واگذاری کار
|
62 |
annotation_intervals = relationship(
|
63 |
"AnnotationInterval",
|
64 |
back_populates="annotator",
|
@@ -73,12 +74,9 @@ 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],
|
@@ -116,8 +114,12 @@ class Annotation(Base):
|
|
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 |
)
|
|
|
22 |
filename = Column(String(255), nullable=False, unique=True)
|
23 |
sentence = Column(Text, nullable=False)
|
24 |
|
25 |
+
annotations = relationship(
|
26 |
+
"Annotation",
|
27 |
+
back_populates="tts_data",
|
28 |
+
cascade="all, delete-orphan",
|
29 |
+
)
|
30 |
|
31 |
# --------------------------------------------------------------------------- #
|
32 |
# Annotator #
|
|
|
40 |
last_login = Column(DateTime)
|
41 |
is_active = Column(Boolean, default=True)
|
42 |
|
|
|
43 |
annotations = relationship(
|
44 |
"Annotation",
|
45 |
back_populates="annotator",
|
46 |
cascade="all, delete-orphan",
|
47 |
)
|
48 |
|
|
|
49 |
validation_records = relationship(
|
50 |
"Validator",
|
51 |
back_populates="annotated_annotator",
|
|
|
53 |
cascade="all, delete-orphan",
|
54 |
)
|
55 |
|
|
|
56 |
validations_done = relationship(
|
57 |
"Validator",
|
58 |
back_populates="validator_user",
|
|
|
60 |
cascade="all, delete-orphan",
|
61 |
)
|
62 |
|
|
|
63 |
annotation_intervals = relationship(
|
64 |
"AnnotationInterval",
|
65 |
back_populates="annotator",
|
|
|
74 |
__tablename__ = "validators"
|
75 |
|
76 |
id = Column(Integer, primary_key=True)
|
|
|
77 |
annotator_id = Column(Integer, ForeignKey("annotators.id"), nullable=False)
|
|
|
78 |
validator_id = Column(Integer, ForeignKey("annotators.id"), nullable=False)
|
79 |
|
|
|
80 |
annotated_annotator = relationship(
|
81 |
"Annotator",
|
82 |
foreign_keys=[annotator_id],
|
|
|
114 |
validated = Column(Boolean, nullable=False)
|
115 |
annotated_at = Column(DateTime, nullable=False)
|
116 |
annotator_id = Column(Integer, ForeignKey("annotators.id"))
|
117 |
+
tts_data_id = Column(Integer, ForeignKey("tts_data.id"))
|
118 |
+
|
119 |
|
120 |
annotator = relationship("Annotator", back_populates="annotations")
|
121 |
+
tts_data = relationship("TTSData", back_populates="annotations")
|
122 |
+
|
123 |
audio_trims = relationship(
|
124 |
"AudioTrim", cascade="all, delete-orphan", back_populates="annotation"
|
125 |
)
|
data/repository/__init__.py
ADDED
File without changes
|
data/repository/annotation_interval_repo.py
ADDED
@@ -0,0 +1,88 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
from typing import List, Optional
|
2 |
+
from sqlalchemy.orm import Session
|
3 |
+
|
4 |
+
from data.models import AnnotationInterval
|
5 |
+
from utils.logger import Logger
|
6 |
+
|
7 |
+
log = Logger()
|
8 |
+
|
9 |
+
|
10 |
+
class AnnotationIntervalRepo:
|
11 |
+
"""
|
12 |
+
Data-access layer for `annotation_intervals` table.
|
13 |
+
"""
|
14 |
+
|
15 |
+
def __init__(self, db: Session) -> None:
|
16 |
+
self.db = db
|
17 |
+
|
18 |
+
# ------------------------------------------------------------------ #
|
19 |
+
# CREATE
|
20 |
+
# ------------------------------------------------------------------ #
|
21 |
+
def assign_interval_to_annotator(
|
22 |
+
self,
|
23 |
+
annotator_id: int,
|
24 |
+
start_idx: int,
|
25 |
+
end_idx: int,
|
26 |
+
allow_overlap: bool = False,
|
27 |
+
) -> AnnotationInterval:
|
28 |
+
"""
|
29 |
+
Create a new interval [start_idx, end_idx] for the given annotator.
|
30 |
+
|
31 |
+
Raises:
|
32 |
+
ValueError: • start >= end
|
33 |
+
• overlap detected (when allow_overlap=False)
|
34 |
+
"""
|
35 |
+
if start_idx >= end_idx:
|
36 |
+
raise ValueError("start_idx must be < end_idx")
|
37 |
+
|
38 |
+
if not allow_overlap:
|
39 |
+
overlap = (
|
40 |
+
self.db.query(AnnotationInterval)
|
41 |
+
.filter(AnnotationInterval.annotator_id == annotator_id)
|
42 |
+
.filter(
|
43 |
+
AnnotationInterval.start_index <= end_idx,
|
44 |
+
AnnotationInterval.end_index >= start_idx,
|
45 |
+
)
|
46 |
+
.first()
|
47 |
+
)
|
48 |
+
if overlap:
|
49 |
+
raise ValueError(
|
50 |
+
f"Overlap with existing interval "
|
51 |
+
f"[{overlap.start_index}, {overlap.end_index}]"
|
52 |
+
)
|
53 |
+
|
54 |
+
interval = AnnotationInterval(
|
55 |
+
annotator_id=annotator_id,
|
56 |
+
start_index=start_idx,
|
57 |
+
end_index=end_idx,
|
58 |
+
)
|
59 |
+
self.db.add(interval)
|
60 |
+
self.db.flush()
|
61 |
+
self.db.refresh(interval)
|
62 |
+
log.info(
|
63 |
+
f"Interval [{start_idx}, {end_idx}] assigned to annotator_id={annotator_id}"
|
64 |
+
)
|
65 |
+
return interval
|
66 |
+
|
67 |
+
# ------------------------------------------------------------------ #
|
68 |
+
# READ
|
69 |
+
# ------------------------------------------------------------------ #
|
70 |
+
def get_intervals_by_annotator(self, annotator_id: int) -> List[AnnotationInterval]:
|
71 |
+
return (
|
72 |
+
self.db.query(AnnotationInterval)
|
73 |
+
.filter(AnnotationInterval.annotator_id == annotator_id)
|
74 |
+
.all()
|
75 |
+
)
|
76 |
+
|
77 |
+
def get_interval(
|
78 |
+
self, annotator_id: int, start_idx: int, end_idx: int
|
79 |
+
) -> Optional[AnnotationInterval]:
|
80 |
+
return (
|
81 |
+
self.db.query(AnnotationInterval)
|
82 |
+
.filter(
|
83 |
+
AnnotationInterval.annotator_id == annotator_id,
|
84 |
+
AnnotationInterval.start_index == start_idx,
|
85 |
+
AnnotationInterval.end_index == end_idx,
|
86 |
+
)
|
87 |
+
.first()
|
88 |
+
)
|
data/repository/annotator_repo.py
CHANGED
@@ -19,18 +19,18 @@ class AnnotatorRepo:
|
|
19 |
# ------------------------------------------------------------------ #
|
20 |
# READ METHODS
|
21 |
# ------------------------------------------------------------------ #
|
22 |
-
def
|
23 |
try:
|
24 |
return (
|
25 |
self.db.query(Annotator)
|
26 |
-
.filter(Annotator.name ==
|
27 |
.first()
|
28 |
)
|
29 |
except Exception as exc:
|
30 |
-
log.error(f"Unable to fetch
|
31 |
raise
|
32 |
|
33 |
-
def
|
34 |
try:
|
35 |
return (
|
36 |
self.db.query(Annotator)
|
@@ -38,15 +38,15 @@ class AnnotatorRepo:
|
|
38 |
.first()
|
39 |
)
|
40 |
except Exception as exc:
|
41 |
-
log.error(f"Unable to fetch
|
42 |
raise
|
43 |
|
44 |
# ------------------------------------------------------------------ #
|
45 |
# WRITE METHODS
|
46 |
# ------------------------------------------------------------------ #
|
47 |
-
def
|
48 |
self,
|
49 |
-
|
50 |
password: str,
|
51 |
*,
|
52 |
is_active: bool = True,
|
@@ -54,27 +54,27 @@ class AnnotatorRepo:
|
|
54 |
"""
|
55 |
Create a new Annotator with a hashed password.
|
56 |
Raises:
|
57 |
-
ValueError: if
|
58 |
"""
|
59 |
try:
|
60 |
-
if self.
|
61 |
-
raise ValueError(f"
|
62 |
|
63 |
# ------------------ HASH PASSWORD ------------------ #
|
64 |
hashed_pass = hash_password(password)
|
65 |
|
66 |
-
|
67 |
-
name=
|
68 |
password=hashed_pass,
|
69 |
is_active=is_active,
|
70 |
)
|
71 |
-
self.db.add(
|
72 |
self.db.flush() # Ensure PK generated
|
73 |
-
self.db.refresh(
|
74 |
|
75 |
-
log.info(f"New
|
76 |
-
return
|
77 |
except Exception as exc:
|
78 |
self.db.rollback()
|
79 |
-
log.error(f"Unable to create
|
80 |
raise
|
|
|
19 |
# ------------------------------------------------------------------ #
|
20 |
# READ METHODS
|
21 |
# ------------------------------------------------------------------ #
|
22 |
+
def get_annotator_by_name(self, name: str) -> Optional[Annotator]:
|
23 |
try:
|
24 |
return (
|
25 |
self.db.query(Annotator)
|
26 |
+
.filter(Annotator.name == name)
|
27 |
.first()
|
28 |
)
|
29 |
except Exception as exc:
|
30 |
+
log.error(f"Unable to fetch annotator <name={name}> : {exc}")
|
31 |
raise
|
32 |
|
33 |
+
def get_annotator_by_id(self, user_id: int) -> Optional[Annotator]:
|
34 |
try:
|
35 |
return (
|
36 |
self.db.query(Annotator)
|
|
|
38 |
.first()
|
39 |
)
|
40 |
except Exception as exc:
|
41 |
+
log.error(f"Unable to fetch annotator <id={user_id}> : {exc}")
|
42 |
raise
|
43 |
|
44 |
# ------------------------------------------------------------------ #
|
45 |
# WRITE METHODS
|
46 |
# ------------------------------------------------------------------ #
|
47 |
+
def add_new_annotator(
|
48 |
self,
|
49 |
+
name: str,
|
50 |
password: str,
|
51 |
*,
|
52 |
is_active: bool = True,
|
|
|
54 |
"""
|
55 |
Create a new Annotator with a hashed password.
|
56 |
Raises:
|
57 |
+
ValueError: if name already exists.
|
58 |
"""
|
59 |
try:
|
60 |
+
if self.get_annotator_by_name(name):
|
61 |
+
raise ValueError(f"name `{name}` already exists.")
|
62 |
|
63 |
# ------------------ HASH PASSWORD ------------------ #
|
64 |
hashed_pass = hash_password(password)
|
65 |
|
66 |
+
annotator = Annotator(
|
67 |
+
name=name,
|
68 |
password=hashed_pass,
|
69 |
is_active=is_active,
|
70 |
)
|
71 |
+
self.db.add(annotator)
|
72 |
self.db.flush() # Ensure PK generated
|
73 |
+
self.db.refresh(annotator)
|
74 |
|
75 |
+
log.info(f"New annotator created <id={annotator.id} name={name}>")
|
76 |
+
return annotator
|
77 |
except Exception as exc:
|
78 |
self.db.rollback()
|
79 |
+
log.error(f"Unable to create annotator `{name}` : {exc}")
|
80 |
raise
|
data/repository/annotator_workload_repo.py
ADDED
@@ -0,0 +1,58 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
from typing import List, Dict, Optional, Any
|
2 |
+
|
3 |
+
from sqlalchemy import and_
|
4 |
+
from sqlalchemy.orm import Session
|
5 |
+
|
6 |
+
from data.models import TTSData, Annotation, AnnotationInterval
|
7 |
+
from data.repository.annotator_repo import AnnotatorRepo
|
8 |
+
from utils.logger import Logger
|
9 |
+
|
10 |
+
log = Logger()
|
11 |
+
|
12 |
+
|
13 |
+
class AnnotatorWorkloadRepo:
|
14 |
+
def __init__(self, db: Session) -> None:
|
15 |
+
self.db = db
|
16 |
+
self.annotator_repo = AnnotatorRepo(db)
|
17 |
+
|
18 |
+
def get_tts_data_with_annotations(
|
19 |
+
self, annotator_name: str
|
20 |
+
) -> List[Dict[str, Optional[Any]]]:
|
21 |
+
"""
|
22 |
+
output: [
|
23 |
+
{"tts_data": <TTSData>, "annotation": <Annotation or None>},
|
24 |
+
...
|
25 |
+
]
|
26 |
+
"""
|
27 |
+
|
28 |
+
annotator = self.annotator_repo.get_annotator_by_name(annotator_name)
|
29 |
+
if annotator is None:
|
30 |
+
raise ValueError(f"Annotator '{annotator_name}' not found")
|
31 |
+
|
32 |
+
query = (
|
33 |
+
self.db.query(
|
34 |
+
TTSData,
|
35 |
+
Annotation,
|
36 |
+
)
|
37 |
+
.join(
|
38 |
+
AnnotationInterval,
|
39 |
+
and_(
|
40 |
+
AnnotationInterval.annotator_id == annotator.id,
|
41 |
+
TTSData.id >= AnnotationInterval.start_index,
|
42 |
+
TTSData.id <= AnnotationInterval.end_index,
|
43 |
+
),
|
44 |
+
)
|
45 |
+
.outerjoin(
|
46 |
+
Annotation,
|
47 |
+
and_(
|
48 |
+
Annotation.tts_data_id == TTSData.id,
|
49 |
+
Annotation.annotator_id == annotator.id,
|
50 |
+
),
|
51 |
+
)
|
52 |
+
.order_by(TTSData.id)
|
53 |
+
).distinct(TTSData.id)
|
54 |
+
|
55 |
+
rows = [{"tts_data": tts, "annotation": ann} for tts, ann in query.all()]
|
56 |
+
|
57 |
+
log.info(f"{len(rows)} TTS rows fetched for annotator '{annotator_name}'.")
|
58 |
+
return rows
|
data/repository/tts_data_repo.py
ADDED
@@ -0,0 +1,19 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
from typing import List
|
2 |
+
from sqlalchemy.orm import Session
|
3 |
+
|
4 |
+
from data.models import TTSData
|
5 |
+
from utils.logger import Logger
|
6 |
+
|
7 |
+
log = Logger()
|
8 |
+
|
9 |
+
|
10 |
+
class TTSDataRepo:
|
11 |
+
def __init__(self, db: Session) -> None:
|
12 |
+
self.db = db
|
13 |
+
|
14 |
+
def get_range(self, start_id: int, end_id: int) -> List[TTSData]:
|
15 |
+
return (
|
16 |
+
self.db.query(TTSData)
|
17 |
+
.filter(TTSData.id >= start_id, TTSData.id <= end_id)
|
18 |
+
.all()
|
19 |
+
)
|
scripts/__init__.py
ADDED
File without changes
|
scripts/assign_interval_to_annotator.py
ADDED
@@ -0,0 +1,33 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
# scripts/assign_interval_to_annotator.py
|
2 |
+
|
3 |
+
from utils.database import get_db
|
4 |
+
from data.repository.annotator_repo import AnnotatorRepo
|
5 |
+
from data.repository.annotation_interval_repo import AnnotationIntervalRepo
|
6 |
+
from utils.logger import Logger
|
7 |
+
|
8 |
+
log = Logger()
|
9 |
+
|
10 |
+
START_IDX = 1
|
11 |
+
END_IDX = 100
|
12 |
+
ANNOTATOR_NAME = "navid"
|
13 |
+
|
14 |
+
with get_db() as db:
|
15 |
+
annot_repo = AnnotatorRepo(db)
|
16 |
+
interval_repo = AnnotationIntervalRepo(db)
|
17 |
+
|
18 |
+
annotator = annot_repo.get_annotator_by_name(ANNOTATOR_NAME)
|
19 |
+
if not annotator:
|
20 |
+
log.info(f"Annotator '{annotator}' not found;")
|
21 |
+
|
22 |
+
try:
|
23 |
+
interval = interval_repo.assign_interval_to_annotator(
|
24 |
+
annotator_id=annotator.id,
|
25 |
+
start_idx=START_IDX,
|
26 |
+
end_idx=END_IDX,
|
27 |
+
)
|
28 |
+
log.info(
|
29 |
+
f"Interval [{interval.start_index}, {interval.end_index}] "
|
30 |
+
f"successfully assigned to '{annotator.name}' (id={annotator.id})"
|
31 |
+
)
|
32 |
+
except ValueError as err:
|
33 |
+
log.error(f"Could not assign interval: {err}")
|
create_new_user.py → scripts/create_new_annotator.py
RENAMED
@@ -8,15 +8,9 @@ try:
|
|
8 |
with get_db() as session:
|
9 |
repo = AnnotatorRepo(session)
|
10 |
|
11 |
-
|
12 |
-
new_user = repo.add_new_user("navid", "123")
|
13 |
log.info(
|
14 |
-
f"User created successfully (id={new_user.id},
|
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:
|
|
|
8 |
with get_db() as session:
|
9 |
repo = AnnotatorRepo(session)
|
10 |
|
11 |
+
new_user = repo.add_new_annotator("user", "pass")
|
|
|
12 |
log.info(
|
13 |
+
f"User created successfully (id={new_user.id}, name='{new_user.name}')"
|
|
|
|
|
|
|
|
|
|
|
14 |
)
|
15 |
|
16 |
except Exception as exc:
|
test/__init__.py
ADDED
File without changes
|
utils/auth.py
CHANGED
@@ -2,6 +2,7 @@ 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()
|
@@ -12,54 +13,140 @@ class AuthService:
|
|
12 |
Authenticate users against DB and drive Gradio UI states.
|
13 |
"""
|
14 |
|
15 |
-
#
|
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 |
-
|
23 |
|
24 |
-
|
25 |
-
|
26 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
27 |
)
|
|
|
|
|
|
|
|
|
28 |
return (
|
29 |
"❌ Wrong username or password!",
|
30 |
gr.update(),
|
31 |
gr.update(visible=False),
|
32 |
gr.update(value=""),
|
|
|
33 |
)
|
34 |
|
35 |
-
|
36 |
-
|
|
|
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, {
|
|
|
|
|
|
|
52 |
)
|
53 |
|
54 |
-
#
|
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 |
)
|
|
|
2 |
from utils.logger import Logger
|
3 |
from utils.database import get_db
|
4 |
from data.repository.annotator_repo import AnnotatorRepo
|
5 |
+
from data.repository.annotator_workload_repo import AnnotatorWorkloadRepo
|
6 |
from utils.security import verify_password
|
7 |
|
8 |
log = Logger()
|
|
|
13 |
Authenticate users against DB and drive Gradio UI states.
|
14 |
"""
|
15 |
|
16 |
+
# ───────────── LOGIN ───────────── #
|
17 |
@staticmethod
|
18 |
def login(username: str, password: str, session: dict):
|
19 |
+
"""
|
20 |
+
خروجیها (به همین ترتیب در LoginPage ثبت شده است):
|
21 |
+
0) message (Markdown داخل فرم لاگین)
|
22 |
+
1) login_container (پنهان/نمایان شدن فرم لاگین)
|
23 |
+
2) dashboard_container (نمایش داشبورد)
|
24 |
+
3) header_welcome (پیام خوشآمد در هدر)
|
25 |
+
4) items_state (لیست رکوردها)
|
26 |
+
5) idx_state (اندیس فعلی)
|
27 |
+
6-11) شش فیلد نمایشی (id, filename, sentence, ann_sentence, ann_at, validated)
|
28 |
+
"""
|
29 |
+
|
30 |
+
# ---------- اعتبارسنجی ---------- #
|
31 |
log.info(f"Login attempt: username={username}")
|
32 |
|
33 |
with get_db() as db:
|
34 |
repo = AnnotatorRepo(db)
|
35 |
+
annotator = repo.get_annotator_by_name(username)
|
36 |
|
37 |
+
# ⬇️ توابع کمکی برای تولید خروجی خالی (درصورت خطا)
|
38 |
+
def empty_dashboard_outputs():
|
39 |
+
return (
|
40 |
+
[], # items_state
|
41 |
+
0, # idx_state
|
42 |
+
"",
|
43 |
+
"",
|
44 |
+
"",
|
45 |
+
"",
|
46 |
+
"",
|
47 |
+
False, # شش فیلد
|
48 |
)
|
49 |
+
|
50 |
+
# --- کاربر موجود نیست / غیر فعال
|
51 |
+
if annotator is None or not annotator.is_active:
|
52 |
+
log.warning("Failed login (not found / inactive)")
|
53 |
return (
|
54 |
"❌ Wrong username or password!",
|
55 |
gr.update(),
|
56 |
gr.update(visible=False),
|
57 |
gr.update(value=""),
|
58 |
+
*empty_dashboard_outputs(),
|
59 |
)
|
60 |
|
61 |
+
# --- رمز عبور اشتباه
|
62 |
+
if not verify_password(password, annotator.password):
|
63 |
+
log.warning("Failed login (bad password)")
|
64 |
return (
|
65 |
"❌ Wrong username or password!",
|
66 |
gr.update(),
|
67 |
gr.update(visible=False),
|
68 |
gr.update(value=""),
|
69 |
+
*empty_dashboard_outputs(),
|
70 |
+
)
|
71 |
+
|
72 |
+
# ---------- ورود موفق ---------- #
|
73 |
+
session["user_id"] = annotator.id
|
74 |
+
session["username"] = annotator.name
|
75 |
+
|
76 |
+
# بارگذاری دادههای داشبورد
|
77 |
+
workload_repo = AnnotatorWorkloadRepo(db)
|
78 |
+
raw_items = workload_repo.get_tts_data_with_annotations(username)
|
79 |
+
|
80 |
+
dashboard_items = [
|
81 |
+
{
|
82 |
+
"id": row["tts_data"].id,
|
83 |
+
"filename": row["tts_data"].filename,
|
84 |
+
"sentence": row["tts_data"].sentence,
|
85 |
+
"annotated_sentence": (
|
86 |
+
row["annotation"].annotated_sentence
|
87 |
+
if row["annotation"]
|
88 |
+
else ""
|
89 |
+
),
|
90 |
+
"annotated_at": (
|
91 |
+
row["annotation"].annotated_at.isoformat()
|
92 |
+
if row["annotation"] and row["annotation"].annotated_at
|
93 |
+
else ""
|
94 |
+
),
|
95 |
+
"validated": (
|
96 |
+
row["annotation"].validated
|
97 |
+
if row["annotation"] is not None
|
98 |
+
else False
|
99 |
+
),
|
100 |
+
}
|
101 |
+
for row in raw_items
|
102 |
+
]
|
103 |
+
|
104 |
+
session["dashboard_items"] = dashboard_items
|
105 |
+
|
106 |
+
# مقداردهی فیلدهای رکورد اول (یا مقادیر تهی)
|
107 |
+
if dashboard_items:
|
108 |
+
first = dashboard_items[0]
|
109 |
+
first_vals = (
|
110 |
+
first["id"],
|
111 |
+
first["filename"],
|
112 |
+
first["sentence"],
|
113 |
+
first["annotated_sentence"],
|
114 |
+
first["annotated_at"],
|
115 |
+
first["validated"],
|
116 |
)
|
117 |
+
else:
|
118 |
+
first_vals = ("", "", "", "", "", False)
|
119 |
|
|
|
|
|
120 |
log.info(f"User '{username}' logged in successfully.")
|
121 |
+
|
122 |
+
# ---------- خروجی نهایی برای Gradio ---------- #
|
123 |
return (
|
124 |
+
None, # 0: پیام خطا وجود ندارد
|
125 |
+
gr.update(visible=False), # 1: فرم لاگین را مخفی کن
|
126 |
+
gr.update(visible=True), # 2: داشبورد را نشان بده
|
127 |
+
gr.update(value=f"👋 Welcome, {annotator.name}!"), # 3
|
128 |
+
dashboard_items, # 4: items_state
|
129 |
+
0, # 5: idx_state
|
130 |
+
*first_vals, # 6-11: شش فیلد نخست
|
131 |
)
|
132 |
|
133 |
+
# ───────────── LOGOUT ───────────── #
|
134 |
@staticmethod
|
135 |
def logout(session: dict):
|
136 |
username = session.get("username", "unknown")
|
137 |
session.clear()
|
138 |
log.info(f"User '{username}' logged out.")
|
139 |
return (
|
140 |
+
gr.update(visible=True), # لاگین فرم را دوباره نشان بده
|
141 |
+
gr.update(visible=False), # داشبورد را پنهان کن
|
142 |
+
gr.update(value=""), # پیامها را پاک کن
|
143 |
+
gr.update(value=""), # متن خوشآمد را پاک کن
|
144 |
+
[],
|
145 |
+
0,
|
146 |
+
"",
|
147 |
+
"",
|
148 |
+
"",
|
149 |
+
"",
|
150 |
+
"",
|
151 |
+
False, # خروجیهای داشبورد را ریست
|
152 |
)
|