From d4eebd69e9271f33ccf78048075ce5302fad0a49 Mon Sep 17 00:00:00 2001 From: "Peter B. Johnson" Date: Thu, 10 Sep 2026 22:58:52 +0100 Subject: [PATCH] Restore granular feedback behind a show_detail parameter The per-note renderer was superseded rather than removed, leaving two renderers with no way to reach the detailed one. Reframe the pair as summary vs detail and let the caller ask for both. - generate_feedback_message -> detail_feedback, polished_feedback_message -> summary_feedback, and the same split in feedback_messages.py. - build_feedback composes them: the summary is always shown, the detail is appended when show_detail is on. SHOW_DETAIL defaults to False and is teacher-configurable through params. - detail_caveat_message introduces the detail section, so students know the per-note claims may not be accurate. - detail_feedback loses its Part 1 overview, which restated the summary. The overview_* messages go with it. - Fix the duration direction: duration_abs_diff was made absolute at the point of measurement, so the detail message always read "longer" and never "shorter". event_level_feedback now also records duration_signed_diff, and the threshold logic is unchanged. Co-Authored-By: Claude Opus 5 --- evaluation_function/compare_MIDI.py | 240 +++++++++-------------- evaluation_function/evaluation.py | 6 +- evaluation_function/evaluation_test.py | 74 ++++++- evaluation_function/feedback_messages.py | 66 ++----- notebooks/Phase1-1.5_summary.ipynb | 16 +- 5 files changed, 193 insertions(+), 209 deletions(-) diff --git a/evaluation_function/compare_MIDI.py b/evaluation_function/compare_MIDI.py index 1c17159..cbd67d0 100644 --- a/evaluation_function/compare_MIDI.py +++ b/evaluation_function/compare_MIDI.py @@ -11,15 +11,16 @@ estimate_global_duration_scale Step 3 -- event_level_feedback (note/chord-level feedback) Step 4 -- compute_stats (summary counts) - Step 5 -- version 1: generate_feedback_message (human-readable text) - version 2: polished_feedback_message (polished version of human-readable text) + Step 5 -- summary_feedback (practice-oriented summary, always shown) + detail_feedback (per-note/chord errors, shown when show_detail) + build_feedback (composes the two into the final message) """ import numpy as np from collections import Counter -# current version: feedback messages in polished_feedback_message() +# Summary report -- always shown. from .feedback_messages import ( pitch_summary_messages, timing_summary_messages, @@ -31,15 +32,9 @@ report_section_titles, report_closing_message, ) -# old version: feedback messages in generate_feedback_message() +# Detail report -- appended only when show_detail is on. from .feedback_messages import ( - overview_tempo_messages, - overview_pitch_error_messages, - overview_missing_note_messages, - overview_extra_note_messages, - overview_chord_summary_message, - overview_chord_missing_message, - overview_chord_extra_message, + detail_caveat_message, note_detail_missing_message, note_detail_extra_message, note_detail_wrong_pitch_message, @@ -51,7 +46,6 @@ chord_detail_missing_pitches_suffix, chord_detail_extra_pitches_suffix, chord_detail_timing_message, - report_overview_header, no_note_errors_message, no_chord_errors_message, ) @@ -77,6 +71,11 @@ # Default threshold: notes starting within 50ms are grouped as one chord. DEFAULT_CHORD_ONSET_WINDOW = 0.05 +# Whether to append the per-note/chord detail section to the feedback. +# Off by default: the per-note claims are only as reliable as the analysis +# behind them, which is weakest when the response was transcribed from audio. +SHOW_DETAIL = False + # template and helper functions for chords # ------------------------------------------------------------------------------ # Chord template dictionary. @@ -609,7 +608,9 @@ def event_level_feedback(operations, response_events, ref_events, "timing_abs_diff" -> float (seconds) or None "timing_relative_diff" -> float or None "duration_correct" -> bool - "duration_abs_diff" -> float (seconds) or None + "duration_abs_diff" -> float (seconds, unsigned) or None + "duration_signed_diff" -> float (seconds, positive = held too + long, negative = too short) or None "duration_relative_diff" -> float or None For chord events, each dict has: @@ -627,7 +628,9 @@ def event_level_feedback(operations, response_events, ref_events, "timing_abs_diff" -> float (seconds) or None "timing_relative_diff" -> float or None "duration_correct" -> bool - "duration_abs_diff" -> float (seconds) or None + "duration_abs_diff" -> float (seconds, unsigned) or None + "duration_signed_diff" -> float (seconds, positive = held too + long, negative = too short) or None "duration_relative_diff" -> float or None """ # Compute IOI for each reference note: ioi[m] = ref_events[m]["start"] - ref_events[m-1]["start"] @@ -666,6 +669,7 @@ def event_level_feedback(operations, response_events, ref_events, "timing_relative_diff": None, "duration_correct": False, "duration_abs_diff": None, + "duration_signed_diff": None, "duration_relative_diff": None, }) else: @@ -691,6 +695,7 @@ def event_level_feedback(operations, response_events, ref_events, "timing_relative_diff": None, "duration_correct": False, "duration_abs_diff": None, + "duration_signed_diff": None, "duration_relative_diff": None, }) else: @@ -712,7 +717,8 @@ def event_level_feedback(operations, response_events, ref_events, # Duration — residual after removing the global duration-scale trend predicted_duration = duration_scale * ref_event["event_duration"] - duration_abs_diff = abs(res_event["event_duration"] - predicted_duration) + duration_signed_diff = res_event["event_duration"] - predicted_duration + duration_abs_diff = abs(duration_signed_diff) ref_dur = max(ref_event["event_duration"], 0.05) # floor at 0.05s to avoid division by zero issues duration_relative_diff = duration_abs_diff / ref_dur duration_correct = (duration_relative_diff <= duration_relative_threshold) @@ -733,6 +739,7 @@ def event_level_feedback(operations, response_events, ref_events, "timing_relative_diff": timing_relative_diff, "duration_correct": duration_correct, "duration_abs_diff": duration_abs_diff, + "duration_signed_diff": duration_signed_diff, "duration_relative_diff": duration_relative_diff, }) else: @@ -758,6 +765,7 @@ def event_level_feedback(operations, response_events, ref_events, "timing_relative_diff": timing_relative_diff, "duration_correct": duration_correct, "duration_abs_diff": duration_abs_diff, + "duration_signed_diff": duration_signed_diff, "duration_relative_diff": duration_relative_diff, }) @@ -865,26 +873,23 @@ def compute_stats(event_level_results, ref_events, timing_scale=1.0, return stats -# Step 5 -- generate_feedback_message +# Step 5 -- detail_feedback # ------------------------------------------------------------------------------ -def generate_feedback_message(event_details, response_events, ref_events, stats, - global_slow_threshold=GLOBAL_SLOW_THRESHOLD, - global_fast_threshold=GLOBAL_FAST_THRESHOLD): +def detail_feedback(event_details, response_events, ref_events, stats): """ - Generate human-readable feedback messages for the student. + List every note and chord error individually. + + Part 1 - Note Detail: pitch, timing, duration errors per note + Part 2 - Chord Detail: errors per chord - Part 1 - Overview: summary of timing trend, duration trend, and total counts - of each error type (pitch / missing / extra). - Part 2 - Note Detail: pitch, timing, duration errors per note - Part 3 - Chord Detail: errors per chord + The overall picture is left to summary_feedback(); this function only + reports the individual errors that the summary rolls up into a score. Args: event_details: list of dicts, output of event_level_feedback() response_events: list of event dicts from group_notes_into_events ref_events: list of event dicts from group_notes_into_events stats: dict, output of compute_stats() - global_slow_threshold: timing_scale above this triggers "too slow" message - global_fast_threshold: timing_scale below this triggers "too fast" message Returns: feedback_message (str) @@ -901,114 +906,10 @@ def generate_feedback_message(event_details, response_events, ref_events, stats, if ch["operation_type"] in ("match", "replacement") ] - timing_scale = stats["timing_scale"] - timing_offset = stats["timing_offset"] - duration_scale = stats["duration_scale"] - - overview_messages = [] note_detail_messages = [] chord_detail_messages = [] - # ---------- Part 1: Overview ---------- - # Tempo: acceptable / too slow / too fast - timing_pct = abs(timing_scale - 1.0) * 100 - duration_pct = abs(duration_scale - 1.0) * 100 - if timing_scale > 1: - timing_direction = "behind" - elif timing_scale < 1: - timing_direction = "ahead of" - else: - timing_direction = "the same as" - - if duration_scale > 1: - duration_direction = "longer than" - elif duration_scale < 1: - duration_direction = "shorter than" - else: - duration_direction = "the same as" - - if timing_scale > global_slow_threshold: - overview_messages.append( - overview_tempo_messages["slow"].format( - timing_pct=timing_pct, timing_direction=timing_direction, - duration_pct=duration_pct, duration_direction=duration_direction, - ) - ) - elif timing_scale < global_fast_threshold: - overview_messages.append( - overview_tempo_messages["fast"].format( - timing_pct=timing_pct, timing_direction=timing_direction, - duration_pct=duration_pct, duration_direction=duration_direction, - ) - ) - else: - overview_messages.append( - overview_tempo_messages["acceptable"].format( - timing_pct=timing_pct, timing_direction=timing_direction, - duration_pct=duration_pct, duration_direction=duration_direction, - ) - ) - - # Wrong notes pitch counts - if stats["total_notes_wrong_pitch"] > 0: - s = "is" if stats["total_notes_wrong_pitch"] == 1 else "are" - note_word = "note" if stats["total_notes_wrong_pitch"] == 1 else "notes" - overview_messages.append( - overview_pitch_error_messages["has_errors"].format( - s=s, count=stats["total_notes_wrong_pitch"], note_word=note_word - ) - ) - else: - overview_messages.append(overview_pitch_error_messages["none"]) - # Missing notes counts - if stats["total_notes_missing"] > 0: - s = "is" if stats["total_notes_missing"] == 1 else "are" - note_word = "note" if stats["total_notes_missing"] == 1 else "notes" - overview_messages.append( - overview_missing_note_messages["has_errors"].format( - s=s, count=stats["total_notes_missing"], note_word=note_word - ) - ) - else: - overview_messages.append(overview_missing_note_messages["none"]) - # Extra notes counts - if stats["total_notes_extra"] > 0: - s = "is" if stats["total_notes_extra"] == 1 else "are" - note_word = "note" if stats["total_notes_extra"] == 1 else "notes" - overview_messages.append( - overview_extra_note_messages["has_errors"].format( - s=s, count=stats["total_notes_extra"], note_word=note_word - ) - ) - else: - overview_messages.append(overview_extra_note_messages["none"]) - # Chord errors counts - if stats["total_chords_in_reference"] > 0: - total = stats["total_chords_in_reference"] - correct = stats["total_chords_correct"] - imperfect = stats["total_chords_imperfect"] - wrong = stats["total_chords_wrong"] - overview_messages.append( - overview_chord_summary_message.format( - correct=correct, total=total, imperfect=imperfect, wrong=wrong - ) - ) - if stats["total_chords_missing"] > 0: - c_word = "chord" if stats["total_chords_missing"] == 1 else "chords" - overview_messages.append( - overview_chord_missing_message.format( - count=stats["total_chords_missing"], chord_word=c_word - ) - ) - if stats["total_chords_extra"] > 0: - c_word = "chord" if stats["total_chords_extra"] == 1 else "chords" - overview_messages.append( - overview_chord_extra_message.format( - count=stats["total_chords_extra"], chord_word=c_word - ) - ) - - # ---------- Part 2: Note Detail ---------- + # ---------- Part 1: Note Detail ---------- # Missing / extra notes for n in note_events: if n["operation_type"] == "missing": @@ -1050,16 +951,16 @@ def generate_feedback_message(event_details, response_events, ref_events, stats, # Local duration errors — these are residuals after removing the global duration trend for n in paired_notes: if not n["duration_correct"]: - direction = "longer" if n["duration_abs_diff"] > 0 else "shorter" - duration_pct_err = abs(n["duration_relative_diff"]) * 100 + direction = "longer" if n["duration_signed_diff"] > 0 else "shorter" + duration_pct_err = n["duration_relative_diff"] * 100 note_detail_messages.append( note_detail_duration_message.format( - index=n["reference_index"], abs_diff=abs(n["duration_abs_diff"]), + index=n["reference_index"], abs_diff=n["duration_abs_diff"], direction=direction, relative_pct=duration_pct_err, ) ) - # ---------- Part 3: Chord Detail ---------- + # ---------- Part 2: Chord Detail ---------- # Missing / extra chords for ch in chord_events: if ch["operation_type"] == "missing": @@ -1103,12 +1004,10 @@ def generate_feedback_message(event_details, response_events, ref_events, stats, ) ) - all_messages = [report_overview_header] + overview_messages - if note_detail_messages: - all_messages = all_messages + ["", "Note Detail:"] + note_detail_messages + all_messages = ["Note Detail:"] + note_detail_messages else: - all_messages = all_messages + ["", no_note_errors_message] + all_messages = [no_note_errors_message] if stats["total_chords_in_reference"] > 0: if chord_detail_messages: @@ -1121,10 +1020,11 @@ def generate_feedback_message(event_details, response_events, ref_events, stats, return "\n".join(all_messages) -# Current version of feedback messages -def polished_feedback_message(event_details, response_events, ref_events, stats, - global_slow_threshold=GLOBAL_SLOW_THRESHOLD, - global_fast_threshold=GLOBAL_FAST_THRESHOLD): +# Step 5 -- summary_feedback +# ------------------------------------------------------------------------------ +def summary_feedback(event_details, response_events, ref_events, stats, + global_slow_threshold=GLOBAL_SLOW_THRESHOLD, + global_fast_threshold=GLOBAL_FAST_THRESHOLD): """ Generate concise, practice-oriented feedback that aims to: 1. summarise current performance level qualitatively @@ -1299,6 +1199,49 @@ def polished_feedback_message(event_details, response_events, ref_events, stats, return "\n".join(all_messages) +# Step 5 -- build_feedback +# ------------------------------------------------------------------------------ +def build_feedback(event_details, response_events, ref_events, stats, + global_slow_threshold=GLOBAL_SLOW_THRESHOLD, + global_fast_threshold=GLOBAL_FAST_THRESHOLD, + show_detail=SHOW_DETAIL): + """ + Assemble the feedback message shown to the student. + + The summary is always included. The per-note/chord detail is appended + only when show_detail is on, because those individual claims are only + as reliable as the analysis behind them. + + Args: + event_details: list of dicts, output of event_level_feedback() + response_events: list of event dicts from group_notes_into_events + ref_events: list of event dicts from group_notes_into_events + stats: dict, output of compute_stats() + global_slow_threshold: timing_scale above this triggers "too slow" message + global_fast_threshold: timing_scale below this triggers "too fast" message + show_detail: bool, append the per-note/chord detail section. + Default False. Teacher-configurable. + + Returns: + feedback_message (str) + """ + parts = [ + summary_feedback( + event_details, response_events, ref_events, stats, + global_slow_threshold=global_slow_threshold, + global_fast_threshold=global_fast_threshold, + ) + ] + + if show_detail: + parts.append(detail_caveat_message) + parts.append( + detail_feedback(event_details, response_events, ref_events, stats) + ) + + return "\n\n".join(parts) + + # FeedbackResult class # ------------------------------------------------------------------------------ class FeedbackResult: @@ -1354,7 +1297,8 @@ def compare_performance_ED(responseMIDI, refMIDI, duration_relative_threshold=DURATION_RELATIVE_THRESHOLD, global_slow_threshold=GLOBAL_SLOW_THRESHOLD, global_fast_threshold=GLOBAL_FAST_THRESHOLD, - chord_onset_window=DEFAULT_CHORD_ONSET_WINDOW): + chord_onset_window=DEFAULT_CHORD_ONSET_WINDOW, + show_detail=SHOW_DETAIL): """ Full pipeline: normalisation -> grouping -> alignment -> global trends -> event-level evaluation -> summary statistics -> feedback. @@ -1365,10 +1309,11 @@ def compare_performance_ED(responseMIDI, refMIDI, gap_penalty: cost of an unaligned event timing_relative_threshold: see event_level_feedback() duration_relative_threshold: see event_level_feedback() - global_slow_threshold: see generate_feedback_message() - global_fast_threshold: see generate_feedback_message() + global_slow_threshold: see summary_feedback() + global_fast_threshold: see summary_feedback() chord_onset_window: float (seconds), notes within this window are grouped into a chord. Default 0.050 (50ms). Teacher-configurable. + show_detail: see build_feedback(). Default False. Teacher-configurable. Returns: FeedbackResult object containing all analysis results @@ -1410,10 +1355,11 @@ def compare_performance_ED(responseMIDI, refMIDI, ) # Step 5: Generate human-readable feedback - feedback_message = polished_feedback_message( + feedback_message = build_feedback( event_details, response_events, ref_events, stats, global_slow_threshold=global_slow_threshold, global_fast_threshold=global_fast_threshold, + show_detail=show_detail, ) # Step 6: Overall pass/fail judgement diff --git a/evaluation_function/evaluation.py b/evaluation_function/evaluation.py index a8485d9..40aa01d 100755 --- a/evaluation_function/evaluation.py +++ b/evaluation_function/evaluation.py @@ -17,7 +17,8 @@ DURATION_RELATIVE_THRESHOLD, GLOBAL_SLOW_THRESHOLD, GLOBAL_FAST_THRESHOLD, - DEFAULT_CHORD_ONSET_WINDOW + DEFAULT_CHORD_ONSET_WINDOW, + SHOW_DETAIL, ) from .audio_processing import ( is_audio_input, @@ -110,7 +111,8 @@ def evaluation_function( ), chord_onset_window=params.get( "chord_onset_window", DEFAULT_CHORD_ONSET_WINDOW - ) + ), + show_detail=params.get("show_detail", SHOW_DETAIL), ) return { diff --git a/evaluation_function/evaluation_test.py b/evaluation_function/evaluation_test.py index 9aca78e..6c9d135 100755 --- a/evaluation_function/evaluation_test.py +++ b/evaluation_function/evaluation_test.py @@ -19,6 +19,7 @@ 8. Tests for evaluation_function (Lambda Feedback integration) 9. Tests for parameter overrides 10. Bulk tests using longer MIDI sequences +11. Tests for the optional detail section (show_detail) """ @@ -43,7 +44,9 @@ GLOBAL_SLOW_THRESHOLD, GLOBAL_FAST_THRESHOLD, DEFAULT_CHORD_ONSET_WINDOW, + SHOW_DETAIL, ) +from .feedback_messages import detail_caveat_message, report_section_titles from .evaluation import evaluation_function @@ -705,4 +708,73 @@ def test_realistic_scenario(case): ] assert len(matching_notes) == 1 flagged_note = matching_notes[0] - assert flagged_note["timing_correct"] is False \ No newline at end of file + assert flagged_note["timing_correct"] is False + + +# 11. Tests for the optional detail section (show_detail) +# ------------------------------------------------------------------------------ +class TestShowDetail(unittest.TestCase): + + # A wrong pitch in the middle of an otherwise correct performance, so + # there is always exactly one thing for the detail section to report. + REF = make_midi([60, 62, 64, 65], [0, 0.5, 1.0, 1.5], [0.4] * 4) + RES = make_midi([60, 62, 63, 65], [0, 0.5, 1.0, 1.5], [0.4] * 4) + + def test_detail_is_off_by_default(self): + assert SHOW_DETAIL is False + feedback = compare_performance_ED(self.RES, self.REF).feedback_message + assert report_section_titles["summary"] in feedback + assert "Note Detail:" not in feedback + assert detail_caveat_message not in feedback + + def test_show_detail_appends_detail_below_the_summary(self): + feedback = compare_performance_ED( + self.RES, self.REF, show_detail=True + ).feedback_message + # The summary is not replaced by the detail, it is still on top. + assert report_section_titles["summary"] in feedback + assert "Note Detail:" in feedback + assert feedback.index(report_section_titles["summary"]) < feedback.index("Note Detail:") + + def test_detail_section_is_introduced_by_the_caveat(self): + feedback = compare_performance_ED( + self.RES, self.REF, show_detail=True + ).feedback_message + assert detail_caveat_message in feedback + assert feedback.index(detail_caveat_message) < feedback.index("Note Detail:") + + def test_show_detail_passed_through_params(self): + assert "Note Detail:" not in evaluation_function(self.RES, self.REF, {})["feedback"] + with_detail = evaluation_function(self.RES, self.REF, {"show_detail": True}) + assert "Note Detail:" in with_detail["feedback"] + + def test_note_held_too_short_is_reported_as_shorter(self): + # Note 3 is held for a quarter of its reference duration while every + # other note is correct, so the global duration trend stays near 1.0 + # and note 3 is left as a local error. + ref = make_midi([60, 62, 64, 65], [0, 0.5, 1.0, 1.5], [0.4] * 4) + res = make_midi([60, 62, 64, 65], [0, 0.5, 1.0, 1.5], [0.4, 0.4, 0.1, 0.4]) + result = compare_performance_ED(res, ref, show_detail=True) + + note_three = [ + n for n in result.event_details + if n["event_type"] == "note" and n.get("reference_index") == 3 + ] + assert len(note_three) == 1 + assert note_three[0]["duration_correct"] is False + assert note_three[0]["duration_signed_diff"] < 0 + assert note_three[0]["duration_abs_diff"] > 0 + assert "Note 3: duration is" in result.feedback_message + assert "shorter than the reference" in result.feedback_message + + def test_note_held_too_long_is_reported_as_longer(self): + ref = make_midi([60, 62, 64, 65], [0, 0.5, 1.0, 1.5], [0.4] * 4) + res = make_midi([60, 62, 64, 65], [0, 0.5, 1.0, 1.5], [0.4, 0.4, 1.6, 0.4]) + result = compare_performance_ED(res, ref, show_detail=True) + + note_three = [ + n for n in result.event_details + if n["event_type"] == "note" and n.get("reference_index") == 3 + ] + assert note_three[0]["duration_signed_diff"] > 0 + assert "longer than the reference" in result.feedback_message diff --git a/evaluation_function/feedback_messages.py b/evaluation_function/feedback_messages.py index 218ef8b..11763a8 100644 --- a/evaluation_function/feedback_messages.py +++ b/evaluation_function/feedback_messages.py @@ -1,12 +1,16 @@ """ feedback_messages.py ===================== -All feedback text shown to students, plain text only, -compare_MIDI.py should only decide which message to use. +All feedback text shown to students, plain text only, +compare_MIDI.py should only decide which message to use. + +Two groups, matching the two renderers in compare_MIDI.py: + - summary: shown by summary_feedback(), always part of the report. + - detail: shown by detail_feedback(), appended only when show_detail is on. """ -# Current version: feedback messages in polished_feedback_message() +# --- summary --- # ================================================================= # ---------- current performance summary (pitch / timing / chords) ---------- pitch_summary_messages = { @@ -153,54 +157,15 @@ report_closing_message = "Keep up the good work and enjoy your music journey!" -# Old version: feedback messages in generate_feedback_message() +# --- detail --- # ================================================================= -overview_tempo_messages = { - "slow": ( - "Overall, your tempo is slower than the reference " - "(timing is about {timing_pct:.0f}% {timing_direction} the reference in general while " - "notes are held about {duration_pct:.0f}% {duration_direction} the reference). " - "No worries! You will get better when you practice more to get more familiar with it!" - ), - "fast": ( - "Overall, your tempo is faster than the reference " - "(timing is about {timing_pct:.0f}% {timing_direction} the reference in general while " - "notes are held about {duration_pct:.0f}% {duration_direction} the reference). " - "Don't rush even if you are confident in your performance." - "Slow down and give each note its full value." - ), - "acceptable": ( - "Timing: your overall tempo is within an acceptable range. Good job! " - "The timing is about {timing_pct:.0f}% {timing_direction} the reference in general while " - "notes are held about {duration_pct:.0f}% {duration_direction} than the reference." - ), -} - -overview_pitch_error_messages = { - "has_errors": "There {s} {count} {note_word} played with the wrong pitch.", - "none": "There are no pitch errors. Well done!", -} - -overview_missing_note_messages = { - "has_errors": "There {s} {count} {note_word} you missed from the reference.", - "none": "There are no missing notes. Great!", -} - -overview_extra_note_messages = { - "has_errors": ( - "There {s} {count} extra {note_word} played during practice. " - "You may need to adjust your fingering or hand position to avoid extra notes." - ), - "none": "There are no extra notes. Good job!", -} - -overview_chord_summary_message = ( - "Chords: {correct}/{total} correct, " - "{imperfect}/{total} imperfect (some notes missing or extra), " - "{wrong}/{total} completely wrong." +# Shown above the detail section, so students know these per-note +# claims are only as reliable as the analysis behind them. +detail_caveat_message = ( + "The per-note comments below are generated automatically and may not " + "be accurate for every note, especially if your recording was " + "transcribed from audio." ) -overview_chord_missing_message = "{count} {chord_word} missed." -overview_chord_extra_message = "{count} extra {chord_word} played." note_detail_missing_message = "Note {index} (pitch {pitch}) is missing in your performance." note_detail_extra_message = "Extra note played: pitch {pitch} at t={time:.2f}s " @@ -231,6 +196,5 @@ "({relative_pct:.0f}% of the expected interval)." ) -report_overview_header = "Overview: " no_note_errors_message = "All melody notes played correctly!!" -no_chord_errors_message = "Great performance! No further issues on chords found." \ No newline at end of file +no_chord_errors_message = "Great performance! No further issues on chords found." diff --git a/notebooks/Phase1-1.5_summary.ipynb b/notebooks/Phase1-1.5_summary.ipynb index 212bc9a..06e557b 100644 --- a/notebooks/Phase1-1.5_summary.ipynb +++ b/notebooks/Phase1-1.5_summary.ipynb @@ -47,8 +47,8 @@ " estimate_global_duration_scale,\n", " event_level_feedback, \n", " compute_stats, \n", - " generate_feedback_message,\n", - " polished_feedback_message,\n", + " detail_feedback,\n", + " summary_feedback,\n", " compare_performance_ED,\n", ")\n", "\n", @@ -915,11 +915,11 @@ ], "source": [ "# Step 5: Generate feedback messages\n", - "# Old version: lists every error individually\n", - "old_feedback_message = generate_feedback_message(\n", + "# Detail: lists every error individually (shown when show_detail is on)\n", + "detail_message = detail_feedback(\n", " event_details, response_events, ref_events, stats,\n", ")\n", - "print(old_feedback_message)" + "print(detail_message)" ] }, { @@ -965,11 +965,11 @@ } ], "source": [ - "# New version: with integrated, practice-oriented summary (current default)\n", - "new_feedback_message = polished_feedback_message(\n", + "# Summary: integrated, practice-oriented overview (always shown)\n", + "summary_message = summary_feedback(\n", " event_details, response_events, ref_events, stats,\n", ")\n", - "print(new_feedback_message)" + "print(summary_message)" ] }, {