duongthienz commited on
Commit
67fb845
·
verified ·
1 Parent(s): c50b6c5

update multifile analysis with plot normalization

Browse files
Files changed (1) hide show
  1. utils.py +46 -102
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 * 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,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 * 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
@@ -486,21 +486,26 @@ def build_multifile_category_df(validNames, results, summaries, categories, cate
486
  if category not in filled:
487
  df6_dict[category].append(0)
488
 
489
- return pd.DataFrame(df6_dict), allCategories
 
 
 
 
 
490
 
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,45 +515,11 @@ def build_multifile_role_voice_df(validNames, results, summaries, categories,
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,73 +530,46 @@ def build_multifile_role_voice_df(validNames, results, summaries, categories,
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
 
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
  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
 
486
  if category not in filled:
487
  df6_dict[category].append(0)
488
 
489
+ # Normalize each file's row so values sum to 100
490
+ df6 = pd.DataFrame(df6_dict)
491
+ value_cols = [c for c in df6.columns if c != "files"]
492
+ row_sums = df6[value_cols].sum(axis=1).replace(0, 1)
493
+ df6[value_cols] = df6[value_cols].div(row_sums, axis=0) * 100
494
+ return df6, allCategories
495
 
496
 
497
  def build_multifile_role_voice_df(validNames, results, summaries, categories,
498
  categorySelect, speakerRenames=None):
499
+ """Build df8: per-file proportions split by role for single voice, plus
500
+ Multi Voice and No Voice.
501
 
502
+ Single Voice time is broken down into each role and an Unassigned bucket
503
+ (speakers in single-voice segments that haven't been assigned to any role).
504
+ Multi Voice and No Voice come from df5 percentiles (0-100 scale) converted
505
+ to 0-1 proportions.
506
 
507
+ This is the combination of df6 (role proportions) and df7 (voice categories)
508
+ where Single Voice is replaced by its constituent roles.
 
 
 
 
509
  """
510
  speakerRenames = speakerRenames or {}
511
  col_names = list(categories) + ["Unassigned", "Multi Voice", "No Voice"]
 
515
 
516
  for fn in validNames:
517
  currAnnotation, totalSeconds = results[fn]
518
+ safe_total = max(totalSeconds, 1)
519
+ prefix = fn + ": "
520
+ renames = speakerRenames.get(fn, {})
 
 
 
 
 
521
 
522
+ # Role proportions same logic as build_multifile_category_df
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
523
  assigned_all = set()
524
  for i, category in enumerate(categories):
525
  assigned_sps = [
 
530
  valid_sps = [sp for sp in assigned_sps if sp in currAnnotation.labels()]
531
  assigned_all.update(valid_sps)
532
  if valid_sps:
533
+ val = su.sumTimes(currAnnotation.subset(valid_sps)) / safe_total
534
  else:
535
  val = 0.0
536
+ df8_dict[category].append(min(val, 1.0))
537
 
538
  # Unassigned speakers
539
  unassigned_sps = [sp for sp in currAnnotation.labels() if sp not in assigned_all]
540
  if unassigned_sps:
541
+ val = su.sumTimes(currAnnotation.subset(unassigned_sps)) / safe_total
542
  else:
543
  val = 0.0
544
+ df8_dict["Unassigned"].append(min(val, 1.0))
545
 
546
+ # Multi Voice and No Voice from df5 percentiles (0-100 → 0-1)
547
+ partial = summaries[fn]["df5"]
548
+ df8_dict["No Voice"].append(partial["percentiles"][0] / 100)
549
+ df8_dict["Multi Voice"].append(partial["percentiles"][2] / 100)
550
 
551
+ # Normalize each file's row so values sum to 100
552
+ df8 = pd.DataFrame(df8_dict)
553
+ row_sums = df8[col_names].sum(axis=1).replace(0, 1)
554
+ df8[col_names] = df8[col_names].div(row_sums, axis=0) * 100
555
+ return df8, col_names
556
 
 
 
557
 
558
+ def build_multifile_voice_df(validNames, summaries):
559
+ """Build df7 (no/one/multi voice percentages per file) for the multi-file expander.
560
+ Values are normalized to sum to 100 per file.
 
 
 
 
 
561
  """
 
562
  voiceNames = ["No Voice", "Single Voice", "Multi Voice"]
563
  df7_dict = {"files": validNames}
564
  for name in voiceNames:
565
  df7_dict[name] = []
566
 
567
  for fn in validNames:
568
+ partial = summaries[fn]["df5"]
569
+ for i, name in enumerate(voiceNames):
570
+ df7_dict[name].append(partial["percentiles"][i])
571
+
572
+ df7 = pd.DataFrame(df7_dict)
573
+ row_sums = df7[voiceNames].sum(axis=1).replace(0, 1)
574
+ df7[voiceNames] = df7[voiceNames].div(row_sums, axis=0) * 100
575
+ return df7, voiceNames