vargha commited on
Commit
c6fc32c
·
1 Parent(s): 11dd3ea

phase 2 validation

Browse files
app.py CHANGED
@@ -4,6 +4,7 @@ from pathlib import Path
4
  from utils.logger import Logger
5
  from components.login_page import LoginPage
6
  from components.dashboard_page import DashboardPage
 
7
  from utils.database import initialize_database
8
  from config import conf
9
 
@@ -27,12 +28,14 @@ def build_app() -> gr.Blocks:
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)
 
4
  from utils.logger import Logger
5
  from components.login_page import LoginPage
6
  from components.dashboard_page import DashboardPage
7
+ from components.review_dashboard_page import ReviewDashboardPage
8
  from utils.database import initialize_database
9
  from config import conf
10
 
 
28
  gr.Markdown(f"### {conf.APP_TITLE}")
29
 
30
  # صفحات
31
+ login_page = LoginPage()
32
+ dashboard_page = DashboardPage() # Phase 1 interface
33
+ review_dashboard_page = ReviewDashboardPage() # Phase 2 interface
34
 
35
  # اتصال رویدادها
36
+ login_page.register_callbacks(dashboard_page, session_state, review_dashboard_page)
37
+ dashboard_page.register_callbacks(login_page, session_state, demo, review_dashboard_page)
38
+ review_dashboard_page.register_callbacks(login_page, session_state, demo)
39
 
40
  # صف پردازش گرادیو
41
  demo.queue(default_concurrency_limit=50)
components/dashboard_page.py CHANGED
@@ -126,9 +126,9 @@ class DashboardPage:
126
 
127
  # ---------------- wiring ---------------- #
128
  def register_callbacks(
129
- self, login_page, session_state: gr.State, root_blocks: gr.Blocks
130
  ):
131
- self.header.register_callbacks(login_page, self, session_state)
132
 
133
  def update_ui_interactive_state(is_interactive: bool):
134
  updates = []
 
126
 
127
  # ---------------- wiring ---------------- #
128
  def register_callbacks(
129
+ self, login_page, session_state: gr.State, root_blocks: gr.Blocks, review_dashboard_page=None
130
  ):
131
+ self.header.register_callbacks(login_page, self, session_state, review_dashboard_page)
132
 
133
  def update_ui_interactive_state(is_interactive: bool):
134
  updates = []
components/header.py CHANGED
@@ -12,21 +12,29 @@ class Header:
12
  self.logout_btn = gr.Button("Log out", scale=0, min_width=90)
13
 
14
  # ---------------- wiring ----------------
15
- def register_callbacks(self, login_page, dashboard_page, session_state):
16
  def logout_and_clear_progress_fn(current_session_state):
17
- # AuthService.logout is expected to return 4 values for the original outputs
18
  logout_outputs = AuthService.logout(current_session_state)
19
  # Add an empty string to clear the progress_display
20
  return list(logout_outputs) + [""]
21
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
22
  self.logout_btn.click(
23
  fn=logout_and_clear_progress_fn,
24
  inputs=[session_state],
25
- outputs=[
26
- login_page.container,
27
- dashboard_page.container,
28
- self.welcome,
29
- login_page.message,
30
- self.progress_display, # Cleared on logout
31
- ],
32
  )
 
12
  self.logout_btn = gr.Button("Log out", scale=0, min_width=90)
13
 
14
  # ---------------- wiring ----------------
15
+ def register_callbacks(self, login_page, dashboard_page, session_state, review_dashboard_page=None):
16
  def logout_and_clear_progress_fn(current_session_state):
17
+ # AuthService.logout returns 5 values for both dashboards
18
  logout_outputs = AuthService.logout(current_session_state)
19
  # Add an empty string to clear the progress_display
20
  return list(logout_outputs) + [""]
21
 
22
+ outputs_for_logout = [
23
+ login_page.container,
24
+ dashboard_page.container,
25
+ ]
26
+
27
+ if review_dashboard_page:
28
+ outputs_for_logout.append(review_dashboard_page.container)
29
+
30
+ outputs_for_logout.extend([
31
+ self.welcome,
32
+ login_page.message,
33
+ self.progress_display, # Cleared on logout
34
+ ])
35
+
36
  self.logout_btn.click(
37
  fn=logout_and_clear_progress_fn,
38
  inputs=[session_state],
39
+ outputs=outputs_for_logout,
 
 
 
 
 
 
40
  )
components/login_page.py CHANGED
@@ -25,10 +25,35 @@ class LoginPage:
25
  gr.Column(scale=1) # right spacer
26
 
27
  # event wiring unchanged …
28
- def register_callbacks(self, dashboard_page, session_state):
29
  header = dashboard_page.header
30
  btn = self.login_btn
