manoskary commited on
Commit
c74169c
ยท
1 Parent(s): dba424d

Enhance README and app.py: Add new analysis tasks and improve MusicXML handling

Browse files
Files changed (2) hide show
  1. README.md +5 -0
  2. app.py +101 -18
README.md CHANGED
@@ -25,8 +25,13 @@ A Gradio web interface for [AnalysisGNN](https://github.com/manoskary/analysisGN
25
  - Harmonic Analysis (Chord Quality, Root, Bass, Inversion)
26
  - Roman Numeral Analysis
27
  - Phrase & Section Segmentation
 
 
 
 
28
  - ๐Ÿ“ˆ **Results Table**: View analysis results in an interactive table
29
  - ๐Ÿ’พ **Export Results**: Download analysis results as CSV
 
30
 
31
  ## Quick Start
32
 
 
25
  - Harmonic Analysis (Chord Quality, Root, Bass, Inversion)
26
  - Roman Numeral Analysis
27
  - Phrase & Section Segmentation
28
+ - Harmonic Rhythm
29
+ - Pitch-Class Set Groupings
30
+ - Non-Chord Tone (TPC-in-label / NCT) Detection
31
+ - Note Degree Labeling
32
  - ๐Ÿ“ˆ **Results Table**: View analysis results in an interactive table
33
  - ๐Ÿ’พ **Export Results**: Download analysis results as CSV
34
+ - ๐Ÿงพ **Parsed Score Download**: Grab the normalized MusicXML that is produced after parsing with Partitura
35
 
36
  ## Quick Start
37
 
app.py CHANGED
@@ -23,7 +23,11 @@ warnings.filterwarnings('ignore')
23
  # Import partitura and AnalysisGNN
24
  import partitura as pt
25
  from analysisgnn.models.analysis import ContinualAnalysisGNN
26
- from analysisgnn.utils.chord_representations import available_representations
 
 
 
 
27
 
28
  # Global model variable
29
  MODEL = None
@@ -101,6 +105,41 @@ def load_model() -> ContinualAnalysisGNN:
101
  return MODEL
102
 
103
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
104
  def render_score_to_image(score: pt.score.Score, output_path: str) -> Optional[str]:
105
  """
106
  Render score to image using partitura.
@@ -140,7 +179,7 @@ def render_score_to_image(score: pt.score.Score, output_path: str) -> Optional[s
140
  except (ImportError, Exception) as e:
141
  # If conversion fails, return the PDF path
142
  print(f"PDF to PNG conversion failed: {e}")
143
- return pdf_path
144
  except Exception as e:
145
  print(f"Error rendering score to PDF: {e}")
146
 
@@ -231,7 +270,7 @@ def predict_analysis(
231
  def process_musicxml(
232
  musicxml_file,
233
  selected_tasks: list
234
- ) -> Tuple[Optional[str], Optional[pd.DataFrame], str]:
235
  """
236
  Process a MusicXML file and return visualization and analysis results.
237
 
@@ -245,15 +284,19 @@ def process_musicxml(
245
  Returns
246
  -------
247
  tuple
248
- (image_path, dataframe, status_message)
249
  """
250
  if musicxml_file is None:
251
- return None, None, "Please upload a MusicXML file."
252
 
253
  if not selected_tasks:
254
- return None, None, "Please select at least one analysis task."
255
 
256
  try:
 
 
 
 
257
  # Load the model
258
  status_msg = "Loading model..."
259
  print(status_msg)
@@ -262,7 +305,9 @@ def process_musicxml(
262
  # Load the score
263
  status_msg = "Loading score..."
264
  print(status_msg)
265
- score = pt.load_musicxml(musicxml_file.name)
 
 
266
 
267
  # Render score to image
268
  status_msg = "Rendering score..."
@@ -287,6 +332,14 @@ def process_musicxml(
287
  if 'note_id' not in df.columns:
288
  df.insert(0, 'note_id', range(len(df)))
289
 
 
 
 
 
 
 
 
 
290
  # Reorder columns to have timing info first, then predictions, then confidence
291
  timing_cols = [col for col in ['note_id', 'onset_beat', 'measure'] if col in df.columns]
292
  confidence_cols = [col for col in df.columns if col.endswith('_confidence')]
@@ -302,17 +355,32 @@ def process_musicxml(
302
 
303
  df = df[ordered_cols]
304
 
 
 
 
 
 
 
 
 
 
 
 
305
  status_msg = f"โœ“ Analysis complete! Analyzed {len(df)} notes with {len(selected_tasks)} task(s)."
 
 
306
  else:
307
  df = pd.DataFrame()
308
  status_msg = "โš  Analysis returned no predictions."
 
 
309
 
310
- return rendered_path, df, status_msg
311
 
312
  except Exception as e:
313
  error_msg = f"Error processing file: {str(e)}\n\n{traceback.format_exc()}"
314
  print(error_msg)
315
- return None, None, error_msg
316
 
317
 
318
  # Define available tasks
@@ -329,6 +397,15 @@ AVAILABLE_TASKS = {
329
  "romanNumeral": "Roman Numeral Analysis",
330
  "phrase": "Phrase Segmentation",
331
  "section": "Section Detection",
 
 
 
 
 
 
 
 
 
332
  }
333
 
334
  # Create Gradio interface
@@ -343,6 +420,8 @@ with gr.Blocks(title="AnalysisGNN Music Analysis", theme=gr.themes.Soft()) as de
343
  - Key Analysis (Local & Tonalized)
344
  - Harmonic Analysis (Chords, Inversions, Roman Numerals)
345
  - Phrase & Section Segmentation
 
 
346
 
347
  **Model:** Pre-trained AnalysisGNN from [manoskary/analysisGNN](https://github.com/manoskary/analysisGNN)
348
  """)
@@ -378,14 +457,18 @@ with gr.Blocks(title="AnalysisGNN Music Analysis", theme=gr.themes.Soft()) as de
378
  interactive=False
379
  )
