Spaces:
Running
Running
Resume Feature Implementation
Browse files- components/review_dashboard_page.py +70 -400
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,10 +26,6 @@ class ReviewDashboardPage:
|
|
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,7 +68,6 @@ class ReviewDashboardPage:
|
|
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,8 +100,7 @@ class ReviewDashboardPage:
|
|
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):
|
@@ -224,19 +218,6 @@ class ReviewDashboardPage:
|
|
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,7 +249,7 @@ class ReviewDashboardPage:
|
|
268 |
# Estimate remaining items
|
269 |
remaining = total_count - reviewed_count
|
270 |
|
271 |
-
# Create the beautiful progress display
|
272 |
progress_html = f"""
|
273 |
<div class="progress-container">
|
274 |
<div class="progress-header">
|
@@ -286,9 +267,6 @@ class ReviewDashboardPage:
|
|
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,82 +278,18 @@ class ReviewDashboardPage:
|
|
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 count of reviewed items for this reviewer
|
336 |
-
reviewed_count = db.query(Validation).join(
|
337 |
-
Annotation, Validation.annotation_id == Annotation.id
|
338 |
-
).filter(
|
339 |
-
Validation.validator_id == user_id,
|
340 |
-
Annotation.annotator_id == target_annotator_obj.id
|
341 |
-
).count()
|
342 |
-
|
343 |
-
# Get current item's position in the overall sequence
|
344 |
-
current_item_id = items[current_idx]["annotation_id"]
|
345 |
-
|
346 |
-
# Calculate position based on the actual review sequence
|
347 |
-
# First, get all annotations for this target annotator ordered by ID
|
348 |
-
all_annotations = db.query(Annotation.id).filter(
|
349 |
-
Annotation.annotator_id == target_annotator_obj.id
|
350 |
-
).order_by(Annotation.id).all()
|
351 |
-
|
352 |
-
# Create a list of annotation IDs
|
353 |
-
all_annotation_ids = [ann.id for ann in all_annotations]
|
354 |
-
|
355 |
-
# Find the position of current item in the overall sequence
|
356 |
-
try:
|
357 |
-
current_position = all_annotation_ids.index(current_item_id) + 1
|
358 |
-
except ValueError:
|
359 |
-
current_position = 0
|
360 |
-
|
361 |
-
return f"π **Position:** {current_position} of {total_count} items"
|
362 |
-
|
363 |
-
except Exception as e:
|
364 |
-
log.error(f"Error getting current item position: {e}")
|
365 |
-
return ""
|
366 |
-
|
367 |
def load_review_items_fn(session):
|
368 |
user_id = session.get("user_id")
|
369 |
username = session.get("username")
|
370 |
|
371 |
if not user_id or not username:
|
372 |
log.warning("load_review_items_fn: user not found in session")
|
373 |
-
return [], 0, "", "", "", "", "", "", "", "", gr.update(value=None, autoplay=False), gr.update(visible=False, value=""), False, gr.update(value="β Reject")
|
374 |
|
375 |
# Check if user is in Phase 2 (should be a reviewer)
|
376 |
if username not in conf.REVIEW_MAPPING.values():
|
377 |
log.warning(f"User {username} is not assigned as a reviewer")
|
378 |
-
return [], 0, "", "", "", "", "", "", "", "", gr.update(value=None, autoplay=False), gr.update(visible=False, value=""), False, gr.update(value="β Reject")
|
379 |
|
380 |
# Find which annotator this user should review
|
381 |
target_annotator = None
|
@@ -386,135 +300,82 @@ class ReviewDashboardPage:
|
|
386 |
|
387 |
if not target_annotator:
|
388 |
log.warning(f"No target annotator found for reviewer {username}")
|
389 |
-
return [], 0, "", "", "", "", "", "", "", "", gr.update(value=None, autoplay=False), gr.update(visible=False, value=""), False, gr.update(value="β Reject")
|
390 |
|
391 |
-
# Load annotations from target annotator
|
392 |
with get_db() as db:
|
393 |
# Get target annotator's ID
|
394 |
target_annotator_obj = db.query(Annotator).filter_by(name=target_annotator).first()
|
395 |
if not target_annotator_obj:
|
396 |
log.error(f"Target annotator {target_annotator} not found in database")
|
397 |
-
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")
|
398 |
|
399 |
log.info(f"Found target annotator with ID: {target_annotator_obj.id}")
|
400 |
|
401 |
-
#
|
402 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
403 |
Annotation.annotator_id == target_annotator_obj.id
|
404 |
-
).
|
405 |
|
406 |
-
|
407 |
-
|
408 |
-
first_unreviewed_query = db.query(
|
409 |
-
Annotation.id,
|
410 |
-
Annotation.tts_data_id,
|
411 |
-
Annotation.annotated_sentence,
|
412 |
-
Annotation.annotated_at
|
413 |
-
).outerjoin(
|
414 |
-
Validation,
|
415 |
-
(Annotation.id == Validation.annotation_id) &
|
416 |
-
(Validation.validator_id == user_id)
|
417 |
-
).filter(
|
418 |
-
Annotation.annotator_id == target_annotator_obj.id,
|
419 |
-
Validation.id.is_(None) # No validation record exists for this reviewer
|
420 |
-
).order_by(Annotation.id).limit(1)
|
421 |
|
422 |
-
|
423 |
-
|
424 |
-
if first_unreviewed_result:
|
425 |
-
# Found unreviewed item - start from there
|
426 |
-
first_unreviewed_id = first_unreviewed_result.id
|
427 |
-
log.info(f"Found first unreviewed item with ID: {first_unreviewed_id}")
|
428 |
-
|
429 |
-
# Load items starting from the unreviewed item
|
430 |
-
# Load a batch around the unreviewed item for context
|
431 |
-
BATCH_SIZE = 10
|
432 |
-
offset = max(0, first_unreviewed_id - 5) # Start 5 items before the unreviewed item
|
433 |
-
|
434 |
-
# Query to get items around the unreviewed item
|
435 |
-
items_query = db.query(
|
436 |
-
Annotation,
|
437 |
-
TTSData.filename,
|
438 |
-
TTSData.sentence
|
439 |
-
).join(
|
440 |
-
TTSData, Annotation.tts_data_id == TTSData.id
|
441 |
-
).filter(
|
442 |
-
Annotation.annotator_id == target_annotator_obj.id,
|
443 |
-
Annotation.id >= offset
|
444 |
-
).order_by(Annotation.id).limit(BATCH_SIZE)
|
445 |
-
|
446 |
-
initial_results = items_query.all()
|
447 |
-
|
448 |
-
# Find the index of the first unreviewed item in our loaded batch
|
449 |
-
initial_idx = 0
|
450 |
-
for i, (annotation, filename, sentence) in enumerate(initial_results):
|
451 |
-
if annotation.id == first_unreviewed_id:
|
452 |
-
initial_idx = i
|
453 |
-
break
|
454 |
-
|
455 |
-
log.info(f"Resuming at first unreviewed item, index: {initial_idx} (ID: {first_unreviewed_id})")
|
456 |
-
else:
|
457 |
-
# No unreviewed items found - all items have been reviewed
|
458 |
-
log.info(f"All items for {target_annotator} have been reviewed by {username}")
|
459 |
-
|
460 |
-
# Load the last batch of items to show completion
|
461 |
-
BATCH_SIZE = 10
|
462 |
-
items_query = db.query(
|
463 |
-
Annotation,
|
464 |
-
TTSData.filename,
|
465 |
-
TTSData.sentence
|
466 |
-
).join(
|
467 |
-
TTSData, Annotation.tts_data_id == TTSData.id
|
468 |
-
).filter(
|
469 |
-
Annotation.annotator_id == target_annotator_obj.id
|
470 |
-
).order_by(Annotation.id.desc()).limit(BATCH_SIZE)
|
471 |
-
|
472 |
-
# Reverse the results to maintain chronological order
|
473 |
-
initial_results = list(reversed(items_query.all()))
|
474 |
-
initial_idx = len(initial_results) - 1 if initial_results else 0
|
475 |
-
|
476 |
-
log.info(f"All items reviewed, starting at last item, index: {initial_idx}")
|
477 |
-
|
478 |
-
# Set completion message
|
479 |
-
if initial_results:
|
480 |
-
review_info_text = f"π **Phase 2 Review Mode** - All annotations for {target_annotator} have been reviewed! Showing last {len(initial_results)} items for reference."
|
481 |
-
else:
|
482 |
-
review_info_text = f"π **Phase 2 Review Mode** - All annotations for {target_annotator} have been reviewed! No items to display."
|
483 |
-
|
484 |
-
return (
|
485 |
-
[],
|
486 |
-
0,
|
487 |
-
review_info_text,
|
488 |
-
"", "", "", "", "", "", "", gr.update(value=None, autoplay=False), gr.update(visible=False, value=""), False, gr.update(value="β Reject"), ""
|
489 |
-
)
|
490 |
|
491 |
-
# Process items
|
492 |
items = []
|
493 |
-
for annotation, filename, sentence in
|
494 |
-
# Check if annotation is deleted
|
495 |
is_deleted = not annotation.annotated_sentence or annotation.annotated_sentence.strip() == ""
|
496 |
annotated_sentence_display = "[DELETED ANNOTATION]" if is_deleted else annotation.annotated_sentence
|
497 |
|
|
|
|
|
|
|
498 |
items.append({
|
499 |
"annotation_id": annotation.id,
|
500 |
"tts_id": annotation.tts_data_id,
|
501 |
"filename": filename,
|
502 |
"sentence": sentence,
|
503 |
"annotated_sentence": annotated_sentence_display,
|
504 |
-
"is_deleted":
|
505 |
"annotated_at": annotation.annotated_at.isoformat() if annotation.annotated_at else "",
|
506 |
-
"validation_status":
|
507 |
-
"validation_loaded":
|
508 |
})
|
509 |
|
510 |
-
|
511 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
512 |
# Set initial display
|
513 |
if items:
|
514 |
initial_item = items[initial_idx]
|
515 |
-
review_info_text = f"π **Phase 2 Review Mode** - Reviewing assigned annotations. Loaded {len(items)}
|
516 |
-
|
517 |
# Ensure correct order of return values for 14 outputs
|
|
|
518 |
rejection_reason_val = ""
|
519 |
rejection_visible_val = False
|
520 |
if initial_item["validation_status"].startswith("Rejected"):
|
@@ -538,42 +399,27 @@ class ReviewDashboardPage:
|
|
538 |
gr.update(value=None, autoplay=False), # audio_update
|
539 |
gr.update(visible=rejection_visible_val, value=rejection_reason_val), # rejection_reason_input update
|
540 |
False, # Reset rejection mode
|
541 |
-
gr.update(value="β Reject")
|
542 |
-
get_current_item_position_info(items, initial_idx, session) # Position info
|
543 |
)
|
544 |
else:
|
545 |
-
# Ensure correct order and number of return values for empty items (
|
546 |
-
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")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
547 |
|
548 |
def show_current_review_item_fn(items, idx, session):
|
549 |
if not items or idx >= len(items) or idx < 0:
|
550 |
-
# tts_id, filename, sentence, ann_sentence, annotated_at, validation_status, annotator_name_placeholder, audio_update, rejection_reason_update, rejection_mode_reset, btn_reject_update
|
551 |
-
return "", "", "", "", "", "", "", gr.update(value=None, autoplay=False), gr.update(visible=False, value=""), False, gr.update(value="β Reject")
|
552 |
|
553 |
current_item = items[idx]
|
554 |
|
555 |
-
#
|
556 |
-
if not current_item.get("validation_loaded", False):
|
557 |
-
user_id = session.get("user_id")
|
558 |
-
if user_id:
|
559 |
-
with get_db() as db:
|
560 |
-
try:
|
561 |
-
# Get the full annotation object for validation processing
|
562 |
-
annotation_obj = db.query(Annotation).filter_by(id=current_item["annotation_id"]).first()
|
563 |
-
if annotation_obj:
|
564 |
-
validation_status, is_deleted = get_validation_status_for_item(db, current_item["annotation_id"], user_id, annotation_obj)
|
565 |
-
current_item["validation_status"] = validation_status
|
566 |
-
current_item["is_deleted"] = is_deleted
|
567 |
-
current_item["validation_loaded"] = True
|
568 |
-
|
569 |
-
# Update displayed annotation if deleted
|
570 |
-
if is_deleted:
|
571 |
-
current_item["annotated_sentence"] = "[DELETED ANNOTATION]"
|
572 |
-
|
573 |
-
log.info(f"Loaded validation status for item {idx}: {validation_status}")
|
574 |
-
except Exception as e:
|
575 |
-
log.error(f"Error loading validation status for item {idx}: {e}")
|
576 |
-
current_item["validation_status"] = "Error loading status"
|
577 |
|
578 |
rejection_reason = ""
|
579 |
rejection_visible = False
|
@@ -589,9 +435,6 @@ class ReviewDashboardPage:
|
|
589 |
rejection_reason = current_item["validation_status"][start_paren+1:end_paren]
|
590 |
rejection_visible = True
|
591 |
|
592 |
-
# Get position info
|
593 |
-
position_info = get_current_item_position_info(items, idx, session)
|
594 |
-
|
595 |
return (
|
596 |
str(current_item["tts_id"]),
|
597 |
current_item["filename"],
|
@@ -603,37 +446,25 @@ class ReviewDashboardPage:
|
|
603 |
gr.update(value=None, autoplay=False),
|
604 |
gr.update(visible=rejection_visible, value=rejection_reason),
|
605 |
False, # Reset rejection mode
|
606 |
-
gr.update(value="β Reject")
|
607 |
-
position_info # Position info
|
608 |
)
|
609 |
|
610 |
def update_review_info_fn(items, total_count):
|
611 |
-
"""Update the review info banner with current loaded items count
|
612 |
if items:
|
613 |
-
|
614 |
-
return f"π **Phase 2 Review Mode** - Reviewing assigned annotations. Loaded {len(items)} of {total_count} total items. Starting from ID: {current_item_id}"
|
615 |
else:
|
616 |
return f"π **Phase 2 Review Mode** - No annotations found for review."
|
617 |
|
618 |
def navigate_and_load_fn(items, current_idx, direction, session):
|
619 |
-
"""
|
620 |
if not items:
|
621 |
return items, 0, ""
|
622 |
|
623 |
# Navigate
|
624 |
if direction == "next":
|
625 |
new_idx = min(current_idx + 1, len(items) - 1)
|
626 |
-
|
627 |
-
should_load_more = (new_idx == len(items) - 1 and len(items) % 5 == 0)
|
628 |
-
if should_load_more:
|
629 |
-
log.info(f"User reached end of loaded items ({new_idx}/{len(items)}), will load more items")
|
630 |
-
# Load more items
|
631 |
-
updated_items, total_count = load_more_items_fn(items, session, current_batch_size=10)
|
632 |
-
# Update review info with new count
|
633 |
-
review_info = update_review_info_fn(updated_items, total_count)
|
634 |
-
return updated_items, new_idx, review_info
|
635 |
-
else:
|
636 |
-
return items, new_idx, "" # No review info update needed
|
637 |
else: # prev
|
638 |
new_idx = max(current_idx - 1, 0)
|
639 |
return items, new_idx, "" # No review info update needed
|
@@ -747,145 +578,7 @@ class ReviewDashboardPage:
|
|
747 |
# gr.Warning(f"Invalid Data ID format: {target_data_id}")
|
748 |
return current_idx
|
749 |
|
750 |
-
def load_more_items_fn(items, session, current_batch_size=10):
|
751 |
-
"""Load more items when user needs them (pagination support)"""
|
752 |
-
user_id = session.get("user_id")
|
753 |
-
username = session.get("username")
|
754 |
-
|
755 |
-
if not user_id or not username:
|
756 |
-
return items, 0 # Return existing items if no user session
|
757 |
-
|
758 |
-
# Find target annotator
|
759 |
-
target_annotator = None
|
760 |
-
for annotator_name, reviewer_name in conf.REVIEW_MAPPING.items():
|
761 |
-
if reviewer_name == username:
|
762 |
-
target_annotator = annotator_name
|
763 |
-
break
|
764 |
-
|
765 |
-
if not target_annotator:
|
766 |
-
return items, 0
|
767 |
-
|
768 |
-
with get_db() as db:
|
769 |
-
target_annotator_obj = db.query(Annotator).filter_by(name=target_annotator).first()
|
770 |
-
if not target_annotator_obj:
|
771 |
-
return items, 0
|
772 |
-
|
773 |
-
# Get total count for updated review info
|
774 |
-
total_count = db.query(Annotation).filter(
|
775 |
-
Annotation.annotator_id == target_annotator_obj.id
|
776 |
-
).count()
|
777 |
-
|
778 |
-
# Load next batch starting from where we left off
|
779 |
-
if items:
|
780 |
-
# Get the last loaded annotation ID to continue from there
|
781 |
-
last_loaded_id = items[-1]["annotation_id"]
|
782 |
-
|
783 |
-
# Load next batch starting after the last loaded item
|
784 |
-
query = db.query(
|
785 |
-
Annotation,
|
786 |
-
TTSData.filename,
|
787 |
-
TTSData.sentence
|
788 |
-
).join(
|
789 |
-
TTSData, Annotation.tts_data_id == TTSData.id
|
790 |
-
).filter(
|
791 |
-
Annotation.annotator_id == target_annotator_obj.id,
|
792 |
-
Annotation.id > last_loaded_id # Continue from where we left off
|
793 |
-
).order_by(Annotation.id).limit(current_batch_size)
|
794 |
-
else:
|
795 |
-
# No items loaded yet, start from the beginning
|
796 |
-
query = db.query(
|
797 |
-
Annotation,
|
798 |
-
TTSData.filename,
|
799 |
-
TTSData.sentence
|
800 |
-
).join(
|
801 |
-
TTSData, Annotation.tts_data_id == TTSData.id
|
802 |
-
).filter(
|
803 |
-
Annotation.annotator_id == target_annotator_obj.id
|
804 |
-
).order_by(Annotation.id).limit(current_batch_size)
|
805 |
-
|
806 |
-
results = query.all()
|
807 |
-
|
808 |
-
# Process new items with minimal data - validation status loaded on-demand
|
809 |
-
new_items = []
|
810 |
-
for annotation, filename, sentence in results:
|
811 |
-
# Check if annotation is deleted (minimal processing)
|
812 |
-
is_deleted = not annotation.annotated_sentence or annotation.annotated_sentence.strip() == ""
|
813 |
-
annotated_sentence_display = "[DELETED ANNOTATION]" if is_deleted else annotation.annotated_sentence
|
814 |
-
|
815 |
-
new_items.append({
|
816 |
-
"annotation_id": annotation.id,
|
817 |
-
"tts_id": annotation.tts_data_id,
|
818 |
-
"filename": filename,
|
819 |
-
"sentence": sentence,
|
820 |
-
"annotated_sentence": annotated_sentence_display,
|
821 |
-
"is_deleted": is_deleted,
|
822 |
-
"annotated_at": annotation.annotated_at.isoformat() if annotation.annotated_at else "",
|
823 |
-
"validation_status": "Loading...", # Will be loaded on-demand
|
824 |
-
"validation_loaded": False # Track if validation status has been loaded
|
825 |
-
})
|
826 |
-
|
827 |
-
# Combine with existing items
|
828 |
-
all_items = items + new_items
|
829 |
-
log.info(f"Loaded {len(new_items)} more items, total now: {len(all_items)}")
|
830 |
-
return all_items, total_count
|
831 |
-
|
832 |
-
def _jump_to_next_unreviewed_fn(items, session):
|
833 |
-
"""Helper function to jump to the next unreviewed item in the review queue."""
|
834 |
-
user_id = session.get("user_id")
|
835 |
-
username = session.get("username")
|
836 |
-
if not user_id or not username:
|
837 |
-
gr.Error("User not logged in")
|
838 |
-
return items, 0, ""
|
839 |
-
|
840 |
-
# Find target annotator
|
841 |
-
target_annotator = None
|
842 |
-
for annotator_name, reviewer_name in conf.REVIEW_MAPPING.items():
|
843 |
-
if reviewer_name == username:
|
844 |
-
target_annotator = annotator_name
|
845 |
-
break
|
846 |
-
|
847 |
-
if not target_annotator:
|
848 |
-
gr.Error("User is not assigned as a reviewer")
|
849 |
-
return items, 0, ""
|
850 |
-
|
851 |
-
with get_db() as db:
|
852 |
-
try:
|
853 |
-
# Get target annotator's ID
|
854 |
-
target_annotator_obj = db.query(Annotator).filter_by(name=target_annotator).first()
|
855 |
-
if not target_annotator_obj:
|
856 |
-
return items, 0, f"β οΈ **Error:** Annotator '{target_annotator}' not found"
|
857 |
|
858 |
-
# Find the first unreviewed annotation for this reviewer
|
859 |
-
first_unreviewed_query = db.query(Annotation.id).outerjoin(
|
860 |
-
Validation,
|
861 |
-
(Annotation.id == Validation.annotation_id) &
|
862 |
-
(Validation.validator_id == user_id)
|
863 |
-
).filter(
|
864 |
-
Annotation.annotator_id == target_annotator_obj.id,
|
865 |
-
Validation.id.is_(None) # No validation record exists for this reviewer
|
866 |
-
).order_by(Annotation.id).limit(1)
|
867 |
-
|
868 |
-
first_unreviewed_result = first_unreviewed_query.first()
|
869 |
-
|
870 |
-
if first_unreviewed_result:
|
871 |
-
target_id = first_unreviewed_result.id
|
872 |
-
log.info(f"Jumping to next unreviewed item with ID: {target_id}")
|
873 |
-
|
874 |
-
# Find this item in the current loaded items
|
875 |
-
for i, item in enumerate(items):
|
876 |
-
if item["annotation_id"] == target_id:
|
877 |
-
return items, i, f"π― **Jumped to unreviewed item ID: {target_id}**"
|
878 |
-
|
879 |
-
# If not found in current items, we need to load more items
|
880 |
-
# For now, just show a message
|
881 |
-
return items, 0, f"π― **Next unreviewed item ID: {target_id}** - Use navigation to reach it"
|
882 |
-
else:
|
883 |
-
log.info(f"No unreviewed items found for user {user_id}. Review queue is empty.")
|
884 |
-
return items, 0, "π **Phase 2 Review Mode** - All annotations for your assigned annotators have been reviewed! Your review queue is empty."
|
885 |
-
except Exception as e:
|
886 |
-
log.error(f"Error jumping to next unreviewed item: {e}")
|
887 |
-
sentry_sdk.capture_exception(e)
|
888 |
-
return items, 0, f"β οΈ **Error:** Could not find next unreviewed item. Error: {e}"
|
889 |
|
890 |
# Output definitions
|
891 |
review_display_outputs = [
|
@@ -896,8 +589,7 @@ class ReviewDashboardPage:
|
|
896 |
self.audio,
|
897 |
self.rejection_reason_input, # Added rejection reason input to display outputs
|
898 |
self.rejection_mode_active, # Added rejection mode state
|
899 |
-
self.btn_reject
|
900 |
-
self.position_info # Added position info to display outputs
|
901 |
]
|
902 |
|
903 |
# Trigger data loading when load_trigger changes (after successful login for a reviewer)
|
@@ -1049,28 +741,6 @@ class ReviewDashboardPage:
|
|
1049 |
outputs=self.jump_data_id_input
|
1050 |
)
|
1051 |
|
1052 |
-
# Jump to Unreviewed button
|
1053 |
-
self.btn_jump_to_unreviewed.click(
|
1054 |
-
fn=lambda: update_ui_interactive_state(False),
|
1055 |
-
outputs=self.interactive_ui_elements
|
1056 |
-
).then(
|
1057 |
-
fn=lambda items, session: _jump_to_next_unreviewed_fn(items, session),
|
1058 |
-
inputs=[self.items_state, session_state],
|
1059 |
-
outputs=[self.items_state, self.idx_state, self.review_info]
|
1060 |
-
).then(
|
1061 |
-
fn=show_current_review_item_fn,
|
1062 |
-
inputs=[self.items_state, self.idx_state, session_state],
|
1063 |
-
outputs=review_display_outputs
|
1064 |
-
).then(
|
1065 |
-
# Auto-load audio with autoplay after jumping
|
1066 |
-
fn=download_voice_fn,
|
1067 |
-
inputs=[self.filename],
|
1068 |
-
outputs=[self.audio, self.original_audio_state, self.audio]
|
1069 |
-
).then(
|
1070 |
-
fn=lambda: update_ui_interactive_state(True),
|
1071 |
-
outputs=self.interactive_ui_elements
|
1072 |
-
)
|
1073 |
-
|
1074 |
# Load audio button
|
1075 |
self.btn_load_voice.click(
|
1076 |
fn=lambda: update_ui_interactive_state(False),
|
|
|
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 |
# 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 |
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 |
# 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):
|
|
|
218 |
Validation.validator_id == user_id
|
219 |
).count()
|
220 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
221 |
if total_count > 0:
|
222 |
percentage = (reviewed_count / total_count) * 100
|
223 |
|
|
|
249 |
# Estimate remaining items
|
250 |
remaining = total_count - reviewed_count
|
251 |
|
252 |
+
# Create the beautiful progress display
|
253 |
progress_html = f"""
|
254 |
<div class="progress-container">
|
255 |
<div class="progress-header">
|
|
|
267 |
π <code>{progress_bar}</code>
|
268 |
<span class="remaining-items">({remaining} remaining)</span>
|
269 |
</div>
|
|
|
|
|
|
|
270 |
</div>
|
271 |
"""
|
272 |
|
|
|
278 |
log.error(f"Error calculating review progress for user {user_id}: {e}")
|
279 |
return f"β οΈ **Error calculating progress**"
|
280 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
281 |
def load_review_items_fn(session):
|
282 |
user_id = session.get("user_id")
|
283 |
username = session.get("username")
|
284 |
|
285 |
if not user_id or not username:
|
286 |
log.warning("load_review_items_fn: user not found in session")
|
287 |
+
return [], 0, "", "", "", "", "", "", "", "", gr.update(value=None, autoplay=False), gr.update(visible=False, value=""), False, gr.update(value="β Reject")
|
288 |
|
289 |
# Check if user is in Phase 2 (should be a reviewer)
|
290 |
if username not in conf.REVIEW_MAPPING.values():
|
291 |
log.warning(f"User {username} is not assigned as a reviewer")
|
292 |
+
return [], 0, "", "", "", "", "", "", "", "", gr.update(value=None, autoplay=False), gr.update(visible=False, value=""), False, gr.update(value="β Reject")
|
293 |
|
294 |
# Find which annotator this user should review
|
295 |
target_annotator = None
|
|
|
300 |
|
301 |
if not target_annotator:
|
302 |
log.warning(f"No target annotator found for reviewer {username}")
|
303 |
+
return [], 0, "", "", "", "", "", "", "", "", gr.update(value=None, autoplay=False), gr.update(visible=False, value=""), False, gr.update(value="β Reject")
|
304 |
|
305 |
+
# Load ALL annotations from target annotator to properly implement "continue from where left off"
|
306 |
with get_db() as db:
|
307 |
# Get target annotator's ID
|
308 |
target_annotator_obj = db.query(Annotator).filter_by(name=target_annotator).first()
|
309 |
if not target_annotator_obj:
|
310 |
log.error(f"Target annotator {target_annotator} not found in database")
|
311 |
+
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")
|
312 |
|
313 |
log.info(f"Found target annotator with ID: {target_annotator_obj.id}")
|
314 |
|
315 |
+
# Load ALL annotations to properly implement resume functionality
|
316 |
+
# This is similar to phase 1 dashboard approach
|
317 |
+
all_annotations_query = db.query(
|
318 |
+
Annotation,
|
319 |
+
TTSData.filename,
|
320 |
+
TTSData.sentence
|
321 |
+
).join(
|
322 |
+
TTSData, Annotation.tts_data_id == TTSData.id
|
323 |
+
).filter(
|
324 |
Annotation.annotator_id == target_annotator_obj.id
|
325 |
+
).order_by(Annotation.id)
|
326 |
|
327 |
+
all_results = all_annotations_query.all()
|
328 |
+
total_count = len(all_results)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
329 |
|
330 |
+
log.info(f"Loaded {total_count} annotations for target annotator ID {target_annotator_obj.id}")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
331 |
|
332 |
+
# Process all items and determine validation status immediately
|
333 |
items = []
|
334 |
+
for annotation, filename, sentence in all_results:
|
335 |
+
# Check if annotation is deleted
|
336 |
is_deleted = not annotation.annotated_sentence or annotation.annotated_sentence.strip() == ""
|
337 |
annotated_sentence_display = "[DELETED ANNOTATION]" if is_deleted else annotation.annotated_sentence
|
338 |
|
339 |
+
# Get validation status immediately (not on-demand)
|
340 |
+
validation_status, is_deleted_updated = get_validation_status_for_item(db, annotation.id, user_id, annotation)
|
341 |
+
|
342 |
items.append({
|
343 |
"annotation_id": annotation.id,
|
344 |
"tts_id": annotation.tts_data_id,
|
345 |
"filename": filename,
|
346 |
"sentence": sentence,
|
347 |
"annotated_sentence": annotated_sentence_display,
|
348 |
+
"is_deleted": is_deleted_updated,
|
349 |
"annotated_at": annotation.annotated_at.isoformat() if annotation.annotated_at else "",
|
350 |
+
"validation_status": validation_status,
|
351 |
+
"validation_loaded": True # Already loaded
|
352 |
})
|
353 |
|
354 |
+
# --- Resume Logic: Find first unreviewed item (similar to phase 1 dashboard) ---
|
355 |
+
initial_idx = 0
|
356 |
+
if items:
|
357 |
+
first_unreviewed_idx = -1
|
358 |
+
for i, item_data in enumerate(items):
|
359 |
+
if item_data["validation_status"] == "Not Reviewed":
|
360 |
+
first_unreviewed_idx = i
|
361 |
+
break
|
362 |
+
|
363 |
+
if first_unreviewed_idx != -1:
|
364 |
+
initial_idx = first_unreviewed_idx
|
365 |
+
log.info(f"Resuming at first unreviewed item, index: {initial_idx} (ID: {items[initial_idx]['tts_id']})")
|
366 |
+
else: # All items are reviewed
|
367 |
+
initial_idx = len(items) - 1
|
368 |
+
log.info(f"All items reviewed, starting at last item, index: {initial_idx} (ID: {items[initial_idx]['tts_id']})")
|
369 |
+
else: # No items assigned
|
370 |
+
initial_idx = 0
|
371 |
+
log.info("No items assigned to user, starting at index 0.")
|
372 |
+
|
373 |
# Set initial display
|
374 |
if items:
|
375 |
initial_item = items[initial_idx]
|
376 |
+
review_info_text = f"π **Phase 2 Review Mode** - Reviewing assigned annotations. Loaded {len(items)} items."
|
|
|
377 |
# Ensure correct order of return values for 14 outputs
|
378 |
+
# items, idx, review_info, tts_id, filename, sentence, ann_sentence, annotated_at, validation_status, annotator_placeholder, audio_update, rejection_reason_update, rejection_mode_reset, btn_reject_update
|
379 |
rejection_reason_val = ""
|
380 |
rejection_visible_val = False
|
381 |
if initial_item["validation_status"].startswith("Rejected"):
|
|
|
399 |
gr.update(value=None, autoplay=False), # audio_update
|
400 |
gr.update(visible=rejection_visible_val, value=rejection_reason_val), # rejection_reason_input update
|
401 |
False, # Reset rejection mode
|
402 |
+
gr.update(value="β Reject") # Reset reject button
|
|
|
403 |
)
|
404 |
else:
|
405 |
+
# Ensure correct order and number of return values for empty items (14 outputs)
|
406 |
+
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")
|
407 |
+
|
408 |
+
# except Exception as e:
|
409 |
+
# log.error(f"Error loading review items: {e}")
|
410 |
+
# sentry_sdk.capture_exception(e)
|
411 |
+
# gr.Error(f"Failed to load review data: {e}")
|
412 |
+
# # Ensure correct order and number of return values for error case (14 outputs)
|
413 |
+
# return [], 0, "", "", "", "", "", "", "", "", gr.update(value=None, autoplay=False), gr.update(visible=False, value=""), False, gr.update(value="β Reject")
|
414 |
|
415 |
def show_current_review_item_fn(items, idx, session):
|
416 |
if not items or idx >= len(items) or idx < 0:
|
417 |
+
# tts_id, filename, sentence, ann_sentence, annotated_at, validation_status, annotator_name_placeholder, audio_update, rejection_reason_update, rejection_mode_reset, btn_reject_update
|
418 |
+
return "", "", "", "", "", "", "", gr.update(value=None, autoplay=False), gr.update(visible=False, value=""), False, gr.update(value="β Reject")
|
419 |
|
420 |
current_item = items[idx]
|
421 |
|
422 |
+
# Validation status is already loaded, no need for on-demand loading
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
423 |
|
424 |
rejection_reason = ""
|
425 |
rejection_visible = False
|
|
|
435 |
rejection_reason = current_item["validation_status"][start_paren+1:end_paren]
|
436 |
rejection_visible = True
|
437 |
|
|
|
|
|
|
|
438 |
return (
|
439 |
str(current_item["tts_id"]),
|
440 |
current_item["filename"],
|
|
|
446 |
gr.update(value=None, autoplay=False),
|
447 |
gr.update(visible=rejection_visible, value=rejection_reason),
|
448 |
False, # Reset rejection mode
|
449 |
+
gr.update(value="β Reject") # Reset reject button text
|
|
|
450 |
)
|
451 |
|
452 |
def update_review_info_fn(items, total_count):
|
453 |
+
"""Update the review info banner with current loaded items count"""
|
454 |
if items:
|
455 |
+
return f"π **Phase 2 Review Mode** - Reviewing assigned annotations. Loaded {len(items)} items."
|
|
|
456 |
else:
|
457 |
return f"π **Phase 2 Review Mode** - No annotations found for review."
|
458 |
|
459 |
def navigate_and_load_fn(items, current_idx, direction, session):
|
460 |
+
"""Simple navigation function - all items are already loaded"""
|
461 |
if not items:
|
462 |
return items, 0, ""
|
463 |
|
464 |
# Navigate
|
465 |
if direction == "next":
|
466 |
new_idx = min(current_idx + 1, len(items) - 1)
|
467 |
+
return items, new_idx, "" # No review info update needed
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
468 |
else: # prev
|
469 |
new_idx = max(current_idx - 1, 0)
|
470 |
return items, new_idx, "" # No review info update needed
|
|
|
578 |
# gr.Warning(f"Invalid Data ID format: {target_data_id}")
|
579 |
return current_idx
|
580 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
581 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
582 |
|
583 |
# Output definitions
|
584 |
review_display_outputs = [
|
|
|
589 |
self.audio,
|
590 |
self.rejection_reason_input, # Added rejection reason input to display outputs
|
591 |
self.rejection_mode_active, # Added rejection mode state
|
592 |
+
self.btn_reject # Added reject button to display outputs
|
|
|
593 |
]
|
594 |
|
595 |
# Trigger data loading when load_trigger changes (after successful login for a reviewer)
|
|
|
741 |
outputs=self.jump_data_id_input
|
742 |
)
|
743 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
744 |
# Load audio button
|
745 |
self.btn_load_voice.click(
|
746 |
fn=lambda: update_ui_interactive_state(False),
|