Spaces:
Running
Running
auto-load enabled and progress bar
Browse files- components/review_dashboard_page.py +78 -100
components/review_dashboard_page.py
CHANGED
@@ -106,13 +106,6 @@ 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 |
-
# 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:
|
@@ -183,6 +176,58 @@ class ReviewDashboardPage:
|
|
183 |
|
184 |
return validation_status, is_deleted
|
185 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
186 |
def load_review_items_fn(session):
|
187 |
user_id = session.get("user_id")
|
188 |
username = session.get("username")
|
@@ -378,81 +423,12 @@ class ReviewDashboardPage:
|
|
378 |
current_item["annotated_at"],
|
379 |
current_item["validation_status"],
|
380 |
"", # Placeholder for annotator_name
|
381 |
-
gr.update(value=None, autoplay=False),
|
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
|
@@ -674,6 +650,10 @@ class ReviewDashboardPage:
|
|
674 |
fn=load_review_items_fn,
|
675 |
inputs=[session_state],
|
676 |
outputs=[self.items_state, self.idx_state, self.review_info] + review_display_outputs
|
|
|
|
|
|
|
|
|
677 |
).then(
|
678 |
fn=lambda: (None, gr.update(value=None)), # Clear audio state
|
679 |
outputs=[self.original_audio_state, self.audio]
|
@@ -685,7 +665,7 @@ class ReviewDashboardPage:
|
|
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
|
689 |
for btn, direction in [(self.btn_prev, "prev"), (self.btn_next, "next")]:
|
690 |
btn.click(
|
691 |
fn=lambda: update_ui_interactive_state(False),
|
@@ -699,8 +679,8 @@ class ReviewDashboardPage:
|
|
699 |
inputs=[self.items_state, self.idx_state, session_state],
|
700 |
outputs=review_display_outputs
|
701 |
).then(
|
702 |
-
# Auto-load audio
|
703 |
-
fn=
|
704 |
inputs=[self.filename],
|
705 |
outputs=[self.audio, self.original_audio_state, self.audio]
|
706 |
).then(
|
@@ -711,22 +691,21 @@ class ReviewDashboardPage:
|
|
711 |
outputs=self.interactive_ui_elements
|
712 |
)
|
713 |
|
714 |
-
# Approve/Reject buttons
|
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],
|
718 |
outputs=[self.items_state, self.current_validation_status, self.rejection_reason_input]
|
|
|
|
|
|
|
|
|
719 |
).then(
|
720 |
fn=lambda: False, # Reset rejection mode
|
721 |
outputs=[self.rejection_mode_active]
|
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],
|
@@ -736,8 +715,8 @@ class ReviewDashboardPage:
|
|
736 |
inputs=[self.items_state, self.idx_state, session_state],
|
737 |
outputs=review_display_outputs
|
738 |
).then(
|
739 |
-
# Auto-load audio
|
740 |
-
fn=
|
741 |
inputs=[self.filename],
|
742 |
outputs=[self.audio, self.original_audio_state, self.audio]
|
743 |
)
|
@@ -747,10 +726,9 @@ class ReviewDashboardPage:
|
|
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
|
751 |
-
|
752 |
-
|
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],
|
@@ -772,13 +750,13 @@ class ReviewDashboardPage:
|
|
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
|
776 |
-
fn=lambda filename, rejection_mode:
|
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)
|
782 |
self.btn_skip.click(
|
783 |
fn=navigate_review_fn,
|
784 |
inputs=[self.items_state, self.idx_state, gr.State("next")],
|
@@ -788,13 +766,13 @@ class ReviewDashboardPage:
|
|
788 |
inputs=[self.items_state, self.idx_state, session_state],
|
789 |
outputs=review_display_outputs
|
790 |
).then(
|
791 |
-
# Auto-load audio
|
792 |
-
fn=
|
793 |
inputs=[self.filename],
|
794 |
outputs=[self.audio, self.original_audio_state, self.audio]
|
795 |
)
|
796 |
|
797 |
-
# Jump button
|
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],
|
@@ -804,8 +782,8 @@ class ReviewDashboardPage:
|
|
804 |
inputs=[self.items_state, self.idx_state, session_state],
|
805 |
outputs=review_display_outputs
|
806 |
).then(
|
807 |
-
# Auto-load audio
|
808 |
-
fn=
|
809 |
inputs=[self.filename],
|
810 |
outputs=[self.audio, self.original_audio_state, self.audio]
|
811 |
).then(
|
|
|
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:
|
|
|
176 |
|
177 |
return validation_status, is_deleted
|
178 |
|
179 |
+
def get_review_progress_fn(session):
|
180 |
+
"""Calculate review progress for the current reviewer"""
|
181 |
+
user_id = session.get("user_id")
|
182 |
+
username = session.get("username")
|
183 |
+
|
184 |
+
if not user_id or not username:
|
185 |
+
return "Review Progress: N/A"
|
186 |
+
|
187 |
+
# Check if user is a reviewer
|
188 |
+
if username not in conf.REVIEW_MAPPING.values():
|
189 |
+
return "Review Progress: N/A (Not a reviewer)"
|
190 |
+
|
191 |
+
# Find target annotator
|
192 |
+
target_annotator = None
|
193 |
+
for annotator_name, reviewer_name in conf.REVIEW_MAPPING.items():
|
194 |
+
if reviewer_name == username:
|
195 |
+
target_annotator = annotator_name
|
196 |
+
break
|
197 |
+
|
198 |
+
if not target_annotator:
|
199 |
+
return "Review Progress: N/A (No target annotator)"
|
200 |
+
|
201 |
+
with get_db() as db:
|
202 |
+
try:
|
203 |
+
# Get target annotator's ID
|
204 |
+
target_annotator_obj = db.query(Annotator).filter_by(name=target_annotator).first()
|
205 |
+
if not target_annotator_obj:
|
206 |
+
return f"Review Progress: Error (Annotator '{target_annotator}' not found)"
|
207 |
+
|
208 |
+
# Count total annotations for target annotator
|
209 |
+
total_count = db.query(Annotation).filter(
|
210 |
+
Annotation.annotator_id == target_annotator_obj.id
|
211 |
+
).count()
|
212 |
+
|
213 |
+
# Count reviewed annotations (have validation from this reviewer)
|
214 |
+
reviewed_count = db.query(Annotation).join(
|
215 |
+
Validation, Annotation.id == Validation.annotation_id
|
216 |
+
).filter(
|
217 |
+
Annotation.annotator_id == target_annotator_obj.id,
|
218 |
+
Validation.validator_id == user_id
|
219 |
+
).count()
|
220 |
+
|
221 |
+
if total_count > 0:
|
222 |
+
percentage = (reviewed_count / total_count) * 100
|
223 |
+
return f"**Review Progress:** {reviewed_count}/{total_count} ({percentage:.1f}%) - Reviewing {target_annotator}'s work"
|
224 |
+
else:
|
225 |
+
return f"**Review Progress:** No items found for {target_annotator}"
|
226 |
+
|
227 |
+
except Exception as e:
|
228 |
+
log.error(f"Error calculating review progress for user {user_id}: {e}")
|
229 |
+
return "Review Progress: Error calculating progress"
|
230 |
+
|
231 |
def load_review_items_fn(session):
|
232 |
user_id = session.get("user_id")
|
233 |
username = session.get("username")
|
|
|
423 |
current_item["annotated_at"],
|
424 |
current_item["validation_status"],
|
425 |
"", # Placeholder for annotator_name
|
426 |
+
gr.update(value=None, autoplay=False),
|
427 |
gr.update(visible=rejection_visible, value=rejection_reason),
|
428 |
False, # Reset rejection mode
|
429 |
gr.update(value="❌ Reject") # Reset reject button text
|
430 |
)
|
431 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
432 |
def navigate_review_fn(items, current_idx, direction):
|
433 |
if not items:
|
434 |
return 0
|
|
|
650 |
fn=load_review_items_fn,
|
651 |
inputs=[session_state],
|
652 |
outputs=[self.items_state, self.idx_state, self.review_info] + review_display_outputs
|
653 |
+
).then(
|
654 |
+
fn=get_review_progress_fn,
|
655 |
+
inputs=[session_state],
|
656 |
+
outputs=[self.header.progress_display]
|
657 |
).then(
|
658 |
fn=lambda: (None, gr.update(value=None)), # Clear audio state
|
659 |
outputs=[self.original_audio_state, self.audio]
|
|
|
665 |
# Audio loading is now manual only via the Load Audio button
|
666 |
# Removed automatic filename.change callback to prevent slow loading during initialization
|
667 |
|
668 |
+
# Navigation buttons
|
669 |
for btn, direction in [(self.btn_prev, "prev"), (self.btn_next, "next")]:
|
670 |
btn.click(
|
671 |
fn=lambda: update_ui_interactive_state(False),
|
|
|
679 |
inputs=[self.items_state, self.idx_state, session_state],
|
680 |
outputs=review_display_outputs
|
681 |
).then(
|
682 |
+
# Auto-load audio with autoplay for smooth navigation
|
683 |
+
fn=download_voice_fn,
|
684 |
inputs=[self.filename],
|
685 |
outputs=[self.audio, self.original_audio_state, self.audio]
|
686 |
).then(
|
|
|
691 |
outputs=self.interactive_ui_elements
|
692 |
)
|
693 |
|
694 |
+
# Approve/Reject buttons
|
695 |
self.btn_approve.click(
|
696 |
fn=lambda items, idx, session: save_validation_fn(items, idx, session, approved=True, rejection_reason=""), # Pass empty rejection_reason
|
697 |
inputs=[self.items_state, self.idx_state, session_state],
|
698 |
outputs=[self.items_state, self.current_validation_status, self.rejection_reason_input]
|
699 |
+
).then(
|
700 |
+
fn=get_review_progress_fn, # Update progress after approval
|
701 |
+
inputs=[session_state],
|
702 |
+
outputs=[self.header.progress_display]
|
703 |
).then(
|
704 |
fn=lambda: False, # Reset rejection mode
|
705 |
outputs=[self.rejection_mode_active]
|
706 |
).then(
|
707 |
fn=lambda: gr.update(value="❌ Reject"), # Reset reject button
|
708 |
outputs=[self.btn_reject]
|
|
|
|
|
|
|
|
|
|
|
709 |
).then(
|
710 |
fn=lambda items, idx: navigate_review_fn(items, idx, "next"),
|
711 |
inputs=[self.items_state, self.idx_state],
|
|
|
715 |
inputs=[self.items_state, self.idx_state, session_state],
|
716 |
outputs=review_display_outputs
|
717 |
).then(
|
718 |
+
# Auto-load audio with autoplay after moving to next item
|
719 |
+
fn=download_voice_fn,
|
720 |
inputs=[self.filename],
|
721 |
outputs=[self.audio, self.original_audio_state, self.audio]
|
722 |
)
|
|
|
726 |
inputs=[self.items_state, self.idx_state, session_state, self.rejection_reason_input, self.rejection_mode_active],
|
727 |
outputs=[self.items_state, self.current_validation_status, self.rejection_reason_input, self.rejection_mode_active, self.btn_reject]
|
728 |
).then(
|
729 |
+
fn=lambda items, idx, session, rejection_mode: get_review_progress_fn(session) if not rejection_mode else "", # Update progress only after successful rejection
|
730 |
+
inputs=[self.items_state, self.idx_state, session_state, self.rejection_mode_active],
|
731 |
+
outputs=[self.header.progress_display]
|
|
|
732 |
).then(
|
733 |
fn=lambda items, idx, rejection_mode: navigate_review_fn(items, idx, "next") if not rejection_mode else idx,
|
734 |
inputs=[self.items_state, self.idx_state, self.rejection_mode_active],
|
|
|
750 |
inputs=[self.items_state, self.idx_state, session_state, self.rejection_mode_active],
|
751 |
outputs=review_display_outputs
|
752 |
).then(
|
753 |
+
# Auto-load audio with autoplay only if we moved to next item (not in rejection mode)
|
754 |
+
fn=lambda filename, rejection_mode: download_voice_fn(filename) if not rejection_mode else (None, None, gr.update(value=None, autoplay=False)),
|
755 |
inputs=[self.filename, self.rejection_mode_active],
|
756 |
outputs=[self.audio, self.original_audio_state, self.audio]
|
757 |
)
|
758 |
|
759 |
+
# Skip button (just navigate to next)
|
760 |
self.btn_skip.click(
|
761 |
fn=navigate_review_fn,
|
762 |
inputs=[self.items_state, self.idx_state, gr.State("next")],
|
|
|
766 |
inputs=[self.items_state, self.idx_state, session_state],
|
767 |
outputs=review_display_outputs
|
768 |
).then(
|
769 |
+
# Auto-load audio with autoplay after skipping
|
770 |
+
fn=download_voice_fn,
|
771 |
inputs=[self.filename],
|
772 |
outputs=[self.audio, self.original_audio_state, self.audio]
|
773 |
)
|
774 |
|
775 |
+
# Jump button
|
776 |
self.btn_jump.click(
|
777 |
fn=jump_by_data_id_fn,
|
778 |
inputs=[self.items_state, self.jump_data_id_input, self.idx_state],
|
|
|
782 |
inputs=[self.items_state, self.idx_state, session_state],
|
783 |
outputs=review_display_outputs
|
784 |
).then(
|
785 |
+
# Auto-load audio with autoplay after jumping
|
786 |
+
fn=download_voice_fn,
|
787 |
inputs=[self.filename],
|
788 |
outputs=[self.audio, self.original_audio_state, self.audio]
|
789 |
).then(
|