380
 
381
- with gr.Row():
382
- with gr.Column():
383
- # Score visualization
384
- gr.Markdown("### ๐ŸŽผ Score Visualization")
385
- image_output = gr.Image(
386
- label="Rendered Score",
387
- type="filepath"
388
- )
 
 
 
 
389
 
390
  with gr.Row():
391
  with gr.Column():
@@ -451,7 +534,7 @@ with gr.Blocks(title="AnalysisGNN Music Analysis", theme=gr.themes.Soft()) as de
451
  analyze_btn.click(
452
  fn=analyze_wrapper,
453
  inputs=[file_input, task_selector],
454
- outputs=[image_output, table_output, status_output]
455
  )
456
 
457
  example_btn.click(
 
23
  # Import partitura and AnalysisGNN
24
  import partitura as pt
25
  from analysisgnn.models.analysis import ContinualAnalysisGNN
26
+ from analysisgnn.utils.chord_representations import available_representations, NoteDegree49
27
+
28
+ # Ensure additional representations are available for decoding
29
+ if "note_degree" not in available_representations and NoteDegree49 is not None:
30
+ available_representations["note_degree"] = NoteDegree49
31
 
32
  # Global model variable
33
  MODEL = None
 
105
  return MODEL
106
 
107
 
108
+ def resolve_musicxml_path(musicxml_file) -> Optional[str]:
109
+ """Return a filesystem path for the uploaded MusicXML file."""
110
+ if musicxml_file is None:
111
+ return None
112
+ if isinstance(musicxml_file, (str, os.PathLike)):
113
+ return str(musicxml_file)
114
+ if isinstance(musicxml_file, dict) and "name" in musicxml_file:
115
+ return musicxml_file["name"]
116
+ file_path = getattr(musicxml_file, "name", None)
117
+ if file_path:
118
+ return file_path
119
+ return None
120
+
121
+
122
+ def save_parsed_musicxml(score: pt.score.Score, original_path: Optional[str]) -> Optional[str]:
123
+ """
124
+ Persist the parsed/normalized score to a temporary MusicXML file.
125
+
126
+ Returns the path to the saved file or None if saving fails.
127
+ """
128
+ try:
129
+ suffix = ".musicxml"
130
+ if original_path:
131
+ original_suffix = Path(original_path).suffix.lower()
132
+ if original_suffix in {".xml", ".musicxml"}:
133
+ suffix = original_suffix
134
+ fd, tmp_path = tempfile.mkstemp(suffix=suffix)
135
+ os.close(fd)
136
+ pt.save_musicxml(score, tmp_path)
137
+ return tmp_path
138
+ except Exception as exc:
139
+ print(f"Warning: Could not save parsed MusicXML: {exc}")
140
+ return None
141
+
142
+
143
  def render_score_to_image(score: pt.score.Score, output_path: str) -> Optional[str]:
144
  """
145
  Render score to image using partitura.
 
179
  except (ImportError, Exception) as e:
180
  # If conversion fails, return the PDF path
181
  print(f"PDF to PNG conversion failed: {e}")
182
+ print("Score rendered as PDF but could not be converted to PNG for visualization.")
183
  except Exception as e:
184
  print(f"Error rendering score to PDF: {e}")
185
 
 
270
  def process_musicxml(
271
  musicxml_file,
272
  selected_tasks: list
273
+ ) -> Tuple[Optional[str], Optional[pd.DataFrame], Optional[str], str]:
274
  """
275
  Process a MusicXML file and return visualization and analysis results.
276
 
 
284
  Returns
285
  -------
286
  tuple
287
+ (image_path, dataframe, parsed_musicxml_path, status_message)
288
  """
289
  if musicxml_file is None:
290
+ return None, None, None, "Please upload a MusicXML file."
291
 
292
  if not selected_tasks:
293
+ return None, None, None, "Please select at least one analysis task."
294
 
295
  try:
296
+ score_path = resolve_musicxml_path(musicxml_file)
297
+ if score_path is None or not os.path.exists(score_path):
298
+ return None, None, None, "Could not locate the uploaded MusicXML file."
299
+
300
  # Load the model
301
  status_msg = "Loading model..."
302
  print(status_msg)
 
305
  # Load the score
306
  status_msg = "Loading score..."
307
  print(status_msg)
308
+ score = pt.load_musicxml(score_path)
309
+
310
+ parsed_score_path = save_parsed_musicxml(score, score_path)
311
 
312
  # Render score to image
313
  status_msg = "Rendering score..."
 
332
  if 'note_id' not in df.columns:
333
  df.insert(0, 'note_id', range(len(df)))
334
 
335
+ # Convert tpc_in_label logits into NCT-friendly labels
336
+ if 'tpc_in_label' in df.columns:
337
+ df['tpc_in_label'] = np.where(
338
+ df['tpc_in_label'].astype(int) == 0,
339
+ "NCT",
340
+ "Chord Tone"
341
+ )
342
+
343
  # Reorder columns to have timing info first, then predictions, then confidence
344
  timing_cols = [col for col in ['note_id', 'onset_beat', 'measure'] if col in df.columns]
345
  confidence_cols = [col for col in df.columns if col.endswith('_confidence')]
 
355
 
356
  df = df[ordered_cols]
357
 
358
+ # Apply user-friendly column names
359
+ rename_map = {}
360
+ for key, label in DISPLAY_NAME_OVERRIDES.items():
361
+ if key in df.columns:
362
+ rename_map[key] = label
363
+ conf_key = f"{key}_confidence"
364
+ if conf_key in df.columns:
365
+ rename_map[conf_key] = f"{label} Confidence"
366
+ if rename_map:
367
+ df = df.rename(columns=rename_map)
368
+
369
  status_msg = f"โœ“ Analysis complete! Analyzed {len(df)} notes with {len(selected_tasks)} task(s)."
370
+ if parsed_score_path:
371
+ status_msg += " Parsed MusicXML ready for download."
372
  else:
373
  df = pd.DataFrame()
374
  status_msg = "โš  Analysis returned no predictions."
375
+ if parsed_score_path:
376
+ status_msg += " Parsed MusicXML ready for download."
377
 
378
+ return rendered_path, df, parsed_score_path, status_msg
379
 
380
  except Exception as e:
381
  error_msg = f"Error processing file: {str(e)}\n\n{traceback.format_exc()}"
382
  print(error_msg)
383
+ return None, None, None, error_msg
384
 
385
 
386
  # Define available tasks
 
397
  "romanNumeral": "Roman Numeral Analysis",
398
  "phrase": "Phrase Segmentation",
399
  "section": "Section Detection",
400
+ "hrhythm": "Harmonic Rhythm",
401
+ "pcset": "Pitch-Class Set",
402
+ "tpc_in_label": "Non-Chord Tone (NCT)",
403
+ "note_degree": "Note Degree",
404
+ }
405
+
406
+ DISPLAY_NAME_OVERRIDES = {
407
+ "tpc_in_label": "NCT",
408
+ "note_degree": "Note Degree",
409
  }
410
 
411
  # Create Gradio interface
 
420
  - Key Analysis (Local & Tonalized)
421
  - Harmonic Analysis (Chords, Inversions, Roman Numerals)
422
  - Phrase & Section Segmentation
423
+ - Non-Chord Tone Detection (TPC-in-label / NCT)
424
+ - Note Degree Labeling
425
 
426
  **Model:** Pre-trained AnalysisGNN from [manoskary/analysisGNN](https://github.com/manoskary/analysisGNN)
427
  """)
 
457
  interactive=False
458
  )
459
 
460
+ with gr.Row():
461
+ with gr.Column():
462
+ # Score visualization
463
+ gr.Markdown("### ๐ŸŽผ Score Visualization")
464
+ image_output = gr.Image(
465
+ label="Rendered Score",
466
+ type="filepath"
467
+ )
468
+ parsed_score_output = gr.File(
469
+ label="Parsed MusicXML (Download)",
470
+ interactive=False
471
+ )
472
 
473
  with gr.Row():
474
  with gr.Column():
 
534
  analyze_btn.click(
535
  fn=analyze_wrapper,
536
  inputs=[file_input, task_selector],
537
+ outputs=[image_output, table_output, parsed_score_output, status_output]
538
  )
539
 
540
  example_btn.click(