Spaces:
Running
Running
progress tracking
Browse files- components/review_dashboard_page.py +69 -54
- data/models.py +20 -1
components/review_dashboard_page.py
CHANGED
@@ -12,6 +12,11 @@ from config import conf
|
|
12 |
from utils.database import get_db
|
13 |
from data.models import Annotation, TTSData, Annotator, Validation
|
14 |
from data.repository.annotator_workload_repo import AnnotatorWorkloadRepo
|
|
|
|
|
|
|
|
|
|
|
15 |
|
16 |
log = Logger()
|
17 |
LOADER = CloudServerAudioLoader(conf.FTP_URL)
|
@@ -302,7 +307,7 @@ class ReviewDashboardPage:
|
|
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 annotations
|
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()
|
@@ -312,33 +317,28 @@ class ReviewDashboardPage:
|
|
312 |
|
313 |
log.info(f"Found target annotator with ID: {target_annotator_obj.id}")
|
314 |
|
315 |
-
#
|
316 |
-
|
317 |
-
INITIAL_BATCH_SIZE = 5 # Load only 5 items initially for instant response
|
318 |
|
319 |
-
#
|
320 |
-
|
321 |
-
|
322 |
-
|
323 |
-
|
324 |
-
).join(
|
325 |
-
TTSData, Annotation.tts_data_id == TTSData.id
|
326 |
-
).filter(
|
327 |
-
Annotation.annotator_id == target_annotator_obj.id
|
328 |
-
).order_by(Annotation.id).limit(INITIAL_BATCH_SIZE)
|
329 |
-
|
330 |
-
initial_results = initial_query.all()
|
331 |
|
332 |
# Get total count for progress info (this is fast)
|
333 |
total_count = db.query(Annotation).filter(
|
334 |
Annotation.annotator_id == target_annotator_obj.id
|
335 |
).count()
|
336 |
|
337 |
-
log.info(f"
|
338 |
|
339 |
# Process items with minimal data - validation status will be loaded on-demand
|
340 |
items = []
|
341 |
-
for annotation
|
|
|
|
|
|
|
342 |
# Check if annotation is deleted (minimal processing)
|
343 |
is_deleted = not annotation.annotated_sentence or annotation.annotated_sentence.strip() == ""
|
344 |
annotated_sentence_display = "[DELETED ANNOTATION]" if is_deleted else annotation.annotated_sentence
|
@@ -346,8 +346,8 @@ class ReviewDashboardPage:
|
|
346 |
items.append({
|
347 |
"annotation_id": annotation.id,
|
348 |
"tts_id": annotation.tts_data_id,
|
349 |
-
"filename": filename,
|
350 |
-
"sentence": sentence,
|
351 |
"annotated_sentence": annotated_sentence_display,
|
352 |
"is_deleted": is_deleted,
|
353 |
"annotated_at": annotation.annotated_at.isoformat() if annotation.annotated_at else "",
|
@@ -355,36 +355,28 @@ class ReviewDashboardPage:
|
|
355 |
"validation_loaded": False # Track if validation status has been loaded
|
356 |
})
|
357 |
|
358 |
-
#
|
359 |
initial_idx = 0
|
360 |
-
if items:
|
361 |
-
found_unreviewed = False
|
362 |
-
# First, try to find unreviewed non-deleted annotations
|
363 |
-
for i, item_data in enumerate(items):
|
364 |
-
if (item_data["validation_status"] == "Not Reviewed" and
|
365 |
-
not item_data.get("is_deleted", False)):
|
366 |
-
initial_idx = i
|
367 |
-
found_unreviewed = True
|
368 |
-
break
|
369 |
-
|
370 |
-
# If no unreviewed non-deleted items, look for any unreviewed items
|
371 |
-
if not found_unreviewed:
|
372 |
-
for i, item_data in enumerate(items):
|
373 |
-
if item_data["validation_status"].startswith("Not Reviewed"):
|
374 |
-
initial_idx = i
|
375 |
-
found_unreviewed = True
|
376 |
-
break
|
377 |
-
|
378 |
-
# If no unreviewed items at all, use the last item
|
379 |
-
if not found_unreviewed:
|
380 |
-
initial_idx = len(items) - 1 if items else 0
|
381 |
|
382 |
# Set initial display
|
383 |
if items:
|
384 |
initial_item = items[initial_idx]
|
385 |
-
review_info_text = f"π **Phase 2 Review Mode** -
|
386 |
-
|
387 |
-
#
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
388 |
rejection_reason_val = ""
|
389 |
rejection_visible_val = False
|
390 |
if initial_item["validation_status"].startswith("Rejected"):
|
@@ -394,6 +386,8 @@ class ReviewDashboardPage:
|
|
394 |
rejection_reason_val = initial_item["validation_status"][start_paren+1:end_paren]
|
395 |
rejection_visible_val = True
|
396 |
|
|
|
|
|
397 |
return (
|
398 |
items,
|
399 |
initial_idx,
|
@@ -411,15 +405,9 @@ class ReviewDashboardPage:
|
|
411 |
gr.update(value="β Reject") # Reset reject button
|
412 |
)
|
413 |
else:
|
414 |
-
#
|
415 |
-
|
416 |
-
|
417 |
-
# except Exception as e:
|
418 |
-
# log.error(f"Error loading review items: {e}")
|
419 |
-
# sentry_sdk.capture_exception(e)
|
420 |
-
# gr.Error(f"Failed to load review data: {e}")
|
421 |
-
# # Ensure correct order and number of return values for error case (14 outputs)
|
422 |
-
# return [], 0, "", "", "", "", "", "", "", "", gr.update(value=None, autoplay=False), gr.update(visible=False, value=""), False, gr.update(value="β Reject")
|
423 |
|
424 |
def show_current_review_item_fn(items, idx, session):
|
425 |
if not items or idx >= len(items) or idx < 0:
|
@@ -549,6 +537,33 @@ class ReviewDashboardPage:
|
|
549 |
db.commit()
|
550 |
log.info(f"Validation saved successfully for annotation_id: {annotation_id}")
|
551 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
552 |
items[idx]["validation_status"] = "Approved" if approved else f"Rejected ({rejection_reason})" if rejection_reason else "Rejected"
|
553 |
|
554 |
# Show rejection reason input only if rejected, otherwise hide and clear
|
|
|
12 |
from utils.database import get_db
|
13 |
from data.models import Annotation, TTSData, Annotator, Validation
|
14 |
from data.repository.annotator_workload_repo import AnnotatorWorkloadRepo
|
15 |
+
from utils.user_progress import (
|
16 |
+
get_next_unreviewed_annotation,
|
17 |
+
update_user_progress,
|
18 |
+
get_annotations_from_position
|
19 |
+
)
|
20 |
|
21 |
log = Logger()
|
22 |
LOADER = CloudServerAudioLoader(conf.FTP_URL)
|
|
|
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 with PROGRESS TRACKING
|
311 |
with get_db() as db:
|
312 |
# Get target annotator's ID
|
313 |
target_annotator_obj = db.query(Annotator).filter_by(name=target_annotator).first()
|
|
|
317 |
|
318 |
log.info(f"Found target annotator with ID: {target_annotator_obj.id}")
|
319 |
|
320 |
+
# π― PROGRESS TRACKING: Find next unreviewed annotation position
|
321 |
+
next_annotation_id, next_position = get_next_unreviewed_annotation(db, user_id, target_annotator_obj.id)
|
|
|
322 |
|
323 |
+
# Load batch size for responsive loading
|
324 |
+
INITIAL_BATCH_SIZE = 10 # Increased from 5 to 10 for better UX
|
325 |
+
|
326 |
+
# Load annotations starting from the next unreviewed position
|
327 |
+
annotations_data = get_annotations_from_position(db, target_annotator_obj.id, next_position, INITIAL_BATCH_SIZE)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
328 |
|
329 |
# Get total count for progress info (this is fast)
|
330 |
total_count = db.query(Annotation).filter(
|
331 |
Annotation.annotator_id == target_annotator_obj.id
|
332 |
).count()
|
333 |
|
334 |
+
log.info(f"Progress-aware load: Starting from position {next_position}, loaded {len(annotations_data)} annotations out of {total_count} total for target annotator ID {target_annotator_obj.id}")
|
335 |
|
336 |
# Process items with minimal data - validation status will be loaded on-demand
|
337 |
items = []
|
338 |
+
for annotation in annotations_data:
|
339 |
+
# Get TTS data
|
340 |
+
tts_data = annotation.tts_data
|
341 |
+
|
342 |
# Check if annotation is deleted (minimal processing)
|
343 |
is_deleted = not annotation.annotated_sentence or annotation.annotated_sentence.strip() == ""
|
344 |
annotated_sentence_display = "[DELETED ANNOTATION]" if is_deleted else annotation.annotated_sentence
|
|
|
346 |
items.append({
|
347 |
"annotation_id": annotation.id,
|
348 |
"tts_id": annotation.tts_data_id,
|
349 |
+
"filename": tts_data.filename,
|
350 |
+
"sentence": tts_data.sentence,
|
351 |
"annotated_sentence": annotated_sentence_display,
|
352 |
"is_deleted": is_deleted,
|
353 |
"annotated_at": annotation.annotated_at.isoformat() if annotation.annotated_at else "",
|
|
|
355 |
"validation_loaded": False # Track if validation status has been loaded
|
356 |
})
|
357 |
|
358 |
+
# π― PROGRESS TRACKING: Start from first item (index 0) since we loaded from the correct position
|
359 |
initial_idx = 0
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
360 |
|
361 |
# Set initial display
|
362 |
if items:
|
363 |
initial_item = items[initial_idx]
|
364 |
+
review_info_text = f"π **Phase 2 Review Mode** - Continuing from position {next_position + 1}/{total_count}. Loaded {len(items)} items."
|
365 |
+
|
366 |
+
# Load validation status for the first item immediately
|
367 |
+
try:
|
368 |
+
annotation_obj = db.query(Annotation).filter_by(id=initial_item["annotation_id"]).first()
|
369 |
+
if annotation_obj:
|
370 |
+
validation_status, is_deleted = get_validation_status_for_item(db, initial_item["annotation_id"], user_id, annotation_obj)
|
371 |
+
initial_item["validation_status"] = validation_status
|
372 |
+
initial_item["is_deleted"] = is_deleted
|
373 |
+
initial_item["validation_loaded"] = True
|
374 |
+
|
375 |
+
if is_deleted:
|
376 |
+
initial_item["annotated_sentence"] = "[DELETED ANNOTATION]"
|
377 |
+
except Exception as e:
|
378 |
+
log.warning(f"Failed to load initial validation status: {e}")
|
379 |
+
|
380 |
rejection_reason_val = ""
|
381 |
rejection_visible_val = False
|
382 |
if initial_item["validation_status"].startswith("Rejected"):
|
|
|
386 |
rejection_reason_val = initial_item["validation_status"][start_paren+1:end_paren]
|
387 |
rejection_visible_val = True
|
388 |
|
389 |
+
log.info(f"π― User {username} resuming review from position {next_position}, annotation ID {initial_item['annotation_id']}")
|
390 |
+
|
391 |
return (
|
392 |
items,
|
393 |
initial_idx,
|
|
|
405 |
gr.update(value="β Reject") # Reset reject button
|
406 |
)
|
407 |
else:
|
408 |
+
# All items have been reviewed
|
409 |
+
review_info_text = f"π **Review Complete!** - All {total_count} annotations have been reviewed for {target_annotator}."
|
410 |
+
return [], 0, review_info_text, "", "", "", "", "", "", "", gr.update(value=None, autoplay=False), gr.update(visible=False, value=""), False, gr.update(value="β Reject")
|
|
|
|
|
|
|
|
|
|
|
|
|
411 |
|
412 |
def show_current_review_item_fn(items, idx, session):
|
413 |
if not items or idx >= len(items) or idx < 0:
|
|
|
537 |
db.commit()
|
538 |
log.info(f"Validation saved successfully for annotation_id: {annotation_id}")
|
539 |
|
540 |
+
# π― UPDATE USER PROGRESS TRACKING
|
541 |
+
try:
|
542 |
+
username = session.get("username")
|
543 |
+
if username:
|
544 |
+
# Find target annotator for this user
|
545 |
+
target_annotator = None
|
546 |
+
for annotator_name, reviewer_name in conf.REVIEW_MAPPING.items():
|
547 |
+
if reviewer_name == username:
|
548 |
+
target_annotator = annotator_name
|
549 |
+
break
|
550 |
+
|
551 |
+
if target_annotator:
|
552 |
+
target_annotator_obj = db.query(Annotator).filter_by(name=target_annotator).first()
|
553 |
+
if target_annotator_obj:
|
554 |
+
# Calculate the current position in the review list
|
555 |
+
current_position = db.query(Annotation).filter(
|
556 |
+
Annotation.annotator_id == target_annotator_obj.id,
|
557 |
+
Annotation.id <= annotation_id
|
558 |
+
).count() - 1 # Convert to 0-based index
|
559 |
+
|
560 |
+
# Update user progress
|
561 |
+
update_user_progress(db, user_id, target_annotator_obj.id, annotation_id, current_position)
|
562 |
+
log.info(f"π― Updated progress for user {user_id}: annotation {annotation_id} at position {current_position}")
|
563 |
+
except Exception as e:
|
564 |
+
log.warning(f"Failed to update user progress: {e}")
|
565 |
+
# Don't fail the validation save if progress tracking fails
|
566 |
+
|
567 |
items[idx]["validation_status"] = "Approved" if approved else f"Rejected ({rejection_reason})" if rejection_reason else "Rejected"
|
568 |
|
569 |
# Show rejection reason input only if rejected, otherwise hide and clear
|
data/models.py
CHANGED
@@ -158,4 +158,23 @@ class Validation(Base):
|
|
158 |
validated_at = Column(DateTime, nullable=False)
|
159 |
|
160 |
annotation = relationship("Annotation")
|
161 |
-
validator = relationship("Annotator", foreign_keys=[validator_id]) # Fixed: should reference Annotator
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
158 |
validated_at = Column(DateTime, nullable=False)
|
159 |
|
160 |
annotation = relationship("Annotation")
|
161 |
+
validator = relationship("Annotator", foreign_keys=[validator_id]) # Fixed: should reference Annotator
|
162 |
+
|
163 |
+
|
164 |
+
# --------------------------------------------------------------------------- #
|
165 |
+
# UserProgress #
|
166 |
+
# --------------------------------------------------------------------------- #
|
167 |
+
class UserProgress(Base):
|
168 |
+
__tablename__ = "user_progress"
|
169 |
+
|
170 |
+
id = Column(Integer, primary_key=True)
|
171 |
+
user_id = Column(Integer, ForeignKey("annotators.id"), nullable=False)
|
172 |
+
target_annotator_id = Column(Integer, ForeignKey("annotators.id"), nullable=False)
|
173 |
+
last_reviewed_annotation_id = Column(Integer, ForeignKey("annotations.id"), nullable=True)
|
174 |
+
last_position = Column(Integer, default=0) # Position in the review list
|
175 |
+
updated_at = Column(DateTime, nullable=False)
|
176 |
+
|
177 |
+
# Relationships
|
178 |
+
user = relationship("Annotator", foreign_keys=[user_id])
|
179 |
+
target_annotator = relationship("Annotator", foreign_keys=[target_annotator_id])
|
180 |
+
last_reviewed_annotation = relationship("Annotation", foreign_keys=[last_reviewed_annotation_id])
|