Spaces:
Running
Running
Resume Feature Implementation
Browse files- assets/styles.css +51 -5
- components/review_dashboard_page.py +294 -237
assets/styles.css
CHANGED
@@ -130,21 +130,55 @@
|
|
130 |
}
|
131 |
|
132 |
.progress-details code {
|
133 |
-
background: #f8f9fa;
|
134 |
-
border: 1px solid #e9ecef;
|
135 |
-
border-radius: 4px;
|
136 |
padding: 2px 6px;
|
137 |
-
|
138 |
font-size: 11px;
|
139 |
-
letter-spacing: 0.5px;
|
140 |
color: #495057;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
141 |
}
|
142 |
|
143 |
.remaining-items {
|
144 |
color: #6c757d;
|
|
|
145 |
font-style: italic;
|
146 |
}
|
147 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
148 |
@keyframes pulse {
|
149 |
0%, 100% { transform: scale(1); }
|
150 |
50% { transform: scale(1.1); }
|
@@ -179,4 +213,16 @@
|
|
179 |
.remaining-items {
|
180 |
color: #adb5bd;
|
181 |
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
182 |
}
|
|
|
130 |
}
|
131 |
|
132 |
.progress-details code {
|
133 |
+
background-color: #f8f9fa;
|
|
|
|
|
134 |
padding: 2px 6px;
|
135 |
+
border-radius: 4px;
|
136 |
font-size: 11px;
|
|
|
137 |
color: #495057;
|
138 |
+
border: 1px solid #dee2e6;
|
139 |
+
}
|
140 |
+
|
141 |
+
/* Progress Position Styles */
|
142 |
+
.progress-position {
|
143 |
+
margin-top: 8px;
|
144 |
+
padding: 8px 12px;
|
145 |
+
background: linear-gradient(135deg, #f0f8ff 0%, #e6f3ff 100%);
|
146 |
+
border: 1px solid #b3d9ff;
|
147 |
+
border-radius: 6px;
|
148 |
+
font-size: 13px;
|
149 |
+
color: #0066cc;
|
150 |
+
text-align: center;
|
151 |
+
font-weight: 500;
|
152 |
+
}
|
153 |
+
|
154 |
+
.progress-position strong {
|
155 |
+
color: #004499;
|
156 |
}
|
157 |
|
158 |
.remaining-items {
|
159 |
color: #6c757d;
|
160 |
+
font-size: 12px;
|
161 |
font-style: italic;
|
162 |
}
|
163 |
|
164 |
+
/* Position Info Styles */
|
165 |
+
.position-info {
|
166 |
+
background: linear-gradient(135deg, #e3f2fd 0%, #bbdefb 100%);
|
167 |
+
border: 1px solid #90caf9;
|
168 |
+
border-radius: 8px;
|
169 |
+
padding: 12px 16px;
|
170 |
+
margin: 8px 0;
|
171 |
+
box-shadow: 0 2px 6px rgba(33, 150, 243, 0.15);
|
172 |
+
font-family: 'SF Mono', Monaco, 'Cascadia Code', 'Roboto Mono', Consolas, 'Courier New', monospace;
|
173 |
+
text-align: center;
|
174 |
+
color: #1565c0;
|
175 |
+
font-weight: 500;
|
176 |
+
}
|
177 |
+
|
178 |
+
.position-info strong {
|
179 |
+
color: #0d47a1;
|
180 |
+
}
|
181 |
+
|
182 |
@keyframes pulse {
|
183 |
0%, 100% { transform: scale(1); }
|
184 |
50% { transform: scale(1.1); }
|
|
|
213 |
.remaining-items {
|
214 |
color: #adb5bd;
|
215 |
}
|
216 |
+
|
217 |
+
/* Dark mode support for position info */
|
218 |
+
.position-info {
|
219 |
+
background: linear-gradient(135deg, #1e3a5f 0%, #1e4d7b 100%);
|
220 |
+
border-color: #1976d2;
|
221 |
+
color: #90caf9;
|
222 |
+
box-shadow: 0 2px 6px rgba(33, 150, 243, 0.3);
|
223 |
+
}
|
224 |
+
|
225 |
+
.position-info strong {
|
226 |
+
color: #64b5f6;
|
227 |
+
}
|
228 |
}
|
components/review_dashboard_page.py
CHANGED
@@ -3,7 +3,7 @@
|
|
3 |
import gradio as gr
|
4 |
import datetime
|
5 |
import sentry_sdk
|
6 |
-
from sqlalchemy import orm
|
7 |
|
8 |
from components.header import Header
|
9 |
from utils.logger import Logger
|
@@ -26,6 +26,10 @@ class ReviewDashboardPage:
|
|
26 |
# Review info banner
|
27 |
with gr.Row():
|
28 |
self.review_info = gr.Markdown("", elem_classes="review-banner")
|
|
|
|
|
|
|
|
|
29 |
|
30 |
with gr.Row():
|
31 |
# Left Column - Review Content
|
@@ -68,6 +72,7 @@ class ReviewDashboardPage:
|
|
68 |
min_width=120
|
69 |
)
|
70 |
self.btn_jump = gr.Button("Go to ID", min_width=70)
|
|
|
71 |
|
72 |
# Right Column - Audio
|
73 |
with gr.Column(scale=2):
|
@@ -100,7 +105,8 @@ class ReviewDashboardPage:
|
|
100 |
# List of interactive UI elements for enabling/disabling
|
101 |
self.interactive_ui_elements = [
|
102 |
self.btn_prev, self.btn_next, self.btn_approve, self.btn_reject,
|
103 |
-
self.btn_skip, self.btn_jump, self.jump_data_id_input, self.btn_load_voice
|
|
|
104 |
]
|
105 |
|
106 |
def register_callbacks(self, login_page, session_state: gr.State, root_blocks: gr.Blocks):
|
@@ -171,13 +177,8 @@ class ReviewDashboardPage:
|
|
171 |
validation_status += f" ({validation.description})"
|
172 |
|
173 |
# For deleted annotations, show special status
|
174 |
-
if is_deleted:
|
175 |
-
|
176 |
-
validation_status = "Not Reviewed (Deleted)"
|
177 |
-
elif validation_status == "Approved":
|
178 |
-
validation_status = "Approved (Deleted)"
|
179 |
-
elif validation_status.startswith("Rejected"):
|
180 |
-
validation_status = f"Rejected (Deleted{validation_status[8:] if validation_status[8:] else ''})"
|
181 |
|
182 |
return validation_status, is_deleted
|
183 |
|
@@ -223,6 +224,19 @@ class ReviewDashboardPage:
|
|
223 |
Validation.validator_id == user_id
|
224 |
).count()
|
225 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
226 |
if total_count > 0:
|
227 |
percentage = (reviewed_count / total_count) * 100
|
228 |
|
@@ -254,7 +268,7 @@ class ReviewDashboardPage:
|
|
254 |
# Estimate remaining items
|
255 |
remaining = total_count - reviewed_count
|
256 |
|
257 |
-
# Create the beautiful progress display
|
258 |
progress_html = f"""
|
259 |
<div class="progress-container">
|
260 |
<div class="progress-header">
|
@@ -272,6 +286,9 @@ class ReviewDashboardPage:
|
|
272 |
π <code>{progress_bar}</code>
|
273 |
<span class="remaining-items">({remaining} remaining)</span>
|
274 |
</div>
|
|
|
|
|
|
|
275 |
</div>
|
276 |
"""
|
277 |
|
@@ -283,18 +300,64 @@ class ReviewDashboardPage:
|
|
283 |
log.error(f"Error calculating review progress for user {user_id}: {e}")
|
284 |
return f"β οΈ **Error calculating progress**"
|
285 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
286 |
def load_review_items_fn(session):
|
287 |
user_id = session.get("user_id")
|
288 |
username = session.get("username")
|
289 |
|
290 |
if not user_id or not username:
|
291 |
log.warning("load_review_items_fn: user not found in session")
|
292 |
-
return [], 0, "", "", "", "", "", "", "", "", gr.update(value=None, autoplay=False), gr.update(visible=False, value=""), False, gr.update(value="β Reject")
|
293 |
|
294 |
# Check if user is in Phase 2 (should be a reviewer)
|
295 |
if username not in conf.REVIEW_MAPPING.values():
|
296 |
log.warning(f"User {username} is not assigned as a reviewer")
|
297 |
-
return [], 0, "", "", "", "", "", "", "", "", gr.update(value=None, autoplay=False), gr.update(visible=False, value=""), False, gr.update(value="β Reject")
|
298 |
|
299 |
# Find which annotator this user should review
|
300 |
target_annotator = None
|
@@ -305,7 +368,7 @@ class ReviewDashboardPage:
|
|
305 |
|
306 |
if not target_annotator:
|
307 |
log.warning(f"No target annotator found for reviewer {username}")
|
308 |
-
return [], 0, "", "", "", "", "", "", "", "", gr.update(value=None, autoplay=False), gr.update(visible=False, value=""), False, gr.update(value="β Reject")
|
309 |
|
310 |
# Load annotations from target annotator with FAST INITIAL LOADING
|
311 |
with get_db() as db:
|
@@ -313,33 +376,99 @@ class ReviewDashboardPage:
|
|
313 |
target_annotator_obj = db.query(Annotator).filter_by(name=target_annotator).first()
|
314 |
if not target_annotator_obj:
|
315 |
log.error(f"Target annotator {target_annotator} not found in database")
|
316 |
-
return [], 0, f"Review Target Error: Annotator '{target_annotator}' not found.", "", "", "", "", "", "", "", gr.update(value=None, autoplay=False), gr.update(visible=False, value=""), False, gr.update(value="β Reject")
|
317 |
|
318 |
log.info(f"Found target annotator with ID: {target_annotator_obj.id}")
|
319 |
|
320 |
-
# FAST INITIAL QUERY: Load only essential data without complex validation processing
|
321 |
-
# Reduced batch size for instant loading in HuggingFace spaces
|
322 |
-
INITIAL_BATCH_SIZE = 5 # Load only 5 items initially for instant response
|
323 |
-
|
324 |
-
# Simple query to get basic annotation data quickly
|
325 |
-
initial_query = db.query(
|
326 |
-
Annotation,
|
327 |
-
TTSData.filename,
|
328 |
-
TTSData.sentence
|
329 |
-
).join(
|
330 |
-
TTSData, Annotation.tts_data_id == TTSData.id
|
331 |
-
).filter(
|
332 |
-
Annotation.annotator_id == target_annotator_obj.id
|
333 |
-
).order_by(Annotation.id).limit(INITIAL_BATCH_SIZE)
|
334 |
-
|
335 |
-
initial_results = initial_query.all()
|
336 |
-
|
337 |
# Get total count for progress info (this is fast)
|
338 |
total_count = db.query(Annotation).filter(
|
339 |
Annotation.annotator_id == target_annotator_obj.id
|
340 |
).count()
|
341 |
|
342 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
343 |
|
344 |
# Process items with minimal data - validation status will be loaded on-demand
|
345 |
items = []
|
@@ -360,100 +489,14 @@ class ReviewDashboardPage:
|
|
360 |
"validation_loaded": False # Track if validation status has been loaded
|
361 |
})
|
362 |
|
363 |
-
|
364 |
-
|
365 |
-
if items:
|
366 |
-
log.info(f"Starting resume logic search with {len(items)} items")
|
367 |
-
# First, try to find the first unreviewed item that is not deleted
|
368 |
-
found_unreviewed = False
|
369 |
-
for i, item_data in enumerate(items):
|
370 |
-
if not item_data.get("is_deleted", False):
|
371 |
-
# Check if this item has been reviewed by the current validator
|
372 |
-
validation = db.query(Validation).filter_by(
|
373 |
-
annotation_id=item_data["annotation_id"],
|
374 |
-
validator_id=user_id
|
375 |
-
).first()
|
376 |
-
|
377 |
-
if not validation:
|
378 |
-
# This item hasn't been reviewed yet
|
379 |
-
initial_idx = i
|
380 |
-
found_unreviewed = True
|
381 |
-
log.info(f"β
Resume point found in initial batch: index {initial_idx}, TTS ID {item_data['tts_id']}, filename: {item_data['filename']}")
|
382 |
-
break
|
383 |
-
else:
|
384 |
-
log.info(f"Item {i} (TTS ID {item_data['tts_id']}) already reviewed by validator {user_id}")
|
385 |
-
else:
|
386 |
-
log.info(f"Item {i} (TTS ID {item_data['tts_id']}) is deleted, skipping")
|
387 |
-
|
388 |
-
# If no unreviewed non-deleted items found in initial batch,
|
389 |
-
# we need to load more items to find the resume point
|
390 |
-
if not found_unreviewed:
|
391 |
-
log.info("No unreviewed items found in initial batch, loading more items to find resume point")
|
392 |
-
|
393 |
-
# Load more items to find the resume point
|
394 |
-
offset = len(items)
|
395 |
-
additional_query = db.query(
|
396 |
-
Annotation,
|
397 |
-
TTSData.filename,
|
398 |
-
TTSData.sentence
|
399 |
-
).join(
|
400 |
-
TTSData, Annotation.tts_data_id == TTSData.id
|
401 |
-
).filter(
|
402 |
-
Annotation.annotator_id == target_annotator_obj.id
|
403 |
-
).order_by(Annotation.id).offset(offset).limit(20) # Load more to find resume point
|
404 |
-
|
405 |
-
additional_results = additional_query.all()
|
406 |
-
log.info(f"Loaded {len(additional_results)} additional items to search for resume point")
|
407 |
-
|
408 |
-
# Process additional items
|
409 |
-
for annotation, filename, sentence in additional_results:
|
410 |
-
is_deleted = not annotation.annotated_sentence or annotation.annotated_sentence.strip() == ""
|
411 |
-
annotated_sentence_display = "[DELETED ANNOTATION]" if is_deleted else annotation.annotated_sentence
|
412 |
-
|
413 |
-
items.append({
|
414 |
-
"annotation_id": annotation.id,
|
415 |
-
"tts_id": annotation.tts_data_id,
|
416 |
-
"filename": filename,
|
417 |
-
"sentence": sentence,
|
418 |
-
"annotated_sentence": annotated_sentence_display,
|
419 |
-
"is_deleted": is_deleted,
|
420 |
-
"annotated_at": annotation.annotated_at.isoformat() if annotation.annotated_at else "",
|
421 |
-
"validation_status": "Loading...",
|
422 |
-
"validation_loaded": False
|
423 |
-
})
|
424 |
-
|
425 |
-
# Now search for the first unreviewed item in the expanded list
|
426 |
-
log.info(f"Searching expanded list of {len(items)} items for resume point")
|
427 |
-
for i, item_data in enumerate(items):
|
428 |
-
if not item_data.get("is_deleted", False):
|
429 |
-
validation = db.query(Validation).filter_by(
|
430 |
-
annotation_id=item_data["annotation_id"],
|
431 |
-
validator_id=user_id
|
432 |
-
).first()
|
433 |
-
|
434 |
-
if not validation:
|
435 |
-
initial_idx = i
|
436 |
-
found_unreviewed = True
|
437 |
-
log.info(f"β
Resume point found in expanded list: index {initial_idx}, TTS ID {item_data['tts_id']}, filename: {item_data['filename']}")
|
438 |
-
break
|
439 |
-
else:
|
440 |
-
log.info(f"Item {i} (TTS ID {item_data['tts_id']}) already reviewed by validator {user_id}")
|
441 |
-
else:
|
442 |
-
log.info(f"Item {i} (TTS ID {item_data['tts_id']}) is deleted, skipping")
|
443 |
-
|
444 |
-
# If still no unreviewed items found, start from the last item
|
445 |
-
if not found_unreviewed:
|
446 |
-
initial_idx = len(items) - 1 if items else 0
|
447 |
-
log.info(f"β οΈ All items reviewed, starting at last item, index: {initial_idx}")
|
448 |
-
|
449 |
-
log.info(f"Final resume decision: starting at index {initial_idx} with {len(items)} total items loaded")
|
450 |
-
|
451 |
# Set initial display
|
452 |
if items:
|
453 |
initial_item = items[initial_idx]
|
454 |
-
review_info_text = f"π **Phase 2 Review Mode** - Reviewing assigned annotations. Loaded {len(items)} of {total_count} total items."
|
455 |
-
|
456 |
-
#
|
457 |
rejection_reason_val = ""
|
458 |
rejection_visible_val = False
|
459 |
if initial_item["validation_status"].startswith("Rejected"):
|
@@ -477,23 +520,17 @@ class ReviewDashboardPage:
|
|
477 |
gr.update(value=None, autoplay=False), # audio_update
|
478 |
gr.update(visible=rejection_visible_val, value=rejection_reason_val), # rejection_reason_input update
|
479 |
False, # Reset rejection mode
|
480 |
-
gr.update(value="β Reject") # Reset reject button
|
|
|
481 |
)
|
482 |
else:
|
483 |
-
# Ensure correct order and number of return values for empty items (
|
484 |
-
return [], 0, f"π **Phase 2 Review Mode** - No annotations found for review.", "", "", "", "", "", "", "", gr.update(value=None, autoplay=False), gr.update(visible=False, value=""), False, gr.update(value="β Reject")
|
485 |
-
|
486 |
-
# except Exception as e:
|
487 |
-
# log.error(f"Error loading review items: {e}")
|
488 |
-
# sentry_sdk.capture_exception(e)
|
489 |
-
# gr.Error(f"Failed to load review data: {e}")
|
490 |
-
# # Ensure correct order and number of return values for error case (14 outputs)
|
491 |
-
# return [], 0, "", "", "", "", "", "", "", "", gr.update(value=None, autoplay=False), gr.update(visible=False, value=""), False, gr.update(value="β Reject")
|
492 |
|
493 |
def show_current_review_item_fn(items, idx, session):
|
494 |
if not items or idx >= len(items) or idx < 0:
|
495 |
-
# tts_id, filename, sentence, ann_sentence, annotated_at, validation_status, annotator_name_placeholder, audio_update, rejection_reason_update, rejection_mode_reset, btn_reject_update
|
496 |
-
return "", "", "", "", "", "", "", gr.update(value=None, autoplay=False), gr.update(visible=False, value=""), False, gr.update(value="β Reject")
|
497 |
|
498 |
current_item = items[idx]
|
499 |
|
@@ -526,7 +563,6 @@ class ReviewDashboardPage:
|
|
526 |
# Check if this is a deleted annotation
|
527 |
is_deleted = current_item.get("is_deleted", False)
|
528 |
|
529 |
-
# Handle validation status display
|
530 |
if current_item["validation_status"].startswith("Rejected"):
|
531 |
# Extract reason from status like "Rejected (reason)" or just use empty if no parenthesis
|
532 |
start_paren = current_item["validation_status"].find("(")
|
@@ -534,14 +570,9 @@ class ReviewDashboardPage:
|
|
534 |
if start_paren != -1 and end_paren != -1:
|
535 |
rejection_reason = current_item["validation_status"][start_paren+1:end_paren]
|
536 |
rejection_visible = True
|
537 |
-
|
538 |
-
|
539 |
-
|
540 |
-
rejection_reason = ""
|
541 |
-
elif current_item["validation_status"] == "Not Reviewed":
|
542 |
-
# Regular unreviewed items
|
543 |
-
rejection_visible = False
|
544 |
-
rejection_reason = ""
|
545 |
|
546 |
return (
|
547 |
str(current_item["tts_id"]),
|
@@ -554,78 +585,18 @@ class ReviewDashboardPage:
|
|
554 |
gr.update(value=None, autoplay=False),
|
555 |
gr.update(visible=rejection_visible, value=rejection_reason),
|
556 |
False, # Reset rejection mode
|
557 |
-
gr.update(value="β Reject") # Reset reject button text
|
|
|
558 |
)
|
559 |
|
560 |
def update_review_info_fn(items, total_count):
|
561 |
-
"""Update the review info banner with current loaded items count"""
|
562 |
if items:
|
563 |
-
|
|
|
564 |
else:
|
565 |
return f"π **Phase 2 Review Mode** - No annotations found for review."
|
566 |
|
567 |
-
def find_next_unreviewed_item(items, current_idx, session):
|
568 |
-
"""Find the next unreviewed item starting from current_idx"""
|
569 |
-
if not items or current_idx >= len(items):
|
570 |
-
log.warning(f"find_next_unreviewed_item: Invalid items or current_idx. items: {len(items) if items else 0}, current_idx: {current_idx}")
|
571 |
-
return current_idx
|
572 |
-
|
573 |
-
user_id = session.get("user_id")
|
574 |
-
if not user_id:
|
575 |
-
log.warning("find_next_unreviewed_item: No user_id in session")
|
576 |
-
return current_idx
|
577 |
-
|
578 |
-
log.info(f"Searching for next unreviewed item starting from index {current_idx} (TTS ID: {items[current_idx]['tts_id'] if current_idx < len(items) else 'N/A'})")
|
579 |
-
|
580 |
-
# Start searching from the next item
|
581 |
-
start_idx = current_idx + 1
|
582 |
-
if start_idx >= len(items):
|
583 |
-
start_idx = 0 # Wrap around to beginning
|
584 |
-
log.info("Wrapping around to beginning of items list")
|
585 |
-
|
586 |
-
# Search forward from start_idx
|
587 |
-
for i in range(len(items)):
|
588 |
-
check_idx = (start_idx + i) % len(items)
|
589 |
-
item_data = items[check_idx]
|
590 |
-
|
591 |
-
log.info(f"Checking item {check_idx} (TTS ID: {item_data['tts_id']}, filename: {item_data['filename']})")
|
592 |
-
|
593 |
-
# Skip deleted annotations
|
594 |
-
if item_data.get("is_deleted", False):
|
595 |
-
log.info(f"Item {check_idx} is deleted, skipping")
|
596 |
-
continue
|
597 |
-
|
598 |
-
# Check if this item has been reviewed
|
599 |
-
if not item_data.get("validation_loaded", False):
|
600 |
-
# Load validation status on-demand
|
601 |
-
with get_db() as db:
|
602 |
-
try:
|
603 |
-
validation = db.query(Validation).filter_by(
|
604 |
-
annotation_id=item_data["annotation_id"],
|
605 |
-
validator_id=user_id
|
606 |
-
).first()
|
607 |
-
item_data["validation_loaded"] = True
|
608 |
-
if validation:
|
609 |
-
item_data["validation_status"] = "Approved" if validation.validated else f"Rejected ({validation.description})" if validation.description else "Rejected"
|
610 |
-
log.info(f"Item {check_idx} validation status loaded: {item_data['validation_status']}")
|
611 |
-
else:
|
612 |
-
item_data["validation_status"] = "Not Reviewed"
|
613 |
-
log.info(f"Item {check_idx} has no validation record - unreviewed")
|
614 |
-
except Exception as e:
|
615 |
-
log.error(f"Error loading validation status for item {check_idx}: {e}")
|
616 |
-
item_data["validation_status"] = "Error loading status"
|
617 |
-
|
618 |
-
# If not reviewed, this is our target
|
619 |
-
if item_data["validation_status"] == "Not Reviewed":
|
620 |
-
log.info(f"β
Found next unreviewed item at index {check_idx} (TTS ID: {item_data['tts_id']}, filename: {item_data['filename']})")
|
621 |
-
return check_idx
|
622 |
-
else:
|
623 |
-
log.info(f"Item {check_idx} already reviewed with status: {item_data['validation_status']}")
|
624 |
-
|
625 |
-
# If no unreviewed items found, return current index
|
626 |
-
log.info("β οΈ No unreviewed items found, staying at current position")
|
627 |
-
return current_idx
|
628 |
-
|
629 |
def navigate_and_load_fn(items, current_idx, direction, session):
|
630 |
"""Combined navigation and loading function"""
|
631 |
if not items:
|
@@ -787,18 +758,32 @@ class ReviewDashboardPage:
|
|
787 |
).count()
|
788 |
|
789 |
# Load next batch starting from where we left off
|
790 |
-
|
791 |
-
|
792 |
-
|
793 |
-
|
794 |
-
|
795 |
-
|
796 |
-
|
797 |
-
|
798 |
-
|
799 |
-
|
800 |
-
|
801 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
802 |
|
803 |
results = query.all()
|
804 |
|
@@ -824,17 +809,66 @@ class ReviewDashboardPage:
|
|
824 |
# Combine with existing items
|
825 |
all_items = items + new_items
|
826 |
log.info(f"Loaded {len(new_items)} more items, total now: {len(all_items)}")
|
827 |
-
|
828 |
-
# If this is the first time loading more items, we might need to find the resume point
|
829 |
-
# This ensures that even if the initial batch didn't find unreviewed items,
|
830 |
-
# we can still resume from the correct position
|
831 |
-
if len(items) == 5 and len(all_items) > 5: # Initial batch was 5 items
|
832 |
-
log.info("First load_more call, checking if we need to find resume point")
|
833 |
-
# The resume logic in load_review_items_fn should have already handled this,
|
834 |
-
# but we can add additional safety here if needed
|
835 |
-
|
836 |
return all_items, total_count
|
837 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
838 |
# Output definitions
|
839 |
review_display_outputs = [
|
840 |
self.tts_id, self.filename, self.sentence, self.ann_sentence,
|
@@ -844,7 +878,8 @@ class ReviewDashboardPage:
|
|
844 |
self.audio,
|
845 |
self.rejection_reason_input, # Added rejection reason input to display outputs
|
846 |
self.rejection_mode_active, # Added rejection mode state
|
847 |
-
self.btn_reject # Added reject button to display outputs
|
|
|
848 |
]
|
849 |
|
850 |
# Trigger data loading when load_trigger changes (after successful login for a reviewer)
|
@@ -912,9 +947,9 @@ class ReviewDashboardPage:
|
|
912 |
fn=lambda: gr.update(value="β Reject"), # Reset reject button
|
913 |
outputs=[self.btn_reject]
|
914 |
).then(
|
915 |
-
fn=lambda items, idx, session:
|
916 |
inputs=[self.items_state, self.idx_state, session_state],
|
917 |
-
outputs=[self.idx_state]
|
918 |
).then(
|
919 |
fn=show_current_review_item_fn,
|
920 |
inputs=[self.items_state, self.idx_state, session_state],
|
@@ -935,9 +970,9 @@ class ReviewDashboardPage:
|
|
935 |
inputs=[self.items_state, self.idx_state, session_state, self.rejection_mode_active],
|
936 |
outputs=[self.header.progress_display]
|
937 |
).then(
|
938 |
-
fn=lambda items, idx, session, rejection_mode:
|
939 |
inputs=[self.items_state, self.idx_state, session_state, self.rejection_mode_active],
|
940 |
-
outputs=[self.idx_state]
|
941 |
).then(
|
942 |
fn=lambda items, idx, session, rejection_mode: show_current_review_item_fn(items, idx, session) if not rejection_mode else (
|
943 |
str(items[idx]["tts_id"]) if items and idx < len(items) else "",
|
@@ -963,9 +998,9 @@ class ReviewDashboardPage:
|
|
963 |
|
964 |
# Skip button (just navigate to next)
|
965 |
self.btn_skip.click(
|
966 |
-
fn=lambda items, idx, session:
|
967 |
inputs=[self.items_state, self.idx_state, session_state],
|
968 |
-
outputs=[self.idx_state]
|
969 |
).then(
|
970 |
fn=show_current_review_item_fn,
|
971 |
inputs=[self.items_state, self.idx_state, session_state],
|
@@ -996,6 +1031,28 @@ class ReviewDashboardPage:
|
|
996 |
outputs=self.jump_data_id_input
|
997 |
)
|
998 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
999 |
# Load audio button
|
1000 |
self.btn_load_voice.click(
|
1001 |
fn=lambda: update_ui_interactive_state(False),
|
|
|
3 |
import gradio as gr
|
4 |
import datetime
|
5 |
import sentry_sdk
|
6 |
+
from sqlalchemy import orm, func
|
7 |
|
8 |
from components.header import Header
|
9 |
from utils.logger import Logger
|
|
|
26 |
# Review info banner
|
27 |
with gr.Row():
|
28 |
self.review_info = gr.Markdown("", elem_classes="review-banner")
|
29 |
+
|
30 |
+
# Position info display
|
31 |
+
with gr.Row():
|
32 |
+
self.position_info = gr.Markdown("", elem_classes="position-info")
|
33 |
|
34 |
with gr.Row():
|
35 |
# Left Column - Review Content
|
|
|
72 |
min_width=120
|
73 |
)
|
74 |
self.btn_jump = gr.Button("Go to ID", min_width=70)
|
75 |
+
self.btn_jump_to_unreviewed = gr.Button("π― Jump to Unreviewed", min_width=150, variant="secondary")
|
76 |
|
77 |
# Right Column - Audio
|
78 |
with gr.Column(scale=2):
|
|
|
105 |
# List of interactive UI elements for enabling/disabling
|
106 |
self.interactive_ui_elements = [
|
107 |
self.btn_prev, self.btn_next, self.btn_approve, self.btn_reject,
|
108 |
+
self.btn_skip, self.btn_jump, self.jump_data_id_input, self.btn_load_voice,
|
109 |
+
self.btn_jump_to_unreviewed
|
110 |
]
|
111 |
|
112 |
def register_callbacks(self, login_page, session_state: gr.State, root_blocks: gr.Blocks):
|
|
|
177 |
validation_status += f" ({validation.description})"
|
178 |
|
179 |
# For deleted annotations, show special status
|
180 |
+
if is_deleted and validation_status == "Not Reviewed":
|
181 |
+
validation_status = "Not Reviewed (Deleted)"
|
|
|
|
|
|
|
|
|
|
|
182 |
|
183 |
return validation_status, is_deleted
|
184 |
|
|
|
224 |
Validation.validator_id == user_id
|
225 |
).count()
|
226 |
|
227 |
+
# Find the first unreviewed item to show current position
|
228 |
+
first_unreviewed_query = db.query(Annotation.id).outerjoin(
|
229 |
+
Validation,
|
230 |
+
(Annotation.id == Validation.annotation_id) &
|
231 |
+
(Validation.validator_id == user_id)
|
232 |
+
).filter(
|
233 |
+
Annotation.annotator_id == target_annotator_obj.id,
|
234 |
+
Validation.id.is_(None) # No validation record exists for this reviewer
|
235 |
+
).order_by(Annotation.id).limit(1)
|
236 |
+
|
237 |
+
first_unreviewed_result = first_unreviewed_query.first()
|
238 |
+
current_position = "Completed" if not first_unreviewed_result else f"ID: {first_unreviewed_result.id}"
|
239 |
+
|
240 |
if total_count > 0:
|
241 |
percentage = (reviewed_count / total_count) * 100
|
242 |
|
|
|
268 |
# Estimate remaining items
|
269 |
remaining = total_count - reviewed_count
|
270 |
|
271 |
+
# Create the beautiful progress display with current position
|
272 |
progress_html = f"""
|
273 |
<div class="progress-container">
|
274 |
<div class="progress-header">
|
|
|
286 |
π <code>{progress_bar}</code>
|
287 |
<span class="remaining-items">({remaining} remaining)</span>
|
288 |
</div>
|
289 |
+
<div class="progress-position">
|
290 |
+
π― <strong>Current Position:</strong> {current_position}
|
291 |
+
</div>
|
292 |
</div>
|
293 |
"""
|
294 |
|
|
|
300 |
log.error(f"Error calculating review progress for user {user_id}: {e}")
|
301 |
return f"β οΈ **Error calculating progress**"
|
302 |
|
303 |
+
def get_current_item_position_info(items, current_idx, session):
|
304 |
+
"""Get information about the current item's position in the review queue"""
|
305 |
+
if not items or current_idx >= len(items):
|
306 |
+
return ""
|
307 |
+
|
308 |
+
user_id = session.get("user_id")
|
309 |
+
username = session.get("username")
|
310 |
+
if not user_id or not username:
|
311 |
+
return ""
|
312 |
+
|
313 |
+
# Find target annotator
|
314 |
+
target_annotator = None
|
315 |
+
for annotator_name, reviewer_name in conf.REVIEW_MAPPING.items():
|
316 |
+
if reviewer_name == username:
|
317 |
+
target_annotator = annotator_name
|
318 |
+
break
|
319 |
+
|
320 |
+
if not target_annotator:
|
321 |
+
return ""
|
322 |
+
|
323 |
+
with get_db() as db:
|
324 |
+
try:
|
325 |
+
# Get target annotator's ID
|
326 |
+
target_annotator_obj = db.query(Annotator).filter_by(name=target_annotator).first()
|
327 |
+
if not target_annotator_obj:
|
328 |
+
return ""
|
329 |
+
|
330 |
+
# Get total count
|
331 |
+
total_count = db.query(Annotation).filter(
|
332 |
+
Annotation.annotator_id == target_annotator_obj.id
|
333 |
+
).count()
|
334 |
+
|
335 |
+
# Get current item's position in the overall sequence
|
336 |
+
current_item_id = items[current_idx]["annotation_id"]
|
337 |
+
position_query = db.query(func.count(Annotation.id)).filter(
|
338 |
+
Annotation.annotator_id == target_annotator_obj.id,
|
339 |
+
Annotation.id <= current_item_id
|
340 |
+
)
|
341 |
+
current_position = position_query.scalar() or 0
|
342 |
+
|
343 |
+
return f"π **Position:** {current_position} of {total_count} items"
|
344 |
+
|
345 |
+
except Exception as e:
|
346 |
+
log.error(f"Error getting current item position: {e}")
|
347 |
+
return ""
|
348 |
+
|
349 |
def load_review_items_fn(session):
|
350 |
user_id = session.get("user_id")
|
351 |
username = session.get("username")
|
352 |
|
353 |
if not user_id or not username:
|
354 |
log.warning("load_review_items_fn: user not found in session")
|
355 |
+
return [], 0, "", "", "", "", "", "", "", "", gr.update(value=None, autoplay=False), gr.update(visible=False, value=""), False, gr.update(value="β Reject"), ""
|
356 |
|
357 |
# Check if user is in Phase 2 (should be a reviewer)
|
358 |
if username not in conf.REVIEW_MAPPING.values():
|
359 |
log.warning(f"User {username} is not assigned as a reviewer")
|
360 |
+
return [], 0, "", "", "", "", "", "", "", "", gr.update(value=None, autoplay=False), gr.update(visible=False, value=""), False, gr.update(value="β Reject"), ""
|
361 |
|
362 |
# Find which annotator this user should review
|
363 |
target_annotator = None
|
|
|
368 |
|
369 |
if not target_annotator:
|
370 |
log.warning(f"No target annotator found for reviewer {username}")
|
371 |
+
return [], 0, "", "", "", "", "", "", "", "", gr.update(value=None, autoplay=False), gr.update(visible=False, value=""), False, gr.update(value="β Reject"), ""
|
372 |
|
373 |
# Load annotations from target annotator with FAST INITIAL LOADING
|
374 |
with get_db() as db:
|
|
|
376 |
target_annotator_obj = db.query(Annotator).filter_by(name=target_annotator).first()
|
377 |
if not target_annotator_obj:
|
378 |
log.error(f"Target annotator {target_annotator} not found in database")
|
379 |
+
return [], 0, f"Review Target Error: Annotator '{target_annotator}' not found.", "", "", "", "", "", "", "", gr.update(value=None, autoplay=False), gr.update(visible=False, value=""), False, gr.update(value="β Reject"), ""
|
380 |
|
381 |
log.info(f"Found target annotator with ID: {target_annotator_obj.id}")
|
382 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
383 |
# Get total count for progress info (this is fast)
|
384 |
total_count = db.query(Annotation).filter(
|
385 |
Annotation.annotator_id == target_annotator_obj.id
|
386 |
).count()
|
387 |
|
388 |
+
# RESUME LOGIC: Find the first unreviewed item by this reviewer
|
389 |
+
# Query to find the first unreviewed annotation for this reviewer
|
390 |
+
first_unreviewed_query = db.query(
|
391 |
+
Annotation.id,
|
392 |
+
Annotation.tts_data_id,
|
393 |
+
Annotation.annotated_sentence,
|
394 |
+
Annotation.annotated_at
|
395 |
+
).outerjoin(
|
396 |
+
Validation,
|
397 |
+
(Annotation.id == Validation.annotation_id) &
|
398 |
+
(Validation.validator_id == user_id)
|
399 |
+
).filter(
|
400 |
+
Annotation.annotator_id == target_annotator_obj.id,
|
401 |
+
Validation.id.is_(None) # No validation record exists for this reviewer
|
402 |
+
).order_by(Annotation.id).limit(1)
|
403 |
+
|
404 |
+
first_unreviewed_result = first_unreviewed_query.first()
|
405 |
+
|
406 |
+
if first_unreviewed_result:
|
407 |
+
# Found unreviewed item - start from there
|
408 |
+
first_unreviewed_id = first_unreviewed_result.id
|
409 |
+
log.info(f"Found first unreviewed item with ID: {first_unreviewed_id}")
|
410 |
+
|
411 |
+
# Load items starting from the unreviewed item
|
412 |
+
# Load a batch around the unreviewed item for context
|
413 |
+
BATCH_SIZE = 10
|
414 |
+
offset = max(0, first_unreviewed_id - 5) # Start 5 items before the unreviewed item
|
415 |
+
|
416 |
+
# Query to get items around the unreviewed item
|
417 |
+
items_query = db.query(
|
418 |
+
Annotation,
|
419 |
+
TTSData.filename,
|
420 |
+
TTSData.sentence
|
421 |
+
).join(
|
422 |
+
TTSData, Annotation.tts_data_id == TTSData.id
|
423 |
+
).filter(
|
424 |
+
Annotation.annotator_id == target_annotator_obj.id,
|
425 |
+
Annotation.id >= offset
|
426 |
+
).order_by(Annotation.id).limit(BATCH_SIZE)
|
427 |
+
|
428 |
+
initial_results = items_query.all()
|
429 |
+
|
430 |
+
# Find the index of the first unreviewed item in our loaded batch
|
431 |
+
initial_idx = 0
|
432 |
+
for i, (annotation, filename, sentence) in enumerate(initial_results):
|
433 |
+
if annotation.id == first_unreviewed_id:
|
434 |
+
initial_idx = i
|
435 |
+
break
|
436 |
+
|
437 |
+
log.info(f"Resuming at first unreviewed item, index: {initial_idx} (ID: {first_unreviewed_id})")
|
438 |
+
else:
|
439 |
+
# No unreviewed items found - all items have been reviewed
|
440 |
+
log.info(f"All items for {target_annotator} have been reviewed by {username}")
|
441 |
+
|
442 |
+
# Load the last batch of items to show completion
|
443 |
+
BATCH_SIZE = 10
|
444 |
+
items_query = db.query(
|
445 |
+
Annotation,
|
446 |
+
TTSData.filename,
|
447 |
+
TTSData.sentence
|
448 |
+
).join(
|
449 |
+
TTSData, Annotation.tts_data_id == TTSData.id
|
450 |
+
).filter(
|
451 |
+
Annotation.annotator_id == target_annotator_obj.id
|
452 |
+
).order_by(Annotation.id.desc()).limit(BATCH_SIZE)
|
453 |
+
|
454 |
+
# Reverse the results to maintain chronological order
|
455 |
+
initial_results = list(reversed(items_query.all()))
|
456 |
+
initial_idx = len(initial_results) - 1 if initial_results else 0
|
457 |
+
|
458 |
+
log.info(f"All items reviewed, starting at last item, index: {initial_idx}")
|
459 |
+
|
460 |
+
# Set completion message
|
461 |
+
if initial_results:
|
462 |
+
review_info_text = f"π **Phase 2 Review Mode** - All annotations for {target_annotator} have been reviewed! Showing last {len(initial_results)} items for reference."
|
463 |
+
else:
|
464 |
+
review_info_text = f"π **Phase 2 Review Mode** - All annotations for {target_annotator} have been reviewed! No items to display."
|
465 |
+
|
466 |
+
return (
|
467 |
+
[],
|
468 |
+
0,
|
469 |
+
review_info_text,
|
470 |
+
"", "", "", "", "", "", "", gr.update(value=None, autoplay=False), gr.update(visible=False, value=""), False, gr.update(value="β Reject"), ""
|
471 |
+
)
|
472 |
|
473 |
# Process items with minimal data - validation status will be loaded on-demand
|
474 |
items = []
|
|
|
489 |
"validation_loaded": False # Track if validation status has been loaded
|
490 |
})
|
491 |
|
492 |
+
log.info(f"Fast initial load: {len(items)} annotations out of {total_count} total for target annotator ID {target_annotator_obj.id}")
|
493 |
+
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
494 |
# Set initial display
|
495 |
if items:
|
496 |
initial_item = items[initial_idx]
|
497 |
+
review_info_text = f"π **Phase 2 Review Mode** - Reviewing assigned annotations. Loaded {len(items)} of {total_count} total items. Resuming from ID: {initial_item['annotation_id']}"
|
498 |
+
|
499 |
+
# Ensure correct order of return values for 14 outputs
|
500 |
rejection_reason_val = ""
|
501 |
rejection_visible_val = False
|
502 |
if initial_item["validation_status"].startswith("Rejected"):
|
|
|
520 |
gr.update(value=None, autoplay=False), # audio_update
|
521 |
gr.update(visible=rejection_visible_val, value=rejection_reason_val), # rejection_reason_input update
|
522 |
False, # Reset rejection mode
|
523 |
+
gr.update(value="β Reject"), # Reset reject button
|
524 |
+
get_current_item_position_info(items, initial_idx, session) # Position info
|
525 |
)
|
526 |
else:
|
527 |
+
# Ensure correct order and number of return values for empty items (15 outputs)
|
528 |
+
return [], 0, f"π **Phase 2 Review Mode** - No annotations found for review.", "", "", "", "", "", "", "", gr.update(value=None, autoplay=False), gr.update(visible=False, value=""), False, gr.update(value="β Reject"), ""
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
529 |
|
530 |
def show_current_review_item_fn(items, idx, session):
|
531 |
if not items or idx >= len(items) or idx < 0:
|
532 |
+
# tts_id, filename, sentence, ann_sentence, annotated_at, validation_status, annotator_name_placeholder, audio_update, rejection_reason_update, rejection_mode_reset, btn_reject_update, position_info
|
533 |
+
return "", "", "", "", "", "", "", gr.update(value=None, autoplay=False), gr.update(visible=False, value=""), False, gr.update(value="β Reject"), ""
|
534 |
|
535 |
current_item = items[idx]
|
536 |
|
|
|
563 |
# Check if this is a deleted annotation
|
564 |
is_deleted = current_item.get("is_deleted", False)
|
565 |
|
|
|
566 |
if current_item["validation_status"].startswith("Rejected"):
|
567 |
# Extract reason from status like "Rejected (reason)" or just use empty if no parenthesis
|
568 |
start_paren = current_item["validation_status"].find("(")
|
|
|
570 |
if start_paren != -1 and end_paren != -1:
|
571 |
rejection_reason = current_item["validation_status"][start_paren+1:end_paren]
|
572 |
rejection_visible = True
|
573 |
+
|
574 |
+
# Get position info
|
575 |
+
position_info = get_current_item_position_info(items, idx, session)
|
|
|
|
|
|
|
|
|
|
|
576 |
|
577 |
return (
|
578 |
str(current_item["tts_id"]),
|
|
|
585 |
gr.update(value=None, autoplay=False),
|
586 |
gr.update(visible=rejection_visible, value=rejection_reason),
|
587 |
False, # Reset rejection mode
|
588 |
+
gr.update(value="β Reject"), # Reset reject button text
|
589 |
+
position_info # Position info
|
590 |
)
|
591 |
|
592 |
def update_review_info_fn(items, total_count):
|
593 |
+
"""Update the review info banner with current loaded items count and position info"""
|
594 |
if items:
|
595 |
+
current_item_id = items[0]["annotation_id"] if items else "N/A"
|
596 |
+
return f"π **Phase 2 Review Mode** - Reviewing assigned annotations. Loaded {len(items)} of {total_count} total items. Starting from ID: {current_item_id}"
|
597 |
else:
|
598 |
return f"π **Phase 2 Review Mode** - No annotations found for review."
|
599 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
600 |
def navigate_and_load_fn(items, current_idx, direction, session):
|
601 |
"""Combined navigation and loading function"""
|
602 |
if not items:
|
|
|
758 |
).count()
|
759 |
|
760 |
# Load next batch starting from where we left off
|
761 |
+
if items:
|
762 |
+
# Get the last loaded annotation ID to continue from there
|
763 |
+
last_loaded_id = items[-1]["annotation_id"]
|
764 |
+
|
765 |
+
# Load next batch starting after the last loaded item
|
766 |
+
query = db.query(
|
767 |
+
Annotation,
|
768 |
+
TTSData.filename,
|
769 |
+
TTSData.sentence
|
770 |
+
).join(
|
771 |
+
TTSData, Annotation.tts_data_id == TTSData.id
|
772 |
+
).filter(
|
773 |
+
Annotation.annotator_id == target_annotator_obj.id,
|
774 |
+
Annotation.id > last_loaded_id # Continue from where we left off
|
775 |
+
).order_by(Annotation.id).limit(current_batch_size)
|
776 |
+
else:
|
777 |
+
# No items loaded yet, start from the beginning
|
778 |
+
query = db.query(
|
779 |
+
Annotation,
|
780 |
+
TTSData.filename,
|
781 |
+
TTSData.sentence
|
782 |
+
).join(
|
783 |
+
TTSData, Annotation.tts_data_id == TTSData.id
|
784 |
+
).filter(
|
785 |
+
Annotation.annotator_id == target_annotator_obj.id
|
786 |
+
).order_by(Annotation.id).limit(current_batch_size)
|
787 |
|
788 |
results = query.all()
|
789 |
|
|
|
809 |
# Combine with existing items
|
810 |
all_items = items + new_items
|
811 |
log.info(f"Loaded {len(new_items)} more items, total now: {len(all_items)}")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
812 |
return all_items, total_count
|
813 |
|
814 |
+
def _jump_to_next_unreviewed_fn(items, session):
|
815 |
+
"""Helper function to jump to the next unreviewed item in the review queue."""
|
816 |
+
user_id = session.get("user_id")
|
817 |
+
username = session.get("username")
|
818 |
+
if not user_id or not username:
|
819 |
+
gr.Error("User not logged in")
|
820 |
+
return items, 0, ""
|
821 |
+
|
822 |
+
# Find target annotator
|
823 |
+
target_annotator = None
|
824 |
+
for annotator_name, reviewer_name in conf.REVIEW_MAPPING.items():
|
825 |
+
if reviewer_name == username:
|
826 |
+
target_annotator = annotator_name
|
827 |
+
break
|
828 |
+
|
829 |
+
if not target_annotator:
|
830 |
+
gr.Error("User is not assigned as a reviewer")
|
831 |
+
return items, 0, ""
|
832 |
+
|
833 |
+
with get_db() as db:
|
834 |
+
try:
|
835 |
+
# Get target annotator's ID
|
836 |
+
target_annotator_obj = db.query(Annotator).filter_by(name=target_annotator).first()
|
837 |
+
if not target_annotator_obj:
|
838 |
+
return items, 0, f"β οΈ **Error:** Annotator '{target_annotator}' not found"
|
839 |
+
|
840 |
+
# Find the first unreviewed annotation for this reviewer
|
841 |
+
first_unreviewed_query = db.query(Annotation.id).outerjoin(
|
842 |
+
Validation,
|
843 |
+
(Annotation.id == Validation.annotation_id) &
|
844 |
+
(Validation.validator_id == user_id)
|
845 |
+
).filter(
|
846 |
+
Annotation.annotator_id == target_annotator_obj.id,
|
847 |
+
Validation.id.is_(None) # No validation record exists for this reviewer
|
848 |
+
).order_by(Annotation.id).limit(1)
|
849 |
+
|
850 |
+
first_unreviewed_result = first_unreviewed_query.first()
|
851 |
+
|
852 |
+
if first_unreviewed_result:
|
853 |
+
target_id = first_unreviewed_result.id
|
854 |
+
log.info(f"Jumping to next unreviewed item with ID: {target_id}")
|
855 |
+
|
856 |
+
# Find this item in the current loaded items
|
857 |
+
for i, item in enumerate(items):
|
858 |
+
if item["annotation_id"] == target_id:
|
859 |
+
return items, i, f"π― **Jumped to unreviewed item ID: {target_id}**"
|
860 |
+
|
861 |
+
# If not found in current items, we need to load more items
|
862 |
+
# For now, just show a message
|
863 |
+
return items, 0, f"π― **Next unreviewed item ID: {target_id}** - Use navigation to reach it"
|
864 |
+
else:
|
865 |
+
log.info(f"No unreviewed items found for user {user_id}. Review queue is empty.")
|
866 |
+
return items, 0, "π **Phase 2 Review Mode** - All annotations for your assigned annotators have been reviewed! Your review queue is empty."
|
867 |
+
except Exception as e:
|
868 |
+
log.error(f"Error jumping to next unreviewed item: {e}")
|
869 |
+
sentry_sdk.capture_exception(e)
|
870 |
+
return items, 0, f"β οΈ **Error:** Could not find next unreviewed item. Error: {e}"
|
871 |
+
|
872 |
# Output definitions
|
873 |
review_display_outputs = [
|
874 |
self.tts_id, self.filename, self.sentence, self.ann_sentence,
|
|
|
878 |
self.audio,
|
879 |
self.rejection_reason_input, # Added rejection reason input to display outputs
|
880 |
self.rejection_mode_active, # Added rejection mode state
|
881 |
+
self.btn_reject, # Added reject button to display outputs
|
882 |
+
self.position_info # Added position info to display outputs
|
883 |
]
|
884 |
|
885 |
# Trigger data loading when load_trigger changes (after successful login for a reviewer)
|
|
|
947 |
fn=lambda: gr.update(value="β Reject"), # Reset reject button
|
948 |
outputs=[self.btn_reject]
|
949 |
).then(
|
950 |
+
fn=lambda items, idx, session: navigate_and_load_fn(items, idx, "next", session),
|
951 |
inputs=[self.items_state, self.idx_state, session_state],
|
952 |
+
outputs=[self.items_state, self.idx_state, self.review_info]
|
953 |
).then(
|
954 |
fn=show_current_review_item_fn,
|
955 |
inputs=[self.items_state, self.idx_state, session_state],
|
|
|
970 |
inputs=[self.items_state, self.idx_state, session_state, self.rejection_mode_active],
|
971 |
outputs=[self.header.progress_display]
|
972 |
).then(
|
973 |
+
fn=lambda items, idx, session, rejection_mode: navigate_and_load_fn(items, idx, "next", session) if not rejection_mode else (items, idx, ""),
|
974 |
inputs=[self.items_state, self.idx_state, session_state, self.rejection_mode_active],
|
975 |
+
outputs=[self.items_state, self.idx_state, self.review_info]
|
976 |
).then(
|
977 |
fn=lambda items, idx, session, rejection_mode: show_current_review_item_fn(items, idx, session) if not rejection_mode else (
|
978 |
str(items[idx]["tts_id"]) if items and idx < len(items) else "",
|
|
|
998 |
|
999 |
# Skip button (just navigate to next)
|
1000 |
self.btn_skip.click(
|
1001 |
+
fn=lambda items, idx, session: navigate_and_load_fn(items, idx, "next", session),
|
1002 |
inputs=[self.items_state, self.idx_state, session_state],
|
1003 |
+
outputs=[self.items_state, self.idx_state, self.review_info]
|
1004 |
).then(
|
1005 |
fn=show_current_review_item_fn,
|
1006 |
inputs=[self.items_state, self.idx_state, session_state],
|
|
|
1031 |
outputs=self.jump_data_id_input
|
1032 |
)
|
1033 |
|
1034 |
+
# Jump to Unreviewed button
|
1035 |
+
self.btn_jump_to_unreviewed.click(
|
1036 |
+
fn=lambda: update_ui_interactive_state(False),
|
1037 |
+
outputs=self.interactive_ui_elements
|
1038 |
+
).then(
|
1039 |
+
fn=lambda items, session: _jump_to_next_unreviewed_fn(items, session),
|
1040 |
+
inputs=[self.items_state, session_state],
|
1041 |
+
outputs=[self.items_state, self.idx_state, self.review_info]
|
1042 |
+
).then(
|
1043 |
+
fn=show_current_review_item_fn,
|
1044 |
+
inputs=[self.items_state, self.idx_state, session_state],
|
1045 |
+
outputs=review_display_outputs
|
1046 |
+
).then(
|
1047 |
+
# Auto-load audio with autoplay after jumping
|
1048 |
+
fn=download_voice_fn,
|
1049 |
+
inputs=[self.filename],
|
1050 |
+
outputs=[self.audio, self.original_audio_state, self.audio]
|
1051 |
+
).then(
|
1052 |
+
fn=lambda: update_ui_interactive_state(True),
|
1053 |
+
outputs=self.interactive_ui_elements
|
1054 |
+
)
|
1055 |
+
|
1056 |
# Load audio button
|
1057 |
self.btn_load_voice.click(
|
1058 |
fn=lambda: update_ui_interactive_state(False),
|