31
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
32
  (
33
  btn.click(
34
  fn=lambda: gr.update(value="🔒 Logging in…", interactive=False),
@@ -37,18 +62,7 @@ class LoginPage:
37
  .then(
38
  fn=AuthService.login,
39
  inputs=[self.username, self.password, session_state],
40
- outputs=[
41
- self.message,
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
- ],
52
  )
53
  .then(
54
  fn=lambda: gr.update(value="Login", interactive=True),
 
25
  gr.Column(scale=1) # right spacer
26
 
27
  # event wiring unchanged …
28
+ def register_callbacks(self, dashboard_page, session_state, review_dashboard_page=None):
29
  header = dashboard_page.header
30
  btn = self.login_btn
31
 
32
+ # Prepare outputs based on whether we have a review dashboard
33
+ login_outputs = [
34
+ self.message,
35
+ self.container,
36
+ dashboard_page.container, # Phase 1 dashboard
37
+ ]
38
+ if review_dashboard_page:
39
+ login_outputs.append(review_dashboard_page.container) # Phase 2 dashboard
40
+ login_outputs.append(review_dashboard_page.load_trigger) # Trigger for loading review data
41
+ else:
42
+ login_outputs.append(gr.update()) # Placeholder for review_dashboard_page.container
43
+ login_outputs.append(gr.update()) # Placeholder for review_dashboard_page.load_trigger
44
+
45
+ login_outputs.extend([
46
+ header.welcome,
47
+ # These are for the main dashboard_page,
48
+ # review_dashboard_page will load its own data via the trigger
49
+ dashboard_page.items_state,
50
+ dashboard_page.idx_state,
51
+ dashboard_page.tts_id,
52
+ dashboard_page.filename,
53
+ dashboard_page.sentence,
54
+ dashboard_page.ann_sentence,
55
+ ])
56
+
57
  (
58
  btn.click(
59
  fn=lambda: gr.update(value="🔒 Logging in…", interactive=False),
 
62
  .then(
63
  fn=AuthService.login,
64
  inputs=[self.username, self.password, session_state],
65
+ outputs=login_outputs,
 
 
 
 
 
 
 
 
 
 
 
66
  )
67
  .then(
68
  fn=lambda: gr.update(value="Login", interactive=True),
components/review_dashboard_page.py ADDED
@@ -0,0 +1,418 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # components/review_dashboard_page.py
2
+
3
+ import gradio as gr
4
+ import datetime
5
+ from sqlalchemy import orm
6
+
7
+ from components.header import Header
8
+ from utils.logger import Logger
9
+ from utils.cloud_server_audio_loader import CloudServerAudioLoader
10
+ from config import conf
11
+ from utils.database import get_db
12
+ from data.models import Annotation, TTSData, Annotator, Validation
13
+ from data.repository.annotator_workload_repo import AnnotatorWorkloadRepo
14
+
15
+ log = Logger()
16
+ LOADER = CloudServerAudioLoader(conf.FTP_URL)
17
+
18
+
19
+ class ReviewDashboardPage:
20
+ def __init__(self) -> None:
21
+ with gr.Column(visible=False) as self.container:
22
+ self.header = Header()
23
+ self.load_trigger = gr.Number(value=0, visible=False) # Add this hidden trigger
24
+
25
+ # Review info banner
26
+ with gr.Row():
27
+ self.review_info = gr.Markdown("", elem_classes="review-banner")
28
+
29
+ with gr.Row():
30
+ # Left Column - Review Content
31
+ with gr.Column(scale=3):
32
+ with gr.Row():
33
+ self.tts_id = gr.Textbox(label="ID", interactive=False, scale=1)
34
+ self.filename = gr.Textbox(label="Filename", interactive=False, scale=3)
35
+
36
+ self.sentence = gr.Textbox(
37
+ label="Original Sentence", interactive=False, max_lines=5, rtl=True
38
+ )
39
+
40
+ self.ann_sentence = gr.Textbox(
41
+ label="Annotated Sentence (by Original Annotator)",
42
+ interactive=False, max_lines=5, rtl=True
43
+ )
44
+
45
+ with gr.Row():
46
+ # self.annotator_name = gr.Textbox(label="Original Annotator", interactive=False, scale=1) # Removed for anonymization
47
+ self.annotated_at = gr.Textbox(label="Annotated At", interactive=False, scale=2)
48
+
49
+ # Review Actions
50
+ with gr.Row():
51
+ self.btn_approve = gr.Button("✅ Approve", variant="primary", min_width=120)
52
+ self.btn_reject = gr.Button("❌ Reject", variant="stop", min_width=120)
53
+ self.btn_skip = gr.Button("⏭️ Skip (No Decision)", min_width=150)
54
+
55
+ # Navigation
56
+ with gr.Row():
57
+ self.btn_prev = gr.Button("⬅️ Previous", min_width=120)
58
+ self.btn_next = gr.Button("Next ➡️", min_width=120)
59
+
60
+ # Jump controls
61
+ with gr.Row():
62
+ self.jump_data_id_input = gr.Number(
63
+ label="Jump to ID",
64
+ value=None,
65
+ precision=0,
66
+ interactive=True,
67
+ min_width=120
68
+ )
69
+ self.btn_jump = gr.Button("Go to ID", min_width=70)
70
+
71
+ # Right Column - Audio
72
+ with gr.Column(scale=2):
73
+ self.btn_load_voice = gr.Button("Load Audio & Play", min_width=150)
74
+ self.audio = gr.Audio(
75
+ label="🔊 Audio", interactive=False, autoplay=True
76
+ )
77
+
78
+ # Review status display
79
+ with gr.Group():
80
+ gr.Markdown("### Review Status")
81
+ self.current_validation_status = gr.Textbox(
82
+ label="Current Status", interactive=False
83
+ )
84
+
85
+ # State variables
86
+ self.items_state = gr.State([])
87
+ self.idx_state = gr.State(0)
88
+ self.original_audio_state = gr.State(None)
89
+
90
+ # List of interactive UI elements for enabling/disabling
91
+ self.interactive_ui_elements = [
92
+ self.btn_prev, self.btn_next, self.btn_approve, self.btn_reject,
93
+ self.btn_skip, self.btn_jump, self.jump_data_id_input, self.btn_load_voice
94
+ ]
95
+
96
+ def register_callbacks(self, login_page, session_state: gr.State, root_blocks: gr.Blocks):
97
+ self.header.register_callbacks(login_page, self, session_state)
98
+
99
+ def update_ui_interactive_state(is_interactive: bool):
100
+ updates = []
101
+ for elem in self.interactive_ui_elements:
102
+ if elem == self.btn_load_voice and not is_interactive:
103
+ updates.append(gr.update(value="⏳ Loading Audio...", interactive=False))
104
+ elif elem == self.btn_load_voice and is_interactive:
105
+ updates.append(gr.update(value="Load Audio & Play", interactive=True))
106
+ else:
107
+ updates.append(gr.update(interactive=is_interactive))
108
+ return updates
109
+
110
+ def download_voice_fn(filename_to_load):
111
+ if not filename_to_load:
112
+ return None, None, gr.update(value=None, autoplay=False)
113
+ try:
114
+ log.info(f"Downloading voice for review: {filename_to_load}")
115
+ sr, wav = LOADER.load_audio(filename_to_load)
116
+ return (sr, wav), (sr, wav.copy()), gr.update(value=(sr, wav), autoplay=True)
117
+ except Exception as e:
118
+ log.error(f"Audio download failed for {filename_to_load}: {e}")
119
+ gr.Error(f"Failed to load audio: {filename_to_load}. Error: {e}")
120
+ return None, None, gr.update(value=None, autoplay=False)
121
+
122
+ def load_review_items_fn(session):
123
+ user_id = session.get("user_id")
124
+ username = session.get("username")
125
+
126
+ if not user_id or not username:
127
+ log.warning("load_review_items_fn: user not found in session")
128
+ return [], 0, "", "", "", "", "", "", "", gr.update(value=None, autoplay=False) # Adjusted output count
129
+
130
+ # Check if user is in Phase 2 (should be a reviewer)
131
+ if username not in conf.REVIEW_MAPPING.values():
132
+ log.warning(f"User {username} is not assigned as a reviewer")
133
+ return [], 0, "", "", "", "", "", "", "", gr.update(value=None, autoplay=False) # Adjusted output count
134
+
135
+ # Find which annotator this user should review
136
+ target_annotator = None
137
+ for annotator_name, reviewer_name in conf.REVIEW_MAPPING.items():
138
+ if reviewer_name == username:
139
+ target_annotator = annotator_name
140
+ break
141
+
142
+ if not target_annotator:
143
+ log.warning(f"No target annotator found for reviewer {username}")
144
+ return [], 0, "", "", "", "", "", "", "", gr.update(value=None, autoplay=False) # Adjusted output count
145
+
146
+ # Load annotations from target annotator
147
+ with get_db() as db:
148
+ try:
149
+ # Get target annotator's ID
150
+ target_annotator_obj = db.query(Annotator).filter_by(name=target_annotator).first()
151
+ if not target_annotator_obj:
152
+ log.error(f"Target annotator {target_annotator} not found in database")
153
+ return [], 0, f"Review Target Error: Annotator '{target_annotator}' not found.", "", "", "", "", "", "", gr.update(value=None, autoplay=False) # Adjusted output count
154
+
155
+ log.info(f"Found target annotator '{target_annotator}' with ID: {target_annotator_obj.id}")
156
+
157
+ # Get all annotations by target annotator
158
+ annotations = db.query(Annotation).join(TTSData).filter(
159
+ Annotation.annotator_id == target_annotator_obj.id,
160
+ Annotation.annotated_sentence.isnot(None),
161
+ Annotation.annotated_sentence != ""
162
+ ).options(
163
+ orm.joinedload(Annotation.tts_data),
164
+ orm.joinedload(Annotation.annotator)
165
+ ).all()
166
+
167
+ log.info(f"Fetched {len(annotations)} annotations for target annotator ID {target_annotator_obj.id} ({target_annotator})")
168
+
169
+ items = []
170
+ for annotation in annotations:
171
+ # Check if this annotation has been reviewed by current user
172
+ existing_validation = db.query(Validation).filter_by(
173
+ annotation_id=annotation.id,
174
+ validator_id=user_id
175
+ ).first()
176
+
177
+ validation_status = "Not Reviewed"
178
+ if existing_validation:
179
+ validation_status = "Approved" if existing_validation.validated else "Rejected"
180
+
181
+ items.append({
182
+ "annotation_id": annotation.id,
183
+ "tts_id": annotation.tts_data.id,
184
+ "filename": annotation.tts_data.filename,
185
+ "sentence": annotation.tts_data.sentence,
186
+ "annotated_sentence": annotation.annotated_sentence,
187
+ # "annotator_name": annotation.annotator.name, # Removed for anonymization
188
+ "annotated_at": annotation.annotated_at.isoformat() if annotation.annotated_at else "",
189
+ "validation_status": validation_status
190
+ })
191
+
192
+ log.info(f"Loaded {len(items)} review items for {username} reviewing {target_annotator}")
193
+
194
+ # Set initial display
195
+ if items:
196
+ initial_item = items[0]
197
+ review_info_text = f"🔍 **Phase 2 Review Mode** - Reviewing assigned annotations." # Anonymized
198
+ return (
199
+ items, 0, review_info_text,
200
+ str(initial_item["tts_id"]), initial_item["filename"],
201
+ initial_item["sentence"], initial_item["annotated_sentence"],
202
+ # initial_item["annotator_name"], # Removed for anonymization
203
+ initial_item["annotated_at"],
204
+ initial_item["validation_status"],
205
+ gr.update(value=None, autoplay=False)
206
+ )
207
+ else:
208
+ return [], 0, f"🔍 **Phase 2 Review Mode** - No annotations found for review.", "", "", "", "", "", "", gr.update(value=None, autoplay=False) # Adjusted output count
209
+
210
+ except Exception as e:
211
+ log.error(f"Error loading review items: {e}")
212
+ gr.Error(f"Failed to load review data: {e}")
213
+ return [], 0, "", "", "", "", "", "", "", gr.update(value=None, autoplay=False) # Adjusted output count
214
+
215
+ def show_current_review_item_fn(items, idx, session):
216
+ if not items or idx >= len(items) or idx < 0:
217
+ return "", "", "", "", "", "", gr.update(value=None, autoplay=False) # Adjusted output count
218
+
219
+ current_item = items[idx]
220
+ return (
221
+ str(current_item["tts_id"]), current_item["filename"],
222
+ current_item["sentence"], current_item["annotated_sentence"],
223
+ # current_item["annotator_name"], # Removed for anonymization
224
+ current_item["annotated_at"],
225
+ current_item["validation_status"],
226
+ gr.update(value=None, autoplay=False)
227
+ )
228
+
229
+ def navigate_review_fn(items, current_idx, direction):
230
+ if not items:
231
+ return 0
232
+ if direction == "next":
233
+ return min(current_idx + 1, len(items) - 1)
234
+ else: # prev
235
+ return max(current_idx - 1, 0)
236
+
237
+ def save_validation_fn(items, idx, session, approved: bool, rejection_reason: str = ""):
238
+ if not items or idx >= len(items):
239
+ gr.Error("Invalid item index")
240
+ return items, "Error: Invalid item index"
241
+
242
+ user_id = session.get("user_id")
243
+ if not user_id:
244
+ gr.Error("User not logged in")
245
+ return items, "Error: User not logged in"
246
+
247
+ current_item = items[idx]
248
+ annotation_id = current_item["annotation_id"]
249
+ log.info(f"Saving validation for annotation_id: {annotation_id}, validator_id: {user_id}, approved: {approved}, reason: {rejection_reason}")
250
+
251
+ with get_db() as db:
252
+ try:
253
+ existing_validation = db.query(Validation).filter_by(
254
+ annotation_id=annotation_id,
255
+ validator_id=user_id
256
+ ).first()
257
+
258
+ if existing_validation:
259
+ log.info(f"Updating existing validation for annotation_id: {annotation_id}")
260
+ existing_validation.validated = approved
261
+ existing_validation.description = rejection_reason if not approved else None
262
+ existing_validation.validated_at = datetime.datetime.utcnow()
263
+ else:
264
+ log.info(f"Creating new validation for annotation_id: {annotation_id}")
265
+ new_validation = Validation(
266
+ annotation_id=annotation_id,
267
+ validator_id=user_id,
268
+ validated=approved,
269
+ description=rejection_reason if not approved else None,
270
+ validated_at=datetime.datetime.utcnow(),
271
+ )
272
+ db.add(new_validation)
273
+
274
+ db.commit()
275
+ log.info(f"Validation saved successfully for annotation_id: {annotation_id}")
276
+
277
+ # Update the item in memory for immediate UI feedback
278
+ items[idx]["validation_status"] = "Approved" if approved else f"Rejected ({rejection_reason})" if rejection_reason else "Rejected"
279
+ return items, items[idx]["validation_status"]
280
+
281
+ except Exception as e:
282
+ db.rollback()
283
+ log.error(f"Error saving validation: {e}")
284
+ gr.Error(f"Failed to save validation: {e}")
285
+ return items, current_item["validation_status"] # Return original status on error
286
+
287
+ def jump_by_data_id_fn(items, target_data_id, current_idx):
288
+ if not target_data_id:
289
+ return current_idx
290
+ try:
291
+ target_id = int(target_data_id)
292
+ for i, item in enumerate(items):
293
+ if item["tts_id"] == target_id:
294
+ return i
295
+ gr.Warning(f"Data ID {target_id} not found in review items")
296
+ except ValueError:
297
+ gr.Warning(f"Invalid Data ID format: {target_data_id}")
298
+ return current_idx
299
+
300
+ # Output definitions
301
+ review_display_outputs = [
302
+ self.tts_id, self.filename, self.sentence, self.ann_sentence,
303
+ # self.annotator_name, # Removed for anonymization
304
+ self.annotated_at, self.current_validation_status, self.audio
305
+ ]
306
+
307
+ # Trigger data loading when load_trigger changes (after successful login for a reviewer)
308
+ self.load_trigger.change(
309
+ fn=lambda: update_ui_interactive_state(False),
310
+ outputs=self.interactive_ui_elements
311
+ ).then(
312
+ fn=load_review_items_fn,
313
+ inputs=[session_state],
314
+ outputs=[self.items_state, self.idx_state, self.review_info] + review_display_outputs
315
+ ).then(
316
+ fn=lambda: (None, gr.update(value=None)), # Clear audio state
317
+ outputs=[self.original_audio_state, self.audio]
318
+ ).then(
319
+ fn=lambda: update_ui_interactive_state(True),
320
+ outputs=self.interactive_ui_elements
321
+ )
322
+
323
+ # Load audio when filename changes
324
+ self.filename.change(
325
+ fn=download_voice_fn,
326
+ inputs=[self.filename],
327
+ outputs=[self.audio, self.original_audio_state, self.audio]
328
+ )
329
+
330
+ # Navigation buttons
331
+ for btn, direction in [(self.btn_prev, "prev"), (self.btn_next, "next")]:
332
+ btn.click(
333
+ fn=lambda: update_ui_interactive_state(False),
334
+ outputs=self.interactive_ui_elements
335
+ ).then(
336
+ fn=navigate_review_fn,
337
+ inputs=[self.items_state, self.idx_state, gr.State(direction)],
338
+ outputs=self.idx_state
339
+ ).then(
340
+ fn=show_current_review_item_fn,
341
+ inputs=[self.items_state, self.idx_state, session_state],
342
+ outputs=review_display_outputs
343
+ ).then(
344
+ lambda: gr.update(value=None),
345
+ outputs=self.jump_data_id_input
346
+ ).then(
347
+ fn=lambda: update_ui_interactive_state(True),
348
+ outputs=self.interactive_ui_elements
349
+ )
350
+
351
+ # Approve/Reject buttons
352
+ self.btn_approve.click(
353
+ fn=lambda items, idx, session: save_validation_fn(items, idx, session, approved=True),
354
+ inputs=[self.items_state, self.idx_state, session_state],
355
+ outputs=[self.items_state, self.current_validation_status] # Update items_state and current_validation_status
356
+ ).then(
357
+ fn=lambda items, idx: navigate_review_fn(items, idx, "next"),
358
+ inputs=[self.items_state, self.idx_state],
359
+ outputs=[self.idx_state]
360
+ ).then(
361
+ fn=show_current_review_item_fn,
362
+ inputs=[self.items_state, self.idx_state, session_state],
363
+ outputs=review_display_outputs
364
+ )
365
+
366
+ self.btn_reject.click(
367
+ fn=lambda items, idx, session: save_validation_fn(items, idx, session, approved=False, rejection_reason="Rejected by reviewer"), # Basic reason for now
368
+ inputs=[self.items_state, self.idx_state, session_state],
369
+ outputs=[self.items_state, self.current_validation_status]
370
+ ).then(
371
+ fn=lambda items, idx: navigate_review_fn(items, idx, "next"),
372
+ inputs=[self.items_state, self.idx_state],
373
+ outputs=[self.idx_state]
374
+ ).then(
375
+ fn=show_current_review_item_fn,
376
+ inputs=[self.items_state, self.idx_state, session_state],
377
+ outputs=review_display_outputs
378
+ )
379
+
380
+ # Skip button (just navigate to next)
381
+ self.btn_skip.click(
382
+ fn=navigate_review_fn,
383
+ inputs=[self.items_state, self.idx_state, gr.State("next")],
384
+ outputs=self.idx_state
385
+ ).then(
386
+ fn=show_current_review_item_fn,
387
+ inputs=[self.items_state, self.idx_state, session_state],
388
+ outputs=review_display_outputs
389
+ )
390
+
391
+ # Jump button
392
+ self.btn_jump.click(
393
+ fn=jump_by_data_id_fn,
394
+ inputs=[self.items_state, self.jump_data_id_input, self.idx_state],
395
+ outputs=self.idx_state
396
+ ).then(
397
+ fn=show_current_review_item_fn,
398
+ inputs=[self.items_state, self.idx_state, session_state],
399
+ outputs=review_display_outputs
400
+ ).then(
401
+ lambda: gr.update(value=None),
402
+ outputs=self.jump_data_id_input
403
+ )
404
+
405
+ # Load audio button
406
+ self.btn_load_voice.click(
407
+ fn=lambda: update_ui_interactive_state(False),
408
+ outputs=self.interactive_ui_elements
409
+ ).then(
410
+ fn=download_voice_fn,
411
+ inputs=[self.filename],
412
+ outputs=[self.audio, self.original_audio_state, self.audio]
413
+ ).then(
414
+ fn=lambda: update_ui_interactive_state(True),
415
+ outputs=self.interactive_ui_elements
416
+ )
417
+
418
+ return self.container
config.py CHANGED
@@ -16,6 +16,13 @@ class Config(BaseSettings):
16
 
17
  APP_TITLE: str = "Gooya TTS Annotation Tools"
18
 
 
 
 
 
 
 
 
19
  class Config:
20
  env_file = ".env"
21
  case_sensitive = True
 
16
 
17
  APP_TITLE: str = "Gooya TTS Annotation Tools"
18
 
19
+ # Phase 2 Review Mapping: Defines who reviews whose work.
20
+ # Key: Original annotator's username, Value: Reviewer's username
21
+ REVIEW_MAPPING: dict[str, str] = {
22
+ "zahra": "amin",
23
+ "amin": "zahra"
24
+ }
25
+
26
  class Config:
27
  env_file = ".env"
28
  case_sensitive = True
data/models.py CHANGED
@@ -152,10 +152,10 @@ class Validation(Base):
152
 
153
  id = Column(Integer, primary_key=True)
154
  annotation_id = Column(Integer, ForeignKey("annotations.id"))
155
- validator_id = Column(Integer, ForeignKey("validators.id"))
156
  validated = Column(Boolean, nullable=False)
157
  description = Column(Text, nullable=True)
158
  validated_at = Column(DateTime, nullable=False)
159
 
160
  annotation = relationship("Annotation")
161
- validator = relationship("Validator")
 
152
 
153
  id = Column(Integer, primary_key=True)
154
  annotation_id = Column(Integer, ForeignKey("annotations.id"))
155
+ validator_id = Column(Integer, ForeignKey("annotators.id")) # Fixed: should reference annotators.id
156
  validated = Column(Boolean, nullable=False)
157
  description = Column(Text, nullable=True)
158
  validated_at = Column(DateTime, nullable=False)
159
 
160
  annotation = relationship("Annotation")
161
+ validator = relationship("Annotator", foreign_keys=[validator_id]) # Fixed: should reference Annotator
utils/auth.py CHANGED
@@ -4,6 +4,8 @@ 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()
9
 
@@ -17,14 +19,13 @@ class AuthService:
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
  # ---------- اعتبارسنجی ---------- #
@@ -51,9 +52,11 @@ class AuthService:
51
  return (
52
  "❌ Wrong username or password!", # message
53
  gr.update(), # login_container (no change)
54
- gr.update(visible=False), # dashboard_container
 
 
55
  gr.update(value=""), # header_welcome
56
- *empty_dashboard_outputs_for_ui(), # items_state, idx_state, and 4 UI textboxes
57
  )
58
 
59
  # --- رمز عبور اشتباه
@@ -62,88 +65,115 @@ class AuthService:
62
  return (
63
  "❌ Wrong username or password!", # message
64
  gr.update(), # login_container (no change)
65
- gr.update(visible=False), # dashboard_container
 
 
66
  gr.update(value=""), # header_welcome
67
- *empty_dashboard_outputs_for_ui(), # items_state, idx_state, and 4 UI textboxes
68
  )
69
 
70
  # ---------- ورود موفق ---------- #
71
  session["user_id"] = annotator.id
72
  session["username"] = annotator.name
 
73
 
74
- # بارگذاری داده‌های داشبورد
75
- workload_repo = AnnotatorWorkloadRepo(db)
76
- raw_items = workload_repo.get_tts_data_with_annotations(username)
77
-
78
- dashboard_items = [
79
- {
80
- "id": row["tts_data"].id,
81
- "filename": row["tts_data"].filename,
82
- "sentence": row["tts_data"].sentence,
83
- "annotated_sentence": (
84
- row["annotation"].annotated_sentence
85
- if row["annotation"] and row["annotation"].annotated_sentence is not None
86
- else ""
87
- ),
88
- "annotated_at": (
89
- row["annotation"].annotated_at.isoformat()
90
- if row["annotation"] and row["annotation"].annotated_at
91
- else ""
92
- ),
93
- "validated": (
94
- row["annotation"].validated
95
- if row["annotation"] is not None
96
- else False
97
- ),
98
- }
99
- for row in raw_items
100
- ]
101
-
102
- session["dashboard_items"] = dashboard_items
103
-
104
- # --- Resume Logic: Find first unannotated or default to first/last item ---
105
- initial_idx = 0
106
- if dashboard_items:
107
- first_unannotated_idx = -1
108
- for i, item_data in enumerate(dashboard_items):
109
- if not item_data["annotated_sentence"]: # Check if annotated_sentence is empty
110
- first_unannotated_idx = i
111
- break
112
-
113
- if first_unannotated_idx != -1:
114
- initial_idx = first_unannotated_idx
115
- log.info(f"User '{username}' resuming at first unannotated item, index: {initial_idx} (ID: {dashboard_items[initial_idx]['id']})")
116
- else: # All items are annotated or list is empty
117
- initial_idx = len(dashboard_items) - 1 if dashboard_items else 0 # Go to the last item if all annotated, else 0
118
- if dashboard_items:
119
- log.info(f"User '{username}' has all items annotated or list is empty, starting at item index: {initial_idx} (ID: {dashboard_items[initial_idx]['id']})")
120
- else: # No items assigned
121
- log.info(f"User '{username}' has no items assigned, starting at index 0.")
122
-
123
- # مقداردهی فیلدهای رکورد بر اساس initial_idx
124
- if dashboard_items: # Check if list is not empty and initial_idx is valid
125
- current_item_for_ui = dashboard_items[initial_idx]
126
- first_vals_for_ui = (
127
- current_item_for_ui["id"],
128
- current_item_for_ui["filename"],
129
- current_item_for_ui["sentence"],
130
- current_item_for_ui["annotated_sentence"],
131
  )
132
  else:
133
- first_vals_for_ui = ("", "", "", "") # id, filename, sentence, ann_sentence
134
-
135
- log.info(f"User '{username}' logged in successfully. Initial index set to: {initial_idx}")
136
-
137
- # ---------- خروجی نهایی برای Gradio ---------- #
138
- return (
139
- None, # 0: پیام خطا وجود ندارد
140
- gr.update(visible=False), # 1: فرم لاگین را مخفی کن
141
- gr.update(visible=True), # 2: داشبورد را نشان بده
142
- gr.update(value=f"👋 Welcome, {annotator.name}!"), # 3
143
- dashboard_items, # 4: items_state
144
- initial_idx, # 5: idx_state (Updated to determined initial_idx)
145
- *first_vals_for_ui, # 6-9: چهار فیلد نخست برای UI (from item at initial_idx)
146
- )
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
147
 
148
  # ───────────── LOGOUT ───────────── #
149
  @staticmethod
@@ -153,7 +183,8 @@ class AuthService:
153
  log.info(f"User '{username}' logged out.")
154
  return (
155
  gr.update(visible=True), # 1 → login_page.container
156
- gr.update(visible=False), # 2 → dashboard_page.container
157
- gr.update(value=""), # 3 → self.welcome
158
- gr.update(value=""), # 4 → login_page.message
 
159
  )
 
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
+ from config import conf
8
+ import time # Add this import
9
 
10
  log = Logger()
11
 
 
19
  @staticmethod
20
  def login(username: str, password: str, session: dict):
21
  """
22
+ Returns different UI states based on user role:
23
+ - Phase 1 users: Normal annotation interface
24
+ - Phase 2 users: Review interface
25
+
26
+ Outputs depend on which interface is being used:
27
+ For Phase 1: (message, login_container, dashboard_container, header_welcome, items_state, idx_state, tts_id, filename, sentence, ann_sentence)
28
+ For Phase 2: (message, login_container, dashboard_container, review_dashboard_container, header_welcome, items_state, idx_state, tts_id, filename, sentence, ann_sentence)
 
29
  """
30
 
31
  # ---------- اعتبارسنجی ---------- #
 
52
  return (
53
  "❌ Wrong username or password!", # message
54
  gr.update(), # login_container (no change)
55
+ gr.update(visible=False), # dashboard_container (Phase 1)
56
+ gr.update(visible=False), # review_dashboard_container (Phase 2)
57
+ gr.update(), # review_dashboard_page.load_trigger
58
  gr.update(value=""), # header_welcome
59
+ *empty_dashboard_outputs_for_ui(),
60
  )
61
 
62
  # --- رمز عبور اشتباه
 
65
  return (
66
  "❌ Wrong username or password!", # message
67
  gr.update(), # login_container (no change)
68
+ gr.update(visible=False), # dashboard_container (Phase 1)
69
+ gr.update(visible=False), # review_dashboard_container (Phase 2)
70
+ gr.update(), # review_dashboard_page.load_trigger
71
  gr.update(value=""), # header_welcome
72
+ *empty_dashboard_outputs_for_ui(),
73
  )
74
 
75
  # ---------- ورود موفق ---------- #
76
  session["user_id"] = annotator.id
77
  session["username"] = annotator.name
78
+ session["is_reviewer"] = annotator.name in conf.REVIEW_MAPPING.values()
79
 
80
+
81
+ if session["is_reviewer"]:
82
+ log.info(f"User '{username}' is a Phase 2 reviewer")
83
+ return (
84
+ None, # 0: message
85
+ gr.update(visible=False), # 1: login_container
86
+ gr.update(visible=False), # 2: dashboard_container (Phase 1)
87
+ gr.update(visible=True), # 3: review_dashboard_container (Phase 2)
88
+ gr.update(value=time.time()), # 4: review_dashboard_page.load_trigger
89
+ gr.update(value=f"👋 Welcome, {annotator.name}! (Phase 2 Review Mode)"), # 5: header_welcome
90
+ # Dummy updates for dashboard_page components
91
+ gr.update(value=[]), # 6: dashboard_page.items_state
92
+ gr.update(value=0), # 7: dashboard_page.idx_state
93
+ gr.update(value=""), # 8: dashboard_page.tts_id
94
+ gr.update(value=""), # 9: dashboard_page.filename
95
+ gr.update(value=""), # 10: dashboard_page.sentence
96
+ gr.update(value=""), # 11: dashboard_page.ann_sentence
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
97
  )
98
  else:
99
+ # Phase 1 users - existing logic
100
+ log.info(f"User '{username}' is a Phase 1 annotator")
101
+
102
+ # بارگذاری داده‌های داشبورد
103
+ workload_repo = AnnotatorWorkloadRepo(db)
104
+ raw_items = workload_repo.get_tts_data_with_annotations(username)
105
+
106
+ dashboard_items = [
107
+ {
108
+ "id": row["tts_data"].id,
109
+ "filename": row["tts_data"].filename,
110
+ "sentence": row["tts_data"].sentence,
111
+ "annotated_sentence": (
112
+ row["annotation"].annotated_sentence
113
+ if row["annotation"] and row["annotation"].annotated_sentence is not None
114
+ else ""
115
+ ),
116
+ "annotated_at": (
117
+ row["annotation"].annotated_at.isoformat()
118
+ if row["annotation"] and row["annotation"].annotated_at
119
+ else ""
120
+ ),
121
+ "validated": (
122
+ row["annotation"].validated
123
+ if row["annotation"] is not None
124
+ else False
125
+ ),
126
+ }
127
+ for row in raw_items
128
+ ]
129
+
130
+ session["dashboard_items"] = dashboard_items
131
+
132
+ # --- Resume Logic: Find first unannotated or default to first/last item ---
133
+ initial_idx = 0
134
+ if dashboard_items:
135
+ first_unannotated_idx = -1
136
+ for i, item_data in enumerate(dashboard_items):
137
+ if not item_data["annotated_sentence"]: # Check if annotated_sentence is empty
138
+ first_unannotated_idx = i
139
+ break
140
+
141
+ if first_unannotated_idx != -1:
142
+ initial_idx = first_unannotated_idx
143
+ log.info(f"User '{username}' resuming at first unannotated item, index: {initial_idx} (ID: {dashboard_items[initial_idx]['id']})")
144
+ else: # All items are annotated or list is empty
145
+ initial_idx = len(dashboard_items) - 1 if dashboard_items else 0 # Go to the last item if all annotated, else 0
146
+ if dashboard_items:
147
+ log.info(f"User '{username}' has all items annotated or list is empty, starting at item index: {initial_idx} (ID: {dashboard_items[initial_idx]['id']})")
148
+ else: # No items assigned
149
+ log.info(f"User '{username}' has no items assigned, starting at index 0.")
150
+
151
+ # مقداردهی فیلدهای رکورد بر اساس initial_idx
152
+ if dashboard_items: # Check if list is not empty and initial_idx is valid
153
+ current_item_for_ui = dashboard_items[initial_idx]
154
+ first_vals_for_ui = (
155
+ current_item_for_ui["id"],
156
+ current_item_for_ui["filename"],
157
+ current_item_for_ui["sentence"],
158
+ current_item_for_ui["annotated_sentence"],
159
+ )
160
+ else:
161
+ first_vals_for_ui = ("", "", "", "") # id, filename, sentence, ann_sentence
162
+
163
+ log.info(f"User '{username}' logged in successfully. Initial index set to: {initial_idx}")
164
+
165
+ # ---------- خروجی نهایی برای Gradio ---------- #
166
+ return (
167
+ None, # 0: پیام خطا وجود ندارد
168
+ gr.update(visible=False), # 1: فرم لاگین را مخفی کن
169
+ gr.update(visible=True), # 2: داشبورد را نشان بده (Phase 1)
170
+ gr.update(visible=False), # 3: review dashboard را مخفی کن (Phase 2)
171
+ gr.update(), # 4: review_dashboard_page.load_trigger (no change)
172
+ gr.update(value=f"👋 Welcome, {annotator.name}!"), # 5
173
+ dashboard_items, # 6: items_state
174
+ initial_idx, # 7: idx_state
175
+ *first_vals_for_ui, # 8-11: چهار فیلد نخست برای UI
176
+ )
177
 
178
  # ───────────── LOGOUT ───────────── #
179
  @staticmethod
 
183
  log.info(f"User '{username}' logged out.")
184
  return (
185
  gr.update(visible=True), # 1 → login_page.container
186
+ gr.update(visible=False), # 2 → dashboard_page.container (Phase 1)
187
+ gr.update(visible=False), # 3 → review_dashboard_page.container (Phase 2)
188
+ gr.update(value=""), # 4 → self.welcome
189
+ gr.update(value=""), # 5 → login_page.message
190
  )