-
Notifications
You must be signed in to change notification settings - Fork 0
PR1.2: add input suitability summary #91
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
f633878
3cb1e8e
bdbd95a
6992600
29d5151
9e1c23a
4a8ebfe
4507d91
a8b7675
ebf10ec
e030620
89295a8
10b9eba
5dc1714
966a39a
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,5 @@ | ||
| from __future__ import annotations | ||
|
|
||
| from practicelens.diagnostics.input_suitability import summarize_input_suitability | ||
|
|
||
| __all__ = ["summarize_input_suitability"] |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,133 @@ | ||
| from __future__ import annotations | ||
|
|
||
| from practicelens.alignment import AlignmentPath | ||
| from practicelens.domain.models import InputSuitabilitySummary | ||
| from practicelens.domain.types import Seconds | ||
| from practicelens.features import FeatureBundle | ||
|
|
||
| _DURATION_RATIO_WARNING_MIN = 0.75 | ||
| _DURATION_RATIO_WARNING_MAX = 1.35 | ||
| _DURATION_RATIO_LOW_MIN = 0.50 | ||
| _DURATION_RATIO_LOW_MAX = 2.00 | ||
| _ALIGNMENT_WARNING_MIN = 0.85 | ||
| _ALIGNMENT_LOW_MIN = 0.60 | ||
| _VOICED_WARNING_MIN = 0.35 | ||
| _VOICED_LOW_MIN = 0.15 | ||
| _ONSET_PRESENT_MIN = 2 | ||
| _SCORE_DIGITS = 6 | ||
|
|
||
|
|
||
| def summarize_input_suitability( | ||
| reference: FeatureBundle, | ||
| take: FeatureBundle, | ||
| alignment: AlignmentPath, | ||
| ) -> InputSuitabilitySummary: | ||
| """Build a deterministic evidence summary for the analyzed input pair.""" | ||
|
|
||
| reference_duration_s = _duration_s(reference) | ||
| take_duration_s = _duration_s(take) | ||
| duration_ratio = _duration_ratio(reference_duration_s, take_duration_s) | ||
| alignment_coverage = _round_ratio(alignment.coverage_ratio) | ||
| reference_voiced_coverage = _round_ratio(_voiced_ratio(reference)) | ||
| take_voiced_coverage = _round_ratio(_voiced_ratio(take)) | ||
| voiced_frame_coverage = _round_ratio(min(reference_voiced_coverage, take_voiced_coverage)) | ||
| reference_onset_count = len(reference.onset_times_s) | ||
| take_onset_count = len(take.onset_times_s) | ||
| onset_evidence = _onset_evidence(reference_onset_count, take_onset_count) | ||
|
|
||
| risk_points = 0 | ||
| low_confidence = False | ||
| reasons: list[str] = [] | ||
|
|
||
| if reference_duration_s <= 0.0 or take_duration_s <= 0.0: | ||
| low_confidence = True | ||
| reasons.append("Reference or take duration is unavailable.") | ||
| elif _DURATION_RATIO_WARNING_MIN <= duration_ratio <= _DURATION_RATIO_WARNING_MAX: | ||
| reasons.append("Take duration is comparable to the reference.") | ||
| else: | ||
| reasons.append("Take duration differs from the reference.") | ||
| risk_points += 1 | ||
| if duration_ratio < _DURATION_RATIO_LOW_MIN or duration_ratio > _DURATION_RATIO_LOW_MAX: | ||
| low_confidence = True | ||
|
|
||
| if alignment_coverage >= _ALIGNMENT_WARNING_MIN: | ||
| reasons.append("Alignment coverage is broad.") | ||
| else: | ||
| reasons.append("Alignment coverage is limited.") | ||
| risk_points += 1 | ||
| if alignment_coverage < _ALIGNMENT_LOW_MIN: | ||
| low_confidence = True | ||
|
|
||
| if voiced_frame_coverage >= _VOICED_WARNING_MIN: | ||
| reasons.append("Voiced-frame coverage is sufficient.") | ||
| else: | ||
| reasons.append("Voiced-frame coverage is limited.") | ||
| risk_points += 1 | ||
| if voiced_frame_coverage < _VOICED_LOW_MIN: | ||
| low_confidence = True | ||
|
|
||
| if onset_evidence == "present": | ||
| reasons.append("Onset evidence is present.") | ||
| elif onset_evidence == "sparse": | ||
| reasons.append("Onset evidence is sparse.") | ||
| risk_points += 1 | ||
| else: | ||
| reasons.append("Onset evidence is absent.") | ||
| risk_points += 1 | ||
|
|
||
| status = _status(low_confidence=low_confidence, risk_points=risk_points) | ||
| return InputSuitabilitySummary( | ||
| status=status, | ||
| reference_duration_s=Seconds(_round_ratio(reference_duration_s)), | ||
| take_duration_s=Seconds(_round_ratio(take_duration_s)), | ||
| duration_ratio=duration_ratio, | ||
| alignment_coverage=alignment_coverage, | ||
| voiced_frame_coverage=voiced_frame_coverage, | ||
| reference_voiced_frame_coverage=reference_voiced_coverage, | ||
| take_voiced_frame_coverage=take_voiced_coverage, | ||
| onset_evidence=onset_evidence, | ||
| reference_onset_count=reference_onset_count, | ||
| take_onset_count=take_onset_count, | ||
| reasons=tuple(reasons), | ||
| ) | ||
|
|
||
|
|
||
| def _duration_s(bundle: FeatureBundle) -> float: | ||
| if not bundle.time_axis_s: | ||
| return 0.0 | ||
| if len(bundle.time_axis_s) == 1: | ||
| return max(0.0, bundle.time_axis_s[0]) | ||
| return max(0.0, bundle.time_axis_s[-1] - bundle.time_axis_s[0]) | ||
|
|
||
|
|
||
| def _duration_ratio(reference_duration_s: float, take_duration_s: float) -> float: | ||
| if reference_duration_s <= 0.0: | ||
| return 0.0 | ||
| return _round_ratio(take_duration_s / reference_duration_s) | ||
|
|
||
|
|
||
| def _voiced_ratio(bundle: FeatureBundle) -> float: | ||
| if not bundle.voiced_mask: | ||
| return 0.0 | ||
| return sum(1 for voiced in bundle.voiced_mask if voiced) / float(len(bundle.voiced_mask)) | ||
|
|
||
|
|
||
| def _onset_evidence(reference_onset_count: int, take_onset_count: int) -> str: | ||
| shared_onset_count = min(reference_onset_count, take_onset_count) | ||
| if shared_onset_count >= _ONSET_PRESENT_MIN: | ||
| return "present" | ||
| if shared_onset_count == 1: | ||
| return "sparse" | ||
| return "absent" | ||
|
|
||
|
|
||
| def _status(*, low_confidence: bool, risk_points: int) -> str: | ||
| if low_confidence or risk_points >= 3: | ||
| return "low_confidence" | ||
| if risk_points > 0: | ||
| return "warning" | ||
| return "ok" | ||
|
|
||
|
|
||
| def _round_ratio(value: float) -> float: | ||
| return round(float(value), _SCORE_DIGITS) |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,21 @@ | ||
| from __future__ import annotations | ||
|
|
||
| from practicelens.domain.models import InputSuitabilitySummary | ||
|
|
||
|
|
||
| def input_suitability_to_payload(summary: InputSuitabilitySummary) -> dict[str, object]: | ||
| return { | ||
| "schema_version": int(summary.schema_version), | ||
| "status": summary.status, | ||
| "reference_duration_s": summary.reference_duration_s, | ||
| "take_duration_s": summary.take_duration_s, | ||
| "duration_ratio": summary.duration_ratio, | ||
| "alignment_coverage": summary.alignment_coverage, | ||
| "voiced_frame_coverage": summary.voiced_frame_coverage, | ||
| "reference_voiced_frame_coverage": summary.reference_voiced_frame_coverage, | ||
| "take_voiced_frame_coverage": summary.take_voiced_frame_coverage, | ||
| "onset_evidence": summary.onset_evidence, | ||
| "reference_onset_count": summary.reference_onset_count, | ||
| "take_onset_count": summary.take_onset_count, | ||
| "reasons": list(summary.reasons), | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -3,6 +3,7 @@ | |
| import json | ||
|
|
||
| from practicelens.domain.models import AnalysisReport | ||
| from practicelens.reporting.input_suitability_payload import input_suitability_to_payload | ||
|
|
||
|
|
||
| def report_to_json_payload(report: AnalysisReport) -> dict[str, object]: | ||
|
|
@@ -78,6 +79,7 @@ def report_to_json_payload(report: AnalysisReport) -> dict[str, object]: | |
| "reasons": list(report.analysis_confidence.reasons), | ||
| "limitations": list(report.analysis_confidence.limitations), | ||
| }, | ||
| "input_suitability": input_suitability_to_payload(report.input_suitability), | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
In this context, running Useful? React with 👍 / 👎. |
||
| "practice_loops": [ | ||
| { | ||
| "section_index": loop.section_index, | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
In this context,
PYTHONPATH=src pytest -q tests/unitfails intest_report_to_debug_payload_is_serializablebecauseevidence_summarynow includesinput_suitabilitybut the stable debug-payload expected value was not updated. Sincedebug_payload.jsonis treated as a stable diagnostic artifact, please update the expected payload/fixtures with this new nested field before merging.Useful? React with 👍 / 👎.