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)" ] }, {