duongthienz commited on
Commit
edc5c09
·
verified ·
1 Parent(s): 0fa9c00

Update multi-file visualization

Browse files
Files changed (1) hide show
  1. utils.py +100 -30
utils.py CHANGED
@@ -461,10 +461,10 @@ def build_multifile_category_df(validNames, results, summaries, categories, cate
461
  ] if i < len(categorySelect) else []
462
  valid_sps = [sp for sp in assigned_sps if sp in currAnnotation.labels()]
463
  if valid_sps:
464
- val = su.sumTimes(currAnnotation.subset(valid_sps)) / safe_total
465
  else:
466
  val = 0.0
467
- df6_dict[category].append(min(val, 1.0))
468
  filled.add(category)
469
 
470
  # For unassigned speakers: each gets their own column
@@ -477,8 +477,8 @@ def build_multifile_category_df(validNames, results, summaries, categories, cate
477
  unassigned = [sp for sp in currAnnotation.labels() if sp not in assigned_all]
478
  for sp in unassigned:
479
  display = renames.get(sp, sp)
480
- val = su.sumTimes(currAnnotation.subset([sp])) / safe_total
481
- df6_dict[display].append(min(val, 1.0))
482
  filled.add(display)
483
 
484
  # Fill 0 for every allCategories column not touched this row
@@ -491,16 +491,16 @@ def build_multifile_category_df(validNames, results, summaries, categories, cate
491
 
492
  def build_multifile_role_voice_df(validNames, results, summaries, categories,
493
  categorySelect, speakerRenames=None):
494
- """Build df8: per-file proportions split by role for single voice, plus
495
- Multi Voice and No Voice.
496
 
497
- Single Voice time is broken down into each role and an Unassigned bucket
498
- (speakers in single-voice segments that haven't been assigned to any role).
499
- Multi Voice and No Voice come from df5 percentiles (0-100 scale) converted
500
- to 0-1 proportions.
501
 
502
- This is the combination of df6 (role proportions) and df7 (voice categories)
503
- where Single Voice is replaced by its constituent roles.
 
 
 
 
504
  """
505
  speakerRenames = speakerRenames or {}
506
  col_names = list(categories) + ["Unassigned", "Multi Voice", "No Voice"]
@@ -510,11 +510,45 @@ def build_multifile_role_voice_df(validNames, results, summaries, categories,
510
 
511
  for fn in validNames:
512
  currAnnotation, totalSeconds = results[fn]
513
- safe_total = max(totalSeconds, 1)
514
- prefix = fn + ": "
515
- renames = speakerRenames.get(fn, {})
 
 
 
 
 
516
 
517
- # Role proportions same logic as build_multifile_category_df
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
518
  assigned_all = set()
519
  for i, category in enumerate(categories):
520
  assigned_sps = [
@@ -525,37 +559,73 @@ def build_multifile_role_voice_df(validNames, results, summaries, categories,
525
  valid_sps = [sp for sp in assigned_sps if sp in currAnnotation.labels()]
526
  assigned_all.update(valid_sps)
527
  if valid_sps:
528
- val = su.sumTimes(currAnnotation.subset(valid_sps)) / safe_total
529
  else:
530
  val = 0.0
531
- df8_dict[category].append(min(val, 1.0))
532
 
533
  # Unassigned speakers
534
  unassigned_sps = [sp for sp in currAnnotation.labels() if sp not in assigned_all]
535
  if unassigned_sps:
536
- val = su.sumTimes(currAnnotation.subset(unassigned_sps)) / safe_total
537
  else:
538
  val = 0.0
539
- df8_dict["Unassigned"].append(min(val, 1.0))
540
-
541
- # Multi Voice and No Voice from df5 percentiles (0-100 → 0-1)
542
- partial = summaries[fn]["df5"]
543
- df8_dict["No Voice"].append(partial["percentiles"][0] / 100)
544
- df8_dict["Multi Voice"].append(partial["percentiles"][2] / 100)
545
 
546
  return pd.DataFrame(df8_dict), col_names
547
 
548
 
549
- def build_multifile_voice_df(validNames, summaries):
550
- """Build df7 (no/one/multi voice percentages per file) for the multi-file expander."""
 
 
 
 
 
 
 
 
 
 
 
551
  voiceNames = ["No Voice", "Single Voice", "Multi Voice"]
552
  df7_dict = {"files": validNames}
553
  for name in voiceNames:
554
  df7_dict[name] = []
555
 
556
  for fn in validNames:
557
- partial = summaries[fn]["df5"]
558
- for i, name in enumerate(voiceNames):
559
- df7_dict[name].append(partial["percentiles"][i])
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
560
 
561
  return pd.DataFrame(df7_dict), voiceNames
 
461
  ] if i < len(categorySelect) else []
462
  valid_sps = [sp for sp in assigned_sps if sp in currAnnotation.labels()]
463
  if valid_sps:
464
+ val = su.sumTimes(currAnnotation.subset(valid_sps)) / safe_total * 100
465
  else:
466
  val = 0.0
467
+ df6_dict[category].append(min(val, 100.0))
468
  filled.add(category)
469
 
470
  # For unassigned speakers: each gets their own column
 
477
  unassigned = [sp for sp in currAnnotation.labels() if sp not in assigned_all]
478
  for sp in unassigned:
479
  display = renames.get(sp, sp)
480
+ val = su.sumTimes(currAnnotation.subset([sp])) / safe_total * 100
481
+ df6_dict[display].append(min(val, 100.0))
482
  filled.add(display)
483
 
484
  # Fill 0 for every allCategories column not touched this row
 
491
 
492
  def build_multifile_role_voice_df(validNames, results, summaries, categories,
493
  categorySelect, speakerRenames=None):
494
+ """Build df8: per-file percentages split by role plus Multi Voice and No Voice.
 
495
 
496
+ All values are on a 0-100 scale (% of file duration).
 
 
 
497
 
498
+ Multi Voice and No Voice are computed directly from the raw annotation using
499
+ pyannote timeline intersection this avoids the index-step scale mismatch
500
+ from annotationToNoiseList/df5 percentiles.
501
+
502
+ Role columns use sumTimes on the union of role speaker segments (overlaps
503
+ within a role counted once).
504
  """
505
  speakerRenames = speakerRenames or {}
506
  col_names = list(categories) + ["Unassigned", "Multi Voice", "No Voice"]
 
510
 
511
  for fn in validNames:
512
  currAnnotation, totalSeconds = results[fn]
513
+ # Use annotation's actual span as denominator to avoid truncation issues
514
+ annotation_end = max(
515
+ (s.end for s in currAnnotation.itersegments()),
516
+ default=totalSeconds
517
+ )
518
+ safe_total = max(annotation_end, totalSeconds, 1)
519
+ prefix = fn + ": "
520
+ renames = speakerRenames.get(fn, {})
521
 
522
+ # Compute Multi Voice directly: time where 2+ speakers overlap
523
+ # by intersecting all pairs of speaker timelines
524
+ labels = [l for l in currAnnotation.labels()]
525
+ multi_voice_tl = currAnnotation.get_timeline(copy=False)
526
+ # Build per-speaker timelines and find overlapping regions
527
+ speaker_timelines = [
528
+ currAnnotation.subset([sp]).get_timeline(copy=False)
529
+ for sp in labels
530
+ ]
531
+ # Multi Voice = union of all pairwise intersections
532
+ from pyannote.core import Timeline
533
+ multi_segments = []
534
+ for i in range(len(speaker_timelines)):
535
+ for j in range(i + 1, len(speaker_timelines)):
536
+ for seg_i in speaker_timelines[i]:
537
+ for seg_j in speaker_timelines[j]:
538
+ overlap = seg_i & seg_j
539
+ if overlap:
540
+ multi_segments.append(overlap)
541
+ multi_tl = Timeline(segments=multi_segments).support()
542
+ multi_voice_pct = multi_tl.duration() / safe_total * 100
543
+
544
+ # No Voice = total duration minus any-speaker coverage
545
+ any_voice_duration = currAnnotation.get_timeline(copy=False).support().duration()
546
+ no_voice_pct = max((safe_total - any_voice_duration) / safe_total * 100, 0.0)
547
+
548
+ df8_dict["Multi Voice"].append(min(multi_voice_pct, 100.0))
549
+ df8_dict["No Voice"].append(min(no_voice_pct, 100.0))
550
+
551
+ # Role proportions — sumTimes on union of role speakers (0-100 scale)
552
  assigned_all = set()
553
  for i, category in enumerate(categories):
554
  assigned_sps = [
 
559
  valid_sps = [sp for sp in assigned_sps if sp in currAnnotation.labels()]
560
  assigned_all.update(valid_sps)
561
  if valid_sps:
562
+ val = su.sumTimes(currAnnotation.subset(valid_sps)) / safe_total * 100
563
  else:
564
  val = 0.0
565
+ df8_dict[category].append(min(val, 100.0))
566
 
567
  # Unassigned speakers
568
  unassigned_sps = [sp for sp in currAnnotation.labels() if sp not in assigned_all]
569
  if unassigned_sps:
570
+ val = su.sumTimes(currAnnotation.subset(unassigned_sps)) / safe_total * 100
571
  else:
572
  val = 0.0
573
+ df8_dict["Unassigned"].append(min(val, 100.0))
 
 
 
 
 
574
 
575
  return pd.DataFrame(df8_dict), col_names
576
 
577
 
578
+ def build_multifile_voice_df(validNames, results):
579
+ """Build df7 (no/single/multi voice percentages per file) for the multi-file expander.
580
+
581
+ All values are on a 0-100 scale (% of file duration).
582
+
583
+ Computed directly from the raw annotation using pyannote timeline methods
584
+ to avoid the index-step scale mismatch in annotationToNoiseList/df5:
585
+ - Multi Voice: union of all pairwise speaker timeline intersections
586
+ - Any Voice: support of the full annotation timeline
587
+ - No Voice: file duration minus Any Voice
588
+ - Single Voice: Any Voice minus Multi Voice
589
+ """
590
+ from pyannote.core import Timeline
591
  voiceNames = ["No Voice", "Single Voice", "Multi Voice"]
592
  df7_dict = {"files": validNames}
593
  for name in voiceNames:
594
  df7_dict[name] = []
595
 
596
  for fn in validNames:
597
+ currAnnotation, totalSeconds = results[fn]
598
+ annotation_end = max(
599
+ (s.end for s in currAnnotation.itersegments()),
600
+ default=totalSeconds
601
+ )
602
+ safe_total = max(annotation_end, totalSeconds, 1)
603
+
604
+ labels = list(currAnnotation.labels())
605
+ speaker_timelines = [
606
+ currAnnotation.subset([sp]).get_timeline(copy=False)
607
+ for sp in labels
608
+ ]
609
+
610
+ # Multi Voice = union of all pairwise overlaps
611
+ multi_segments = []
612
+ for i in range(len(speaker_timelines)):
613
+ for j in range(i + 1, len(speaker_timelines)):
614
+ for seg_i in speaker_timelines[i]:
615
+ for seg_j in speaker_timelines[j]:
616
+ overlap = seg_i & seg_j
617
+ if overlap:
618
+ multi_segments.append(overlap)
619
+ multi_tl = Timeline(segments=multi_segments).support()
620
+ multi_pct = multi_tl.duration() / safe_total * 100
621
+
622
+ # Any Voice = support of full annotation
623
+ any_voice = currAnnotation.get_timeline(copy=False).support().duration()
624
+ no_voice_pct = max((safe_total - any_voice) / safe_total * 100, 0.0)
625
+ single_pct = max(any_voice / safe_total * 100 - multi_pct, 0.0)
626
+
627
+ df7_dict["No Voice"].append(min(no_voice_pct, 100.0))
628
+ df7_dict["Single Voice"].append(min(single_pct, 100.0))
629
+ df7_dict["Multi Voice"].append(min(multi_pct, 100.0))
630
 
631
  return pd.DataFrame(df7_dict), voiceNames