Spaces:
Sleeping
Sleeping
update multifile analysis with plot normalization
Browse files
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,
|
| 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,
|
| 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 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 490 |
|
| 491 |
|
| 492 |
def build_multifile_role_voice_df(validNames, results, summaries, categories,
|
| 493 |
categorySelect, speakerRenames=None):
|
| 494 |
-
"""Build df8: per-file
|
|
|
|
| 495 |
|
| 496 |
-
|
|
|
|
|
|
|
|
|
|
| 497 |
|
| 498 |
-
|
| 499 |
-
|
| 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 |
-
|
| 514 |
-
|
| 515 |
-
|
| 516 |
-
default=totalSeconds
|
| 517 |
-
)
|
| 518 |
-
safe_total = max(annotation_end, totalSeconds, 1)
|
| 519 |
-
prefix = fn + ": "
|
| 520 |
-
renames = speakerRenames.get(fn, {})
|
| 521 |
|
| 522 |
-
#
|
| 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
|
| 563 |
else:
|
| 564 |
val = 0.0
|
| 565 |
-
df8_dict[category].append(min(val,
|
| 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
|
| 571 |
else:
|
| 572 |
val = 0.0
|
| 573 |
-
df8_dict["Unassigned"].append(min(val,
|
| 574 |
|
| 575 |
-
|
|
|
|
|
|
|
|
|
|
| 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 |
-
|
| 582 |
-
|
| 583 |
-
|
| 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 |
-
|
| 598 |
-
|
| 599 |
-
|
| 600 |
-
|
| 601 |
-
|
| 602 |
-
|
| 603 |
-
|
| 604 |
-
|
| 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
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|