Spaces:
Running
Running
phase 2 validation
Browse files- app.py +7 -4
- components/dashboard_page.py +2 -2
- components/header.py +17 -9
- components/login_page.py +27 -13
- components/review_dashboard_page.py +418 -0
- config.py +7 -0
- data/models.py +2 -2
- utils/auth.py +117 -86
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
|
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
|
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("
|
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("
|
|
|
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 |
-
|
21 |
-
|
22 |
-
|
23 |
-
|
24 |
-
|
25 |
-
|
26 |
-
|
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(),
|
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(),
|
68 |
)
|
69 |
|
70 |
# ---------- ورود موفق ---------- #
|
71 |
session["user_id"] = annotator.id
|
72 |
session["username"] = annotator.name
|
|
|
73 |
|
74 |
-
|
75 |
-
|
76 |
-
|
77 |
-
|
78 |
-
|
79 |
-
|
80 |
-
|
81 |
-
|
82 |
-
|
83 |
-
"
|
84 |
-
|
85 |
-
|
86 |
-
|
87 |
-
),
|
88 |
-
"
|
89 |
-
|
90 |
-
|
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 |
-
|
134 |
-
|
135 |
-
|
136 |
-
|
137 |
-
|
138 |
-
|
139 |
-
|
140 |
-
|
141 |
-
|
142 |
-
|
143 |
-
|
144 |
-
|
145 |
-
|
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(
|
158 |
-
gr.update(value=""), # 4 →
|
|
|
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 |
)
|