Spaces:
Running
Running
auto-load enabled and progress bar
Browse files
components/review_dashboard_page.py
CHANGED
@@ -106,6 +106,13 @@ class ReviewDashboardPage:
|
|
106 |
def register_callbacks(self, login_page, session_state: gr.State, root_blocks: gr.Blocks):
|
107 |
self.header.register_callbacks(login_page, self, session_state)
|
108 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
109 |
def update_ui_interactive_state(is_interactive: bool):
|
110 |
updates = []
|
111 |
for elem in self.interactive_ui_elements:
|
@@ -371,12 +378,81 @@ class ReviewDashboardPage:
|
|
371 |
current_item["annotated_at"],
|
372 |
current_item["validation_status"],
|
373 |
"", # Placeholder for annotator_name
|
374 |
-
gr.update(value=None, autoplay=False),
|
375 |
gr.update(visible=rejection_visible, value=rejection_reason),
|
376 |
False, # Reset rejection mode
|
377 |
gr.update(value="β Reject") # Reset reject button text
|
378 |
)
|
379 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
380 |
def navigate_review_fn(items, current_idx, direction):
|
381 |
if not items:
|
382 |
return 0
|
@@ -609,7 +685,7 @@ class ReviewDashboardPage:
|
|
609 |
# Audio loading is now manual only via the Load Audio button
|
610 |
# Removed automatic filename.change callback to prevent slow loading during initialization
|
611 |
|
612 |
-
# Navigation buttons
|
613 |
for btn, direction in [(self.btn_prev, "prev"), (self.btn_next, "next")]:
|
614 |
btn.click(
|
615 |
fn=lambda: update_ui_interactive_state(False),
|
@@ -622,6 +698,11 @@ class ReviewDashboardPage:
|
|
622 |
fn=show_current_review_item_fn,
|
623 |
inputs=[self.items_state, self.idx_state, session_state],
|
624 |
outputs=review_display_outputs
|
|
|
|
|
|
|
|
|
|
|
625 |
).then(
|
626 |
lambda: gr.update(value=None),
|
627 |
outputs=self.jump_data_id_input
|
@@ -630,7 +711,7 @@ class ReviewDashboardPage:
|
|
630 |
outputs=self.interactive_ui_elements
|
631 |
)
|
632 |
|
633 |
-
# Approve/Reject buttons
|
634 |
self.btn_approve.click(
|
635 |
fn=lambda items, idx, session: save_validation_fn(items, idx, session, approved=True, rejection_reason=""), # Pass empty rejection_reason
|
636 |
inputs=[self.items_state, self.idx_state, session_state],
|
@@ -641,6 +722,11 @@ class ReviewDashboardPage:
|
|
641 |
).then(
|
642 |
fn=lambda: gr.update(value="β Reject"), # Reset reject button
|
643 |
outputs=[self.btn_reject]
|
|
|
|
|
|
|
|
|
|
|
644 |
).then(
|
645 |
fn=lambda items, idx: navigate_review_fn(items, idx, "next"),
|
646 |
inputs=[self.items_state, self.idx_state],
|
@@ -649,12 +735,22 @@ class ReviewDashboardPage:
|
|
649 |
fn=show_current_review_item_fn,
|
650 |
inputs=[self.items_state, self.idx_state, session_state],
|
651 |
outputs=review_display_outputs
|
|
|
|
|
|
|
|
|
|
|
652 |
)
|
653 |
|
654 |
self.btn_reject.click(
|
655 |
fn=handle_rejection_fn,
|
656 |
inputs=[self.items_state, self.idx_state, session_state, self.rejection_reason_input, self.rejection_mode_active],
|
657 |
outputs=[self.items_state, self.current_validation_status, self.rejection_reason_input, self.rejection_mode_active, self.btn_reject]
|
|
|
|
|
|
|
|
|
|
|
658 |
).then(
|
659 |
fn=lambda items, idx, rejection_mode: navigate_review_fn(items, idx, "next") if not rejection_mode else idx,
|
660 |
inputs=[self.items_state, self.idx_state, self.rejection_mode_active],
|
@@ -675,9 +771,14 @@ class ReviewDashboardPage:
|
|
675 |
),
|
676 |
inputs=[self.items_state, self.idx_state, session_state, self.rejection_mode_active],
|
677 |
outputs=review_display_outputs
|
|
|
|
|
|
|
|
|
|
|
678 |
)
|
679 |
|
680 |
-
# Skip button (just navigate to next)
|
681 |
self.btn_skip.click(
|
682 |
fn=navigate_review_fn,
|
683 |
inputs=[self.items_state, self.idx_state, gr.State("next")],
|
@@ -686,9 +787,14 @@ class ReviewDashboardPage:
|
|
686 |
fn=show_current_review_item_fn,
|
687 |
inputs=[self.items_state, self.idx_state, session_state],
|
688 |
outputs=review_display_outputs
|
|
|
|
|
|
|
|
|
|
|
689 |
)
|
690 |
|
691 |
-
# Jump button
|
692 |
self.btn_jump.click(
|
693 |
fn=jump_by_data_id_fn,
|
694 |
inputs=[self.items_state, self.jump_data_id_input, self.idx_state],
|
@@ -697,6 +803,11 @@ class ReviewDashboardPage:
|
|
697 |
fn=show_current_review_item_fn,
|
698 |
inputs=[self.items_state, self.idx_state, session_state],
|
699 |
outputs=review_display_outputs
|
|
|
|
|
|
|
|
|
|
|
700 |
).then(
|
701 |
lambda: gr.update(value=None),
|
702 |
outputs=self.jump_data_id_input
|
|
|
106 |
def register_callbacks(self, login_page, session_state: gr.State, root_blocks: gr.Blocks):
|
107 |
self.header.register_callbacks(login_page, self, session_state)
|
108 |
|
109 |
+
# Register progress update callback
|
110 |
+
self.load_trigger.change(
|
111 |
+
fn=get_review_progress_fn,
|
112 |
+
inputs=[session_state],
|
113 |
+
outputs=self.header.progress_display
|
114 |
+
)
|
115 |
+
|
116 |
def update_ui_interactive_state(is_interactive: bool):
|
117 |
updates = []
|
118 |
for elem in self.interactive_ui_elements:
|
|
|
378 |
current_item["annotated_at"],
|
379 |
current_item["validation_status"],
|
380 |
"", # Placeholder for annotator_name
|
381 |
+
gr.update(value=None, autoplay=False), # Reset audio (will be loaded manually)
|
382 |
gr.update(visible=rejection_visible, value=rejection_reason),
|
383 |
False, # Reset rejection mode
|
384 |
gr.update(value="β Reject") # Reset reject button text
|
385 |
)
|
386 |
|
387 |
+
def get_review_progress_fn(session):
|
388 |
+
"""Get progress for reviewer showing how many items they've reviewed"""
|
389 |
+
user_id = session.get("user_id")
|
390 |
+
username = session.get("username")
|
391 |
+
|
392 |
+
if not user_id or not username:
|
393 |
+
return "Review Progress: N/A"
|
394 |
+
|
395 |
+
# Check if user is a reviewer
|
396 |
+
if username not in conf.REVIEW_MAPPING.values():
|
397 |
+
return "Review Progress: N/A (Not a reviewer)"
|
398 |
+
|
399 |
+
# Find which annotator this user should review
|
400 |
+
target_annotator = None
|
401 |
+
for annotator_name, reviewer_name in conf.REVIEW_MAPPING.items():
|
402 |
+
if reviewer_name == username:
|
403 |
+
target_annotator = annotator_name
|
404 |
+
break
|
405 |
+
|
406 |
+
if not target_annotator:
|
407 |
+
return "Review Progress: N/A (No assignment)"
|
408 |
+
|
409 |
+
with get_db() as db:
|
410 |
+
try:
|
411 |
+
# Get target annotator's ID
|
412 |
+
target_annotator_obj = db.query(Annotator).filter_by(name=target_annotator).first()
|
413 |
+
if not target_annotator_obj:
|
414 |
+
return "Review Progress: N/A (Annotator not found)"
|
415 |
+
|
416 |
+
# Count total annotations by target annotator
|
417 |
+
total_annotations = db.query(Annotation).filter(
|
418 |
+
Annotation.annotator_id == target_annotator_obj.id
|
419 |
+
).count()
|
420 |
+
|
421 |
+
# Count reviewed annotations (both approved and rejected)
|
422 |
+
reviewed_count = db.query(Validation).filter(
|
423 |
+
Validation.validator_id == user_id
|
424 |
+
).join(
|
425 |
+
Annotation, Validation.annotation_id == Annotation.id
|
426 |
+
).filter(
|
427 |
+
Annotation.annotator_id == target_annotator_obj.id
|
428 |
+
).count()
|
429 |
+
|
430 |
+
if total_annotations > 0:
|
431 |
+
percent = (reviewed_count / total_annotations) * 100
|
432 |
+
bar_length = 20 # Length of the progress bar
|
433 |
+
filled_length = int(bar_length * reviewed_count // total_annotations)
|
434 |
+
bar = 'β' * filled_length + 'β' * (bar_length - filled_length)
|
435 |
+
return f"Review Progress: {bar} {reviewed_count}/{total_annotations} ({percent:.1f}%)"
|
436 |
+
else:
|
437 |
+
return "Review Progress: No items to review"
|
438 |
+
|
439 |
+
except Exception as e:
|
440 |
+
log.error(f"Error calculating review progress: {e}")
|
441 |
+
return "Review Progress: Error calculating"
|
442 |
+
|
443 |
+
def auto_load_audio_on_navigate_fn(filename_to_load):
|
444 |
+
"""Auto-load audio when navigating between items for smooth UX"""
|
445 |
+
if not filename_to_load:
|
446 |
+
return None, None, gr.update(value=None, autoplay=False)
|
447 |
+
try:
|
448 |
+
log.info(f"Auto-loading audio for navigation: {filename_to_load}")
|
449 |
+
sr, wav = LOADER.load_audio(filename_to_load)
|
450 |
+
log.info(f"Auto-loaded audio: {filename_to_load} (SR: {sr}, Length: {len(wav)} samples)")
|
451 |
+
return (sr, wav), (sr, wav.copy()), gr.update(value=(sr, wav), autoplay=True)
|
452 |
+
except Exception as e:
|
453 |
+
log.error(f"Auto audio load failed for {filename_to_load}: {e}")
|
454 |
+
return None, None, gr.update(value=None, autoplay=False)
|
455 |
+
|
456 |
def navigate_review_fn(items, current_idx, direction):
|
457 |
if not items:
|
458 |
return 0
|
|
|
685 |
# Audio loading is now manual only via the Load Audio button
|
686 |
# Removed automatic filename.change callback to prevent slow loading during initialization
|
687 |
|
688 |
+
# Navigation buttons with auto-audio loading
|
689 |
for btn, direction in [(self.btn_prev, "prev"), (self.btn_next, "next")]:
|
690 |
btn.click(
|
691 |
fn=lambda: update_ui_interactive_state(False),
|
|
|
698 |
fn=show_current_review_item_fn,
|
699 |
inputs=[self.items_state, self.idx_state, session_state],
|
700 |
outputs=review_display_outputs
|
701 |
+
).then(
|
702 |
+
# Auto-load audio on navigation for smooth UX
|
703 |
+
fn=auto_load_audio_on_navigate_fn,
|
704 |
+
inputs=[self.filename],
|
705 |
+
outputs=[self.audio, self.original_audio_state, self.audio]
|
706 |
).then(
|
707 |
lambda: gr.update(value=None),
|
708 |
outputs=self.jump_data_id_input
|
|
|
711 |
outputs=self.interactive_ui_elements
|
712 |
)
|
713 |
|
714 |
+
# Approve/Reject buttons with auto-audio loading and progress updates
|
715 |
self.btn_approve.click(
|
716 |
fn=lambda items, idx, session: save_validation_fn(items, idx, session, approved=True, rejection_reason=""), # Pass empty rejection_reason
|
717 |
inputs=[self.items_state, self.idx_state, session_state],
|
|
|
722 |
).then(
|
723 |
fn=lambda: gr.update(value="β Reject"), # Reset reject button
|
724 |
outputs=[self.btn_reject]
|
725 |
+
).then(
|
726 |
+
# Update progress after approval
|
727 |
+
fn=get_review_progress_fn,
|
728 |
+
inputs=[session_state],
|
729 |
+
outputs=self.header.progress_display
|
730 |
).then(
|
731 |
fn=lambda items, idx: navigate_review_fn(items, idx, "next"),
|
732 |
inputs=[self.items_state, self.idx_state],
|
|
|
735 |
fn=show_current_review_item_fn,
|
736 |
inputs=[self.items_state, self.idx_state, session_state],
|
737 |
outputs=review_display_outputs
|
738 |
+
).then(
|
739 |
+
# Auto-load audio for next item
|
740 |
+
fn=auto_load_audio_on_navigate_fn,
|
741 |
+
inputs=[self.filename],
|
742 |
+
outputs=[self.audio, self.original_audio_state, self.audio]
|
743 |
)
|
744 |
|
745 |
self.btn_reject.click(
|
746 |
fn=handle_rejection_fn,
|
747 |
inputs=[self.items_state, self.idx_state, session_state, self.rejection_reason_input, self.rejection_mode_active],
|
748 |
outputs=[self.items_state, self.current_validation_status, self.rejection_reason_input, self.rejection_mode_active, self.btn_reject]
|
749 |
+
).then(
|
750 |
+
# Update progress after rejection (only if completed)
|
751 |
+
fn=lambda session, rejection_mode: get_review_progress_fn(session) if not rejection_mode else "",
|
752 |
+
inputs=[session_state, self.rejection_mode_active],
|
753 |
+
outputs=self.header.progress_display
|
754 |
).then(
|
755 |
fn=lambda items, idx, rejection_mode: navigate_review_fn(items, idx, "next") if not rejection_mode else idx,
|
756 |
inputs=[self.items_state, self.idx_state, self.rejection_mode_active],
|
|
|
771 |
),
|
772 |
inputs=[self.items_state, self.idx_state, session_state, self.rejection_mode_active],
|
773 |
outputs=review_display_outputs
|
774 |
+
).then(
|
775 |
+
# Auto-load audio for next item (only if completed rejection)
|
776 |
+
fn=lambda filename, rejection_mode: auto_load_audio_on_navigate_fn(filename) if not rejection_mode else (None, None, gr.update()),
|
777 |
+
inputs=[self.filename, self.rejection_mode_active],
|
778 |
+
outputs=[self.audio, self.original_audio_state, self.audio]
|
779 |
)
|
780 |
|
781 |
+
# Skip button (just navigate to next) with auto-audio loading
|
782 |
self.btn_skip.click(
|
783 |
fn=navigate_review_fn,
|
784 |
inputs=[self.items_state, self.idx_state, gr.State("next")],
|
|
|
787 |
fn=show_current_review_item_fn,
|
788 |
inputs=[self.items_state, self.idx_state, session_state],
|
789 |
outputs=review_display_outputs
|
790 |
+
).then(
|
791 |
+
# Auto-load audio for next item
|
792 |
+
fn=auto_load_audio_on_navigate_fn,
|
793 |
+
inputs=[self.filename],
|
794 |
+
outputs=[self.audio, self.original_audio_state, self.audio]
|
795 |
)
|
796 |
|
797 |
+
# Jump button with auto-audio loading
|
798 |
self.btn_jump.click(
|
799 |
fn=jump_by_data_id_fn,
|
800 |
inputs=[self.items_state, self.jump_data_id_input, self.idx_state],
|
|
|
803 |
fn=show_current_review_item_fn,
|
804 |
inputs=[self.items_state, self.idx_state, session_state],
|
805 |
outputs=review_display_outputs
|
806 |
+
).then(
|
807 |
+
# Auto-load audio for jumped item
|
808 |
+
fn=auto_load_audio_on_navigate_fn,
|
809 |
+
inputs=[self.filename],
|
810 |
+
outputs=[self.audio, self.original_audio_state, self.audio]
|
811 |
).then(
|
812 |
lambda: gr.update(value=None),
|
813 |
outputs=self.jump_data_id_input
|