manoskary commited on
Commit
b57c33d
Ā·
1 Parent(s): c850e33

Add initial implementation of AnalysisGNN Gradio app with model integration and requirements

Browse files
Files changed (5) hide show
  1. .gitignore +215 -0
  2. README.md +91 -3
  3. app.py +488 -0
  4. checkpoint/model.ckpt +3 -0
  5. requirements.txt +38 -0
.gitignore ADDED
@@ -0,0 +1,215 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Byte-compiled / optimized / DLL files
2
+ __pycache__/
3
+ *.py[codz]
4
+ *$py.class
5
+
6
+ # C extensions
7
+ *.so
8
+
9
+ # Distribution / packaging
10
+ .Python
11
+ build/
12
+ develop-eggs/
13
+ dist/
14
+ downloads/
15
+ eggs/
16
+ .eggs/
17
+ lib/
18
+ lib64/
19
+ parts/
20
+ sdist/
21
+ var/
22
+ wheels/
23
+ share/python-wheels/
24
+ *.egg-info/
25
+ .installed.cfg
26
+ *.egg
27
+ MANIFEST
28
+
29
+ # PyInstaller
30
+ # Usually these files are written by a python script from a template
31
+ # before PyInstaller builds the exe, so as to inject date/other infos into it.
32
+ *.manifest
33
+ *.spec
34
+
35
+ # Installer logs
36
+ pip-log.txt
37
+ pip-delete-this-directory.txt
38
+
39
+ # Unit test / coverage reports
40
+ htmlcov/
41
+ .tox/
42
+ .nox/
43
+ .coverage
44
+ .coverage.*
45
+ .cache
46
+ nosetests.xml
47
+ coverage.xml
48
+ *.cover
49
+ *.py.cover
50
+ .hypothesis/
51
+ .pytest_cache/
52
+ cover/
53
+
54
+ # Translations
55
+ *.mo
56
+ *.pot
57
+
58
+ # Django stuff:
59
+ *.log
60
+ local_settings.py
61
+ db.sqlite3
62
+ db.sqlite3-journal
63
+
64
+ # Flask stuff:
65
+ instance/
66
+ .webassets-cache
67
+
68
+ # Scrapy stuff:
69
+ .scrapy
70
+
71
+ # Sphinx documentation
72
+ docs/_build/
73
+
74
+ # PyBuilder
75
+ .pybuilder/
76
+ target/
77
+
78
+ # Jupyter Notebook
79
+ .ipynb_checkpoints
80
+
81
+ # IPython
82
+ profile_default/
83
+ ipython_config.py
84
+
85
+ # pyenv
86
+ # For a library or package, you might want to ignore these files since the code is
87
+ # intended to run in multiple environments; otherwise, check them in:
88
+ # .python-version
89
+
90
+ # pipenv
91
+ # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
92
+ # However, in case of collaboration, if having platform-specific dependencies or dependencies
93
+ # having no cross-platform support, pipenv may install dependencies that don't work, or not
94
+ # install all needed dependencies.
95
+ #Pipfile.lock
96
+
97
+ # UV
98
+ # Similar to Pipfile.lock, it is generally recommended to include uv.lock in version control.
99
+ # This is especially recommended for binary packages to ensure reproducibility, and is more
100
+ # commonly ignored for libraries.
101
+ #uv.lock
102
+
103
+ # poetry
104
+ # Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control.
105
+ # This is especially recommended for binary packages to ensure reproducibility, and is more
106
+ # commonly ignored for libraries.
107
+ # https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control
108
+ #poetry.lock
109
+ #poetry.toml
110
+
111
+ # pdm
112
+ # Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control.
113
+ # pdm recommends including project-wide configuration in pdm.toml, but excluding .pdm-python.
114
+ # https://pdm-project.org/en/latest/usage/project/#working-with-version-control
115
+ #pdm.lock
116
+ #pdm.toml
117
+ .pdm-python
118
+ .pdm-build/
119
+
120
+ # pixi
121
+ # Similar to Pipfile.lock, it is generally recommended to include pixi.lock in version control.
122
+ #pixi.lock
123
+ # Pixi creates a virtual environment in the .pixi directory, just like venv module creates one
124
+ # in the .venv directory. It is recommended not to include this directory in version control.
125
+ .pixi
126
+
127
+ # PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm
128
+ __pypackages__/
129
+
130
+ # Celery stuff
131
+ celerybeat-schedule
132
+ celerybeat.pid
133
+
134
+ # SageMath parsed files
135
+ *.sage.py
136
+
137
+ # Environments
138
+ .env
139
+ .envrc
140
+ .venv
141
+ env/
142
+ venv/
143
+ ENV/
144
+ env.bak/
145
+ venv.bak/
146
+
147
+ # Spyder project settings
148
+ .spyderproject
149
+ .spyproject
150
+
151
+ # Rope project settings
152
+ .ropeproject
153
+
154
+ # mkdocs documentation
155
+ /site
156
+
157
+ # mypy
158
+ .mypy_cache/
159
+ .dmypy.json
160
+ dmypy.json
161
+
162
+ # Pyre type checker
163
+ .pyre/
164
+
165
+ # pytype static type analyzer
166
+ .pytype/
167
+
168
+ # Cython debug symbols
169
+ cython_debug/
170
+
171
+ # PyCharm
172
+ # JetBrains specific template is maintained in a separate JetBrains.gitignore that can
173
+ # be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore
174
+ # and can be added to the global gitignore or merged into this file. For a more nuclear
175
+ # option (not recommended) you can uncomment the following to ignore the entire idea folder.
176
+ #.idea/
177
+
178
+ # Abstra
179
+ # Abstra is an AI-powered process automation framework.
180
+ # Ignore directories containing user credentials, local state, and settings.
181
+ # Learn more at https://abstra.io/docs
182
+ .abstra/
183
+
184
+ # Visual Studio Code
185
+ # Visual Studio Code specific template is maintained in a separate VisualStudioCode.gitignore
186
+ # that can be found at https://github.com/github/gitignore/blob/main/Global/VisualStudioCode.gitignore
187
+ # and can be added to the global gitignore or merged into this file. However, if you prefer,
188
+ # you could uncomment the following to ignore the entire vscode folder
189
+ # .vscode/
190
+
191
+ # Ruff stuff:
192
+ .ruff_cache/
193
+
194
+ # PyPI configuration file
195
+ .pypirc
196
+
197
+ # Cursor
198
+ # Cursor is an AI-powered code editor. `.cursorignore` specifies files/directories to
199
+ # exclude from AI features like autocomplete and code analysis. Recommended for sensitive data
200
+ # refer to https://docs.cursor.com/context/ignore-files
201
+ .cursorignore
202
+ .cursorindexingignore
203
+
204
+ # Marimo
205
+ marimo/_static/
206
+ marimo/_lsp/
207
+ __marimo__/
208
+
209
+ # AnalysisGNN artifacts
210
+ artifacts/
211
+ outputs/
212
+
213
+ # Gradio cache
214
+ gradio_cached_examples/
215
+ flagged/
README.md CHANGED
@@ -1,13 +1,101 @@
1
  ---
2
- title: Analysisgnn
3
- emoji: šŸŒ
4
  colorFrom: red
5
  colorTo: pink
6
  sdk: gradio
7
  sdk_version: 5.49.1
8
  app_file: app.py
9
  pinned: false
 
10
  short_description: Inference for the AnalysisGNN score analysis model
11
  ---
12
 
13
- Check out the configuration reference at https://huggingface.co/docs/hub/spaces-config-reference
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
  ---
2
+ title: AnalysisGNN Music Analysis
3
+ emoji: šŸŽµ
4
  colorFrom: red
5
  colorTo: pink
6
  sdk: gradio
7
  sdk_version: 5.49.1
8
  app_file: app.py
9
  pinned: false
10
+ license: mit
11
  short_description: Inference for the AnalysisGNN score analysis model
12
  ---
13
 
14
+ # AnalysisGNN Gradio Interface
15
+
16
+ A Gradio web interface for [AnalysisGNN](https://github.com/manoskary/analysisGNN), a unified music analysis model using Graph Neural Networks.
17
+
18
+ ## Features
19
+
20
+ - šŸŽ¼ **MusicXML Upload**: Upload and analyze musical scores in MusicXML format
21
+ - šŸŽØ **Score Visualization**: Automatic rendering of uploaded scores to images
22
+ - šŸ“Š **Multi-task Analysis**: Perform various music analysis tasks:
23
+ - Cadence Detection
24
+ - Key Analysis (Local & Tonalized)
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
+
33
+ ### Local Installation
34
+
35
+ ```bash
36
+ # Clone the repository
37
+ git clone https://github.com/manoskary/analysisgnn-gradio.git
38
+ cd analysisgnn-gradio
39
+
40
+ # Create a virtual environment (recommended)
41
+ python -m venv venv
42
+ source venv/bin/activate # On Windows: venv\Scripts\activate
43
+
44
+ # Install dependencies
45
+ pip install -r requirements.txt
46
+
47
+ # Run the app
48
+ python app.py
49
+ ```
50
+
51
+ The app will be available at `http://localhost:7860`
52
+
53
+ ### Hugging Face Spaces
54
+
55
+ This app is designed to run on Hugging Face Spaces. Simply deploy it as a Gradio Space.
56
+
57
+ ## Usage
58
+
59
+ 1. **Upload a MusicXML file** using the file upload button
60
+ 2. **Select analysis tasks** you want to perform (cadence, key, harmony, etc.)
61
+ 3. **Click "Analyze Score"** to run the inference
62
+ 4. **View results**:
63
+ - Score visualization (rendered image)
64
+ - Analysis results table (note-level predictions)
65
+ 5. **Download results** as CSV if needed
66
+
67
+ ## Model
68
+
69
+ The app uses a pre-trained AnalysisGNN model automatically downloaded from Weights & Biases. The model is cached in the `./artifacts/` folder to avoid re-downloading.
70
+
71
+ ## Dependencies
72
+
73
+ - `analysisgnn`: Core music analysis library
74
+ - `gradio`: Web interface framework
75
+ - `partitura`: Music processing library
76
+ - `torch`: Deep learning framework
77
+ - `pandas`: Data manipulation
78
+ - See `requirements.txt` for complete list
79
+
80
+ ## Citation
81
+
82
+ If you use this interface or AnalysisGNN in your research, please cite:
83
+
84
+ ```bibtex
85
+ @inproceedings{karystinaios2024analysisgnn,
86
+ title={AnalysisGNN: A Unified Music Analysis Model with Graph Neural Networks},
87
+ author={Karystinaios, Emmanouil and Hentschel, Johannes and Neuwirth, Markus and Widmer, Gerhard},
88
+ booktitle={International Symposium on Computer Music Multidisciplinary Research (CMMR)},
89
+ year={2025}
90
+ }
91
+ ```
92
+
93
+ ## License
94
+
95
+ MIT License - See the [AnalysisGNN repository](https://github.com/manoskary/analysisGNN) for more details.
96
+
97
+ ## Acknowledgments
98
+
99
+ - Built with [Gradio](https://gradio.app/)
100
+ - Powered by [AnalysisGNN](https://github.com/manoskary/analysisGNN)
101
+ - Music processing with [Partitura](https://github.com/CPJKU/partitura)
app.py ADDED
@@ -0,0 +1,488 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/env python3
2
+ """
3
+ AnalysisGNN Gradio App
4
+
5
+ A Gradio interface for AnalysisGNN music analysis.
6
+ Users can upload MusicXML scores, run the model, and view results.
7
+ """
8
+
9
+ import gradio as gr
10
+ import pandas as pd
11
+ import numpy as np
12
+ import os
13
+ import tempfile
14
+ import torch
15
+ from pathlib import Path
16
+ from typing import Tuple, Optional, Dict
17
+ import traceback
18
+ import warnings
19
+
20
+ # Suppress warnings for cleaner output
21
+ warnings.filterwarnings('ignore')
22
+
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
30
+ DEVICE = "cuda" if torch.cuda.is_available() else "cpu"
31
+
32
+ print(f"Using device: {DEVICE}")
33
+ if torch.cuda.is_available():
34
+ print(f"CUDA device: {torch.cuda.get_device_name(0)}")
35
+
36
+
37
+ def download_wandb_checkpoint(artifact_path: str = "melkisedeath/AnalysisGNN/model-uvj2ddun:v1") -> str:
38
+ """Download checkpoint from Weights & Biases, or use cached version if available."""
39
+ # Create artifacts directory structure
40
+ artifacts_dir = "checkpoint"
41
+ os.makedirs(artifacts_dir, exist_ok=True)
42
+
43
+ # Check if checkpoint already exists directly in artifacts/models
44
+ checkpoint_path = os.path.join(artifacts_dir, "model.ckpt")
45
+ if os.path.exists(checkpoint_path):
46
+ print(f"Using cached checkpoint: {checkpoint_path}")
47
+ return checkpoint_path
48
+
49
+ # Check for any .ckpt file in the artifacts/models directory
50
+ if os.path.exists(artifacts_dir):
51
+ for fname in os.listdir(artifacts_dir):
52
+ if fname.endswith('.ckpt'):
53
+ checkpoint_path = os.path.join(artifacts_dir, fname)
54
+ print(f"Using cached checkpoint: {checkpoint_path}")
55
+ return checkpoint_path
56
+
57
+ # Check artifact-specific subdirectory
58
+ artifact_dir = os.path.join(artifacts_dir, os.path.basename(artifact_path))
59
+ checkpoint_path = os.path.join(artifact_dir, "model.ckpt")
60
+ if os.path.exists(checkpoint_path):
61
+ print(f"Using cached checkpoint: {checkpoint_path}")
62
+ return checkpoint_path
63
+
64
+ # Only import and use wandb if checkpoint is not cached
65
+ import wandb
66
+ print(f"Downloading checkpoint from W&B: {artifact_path}")
67
+
68
+ # Initialize wandb in offline mode to avoid creating online runs
69
+ run = wandb.init(mode="offline")
70
+ try:
71
+ artifact = run.use_artifact(artifact_path, type='model')
72
+ artifact_dir = artifact.download(root=artifacts_dir)
73
+ finally:
74
+ wandb.finish()
75
+
76
+ # Find the checkpoint file
77
+ checkpoint_path = os.path.join(artifact_dir, "model.ckpt")
78
+ if not os.path.exists(checkpoint_path):
79
+ for fname in os.listdir(artifact_dir):
80
+ if fname.endswith('.ckpt'):
81
+ checkpoint_path = os.path.join(artifact_dir, fname)
82
+ break
83
+
84
+ return checkpoint_path
85
+
86
+
87
+ def load_model() -> ContinualAnalysisGNN:
88
+ """Load the AnalysisGNN model."""
89
+ global MODEL
90
+
91
+ if MODEL is None:
92
+ checkpoint_path = download_wandb_checkpoint()
93
+ print(f"Loading model from: {checkpoint_path}")
94
+ MODEL = ContinualAnalysisGNN.load_from_checkpoint(
95
+ checkpoint_path,
96
+ map_location=DEVICE
97
+ )
98
+ MODEL.eval()
99
+ MODEL.to(DEVICE)
100
+ print("Model loaded successfully!")
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.
107
+
108
+ Parameters
109
+ ----------
110
+ score : pt.score.Score
111
+ The score to render
112
+ output_path : str
113
+ Path to save the rendered image
114
+
115
+ Returns
116
+ -------
117
+ str or None
118
+ Path to the rendered image, or None if rendering failed
119
+ """
120
+ try:
121
+ # Try to render to PNG using partitura
122
+ pt.render(score, fmt="png", out=output_path)
123
+ if os.path.exists(output_path):
124
+ return output_path
125
+ except Exception as e:
126
+ print(f"Error rendering score to PNG: {e}")
127
+
128
+ # If PNG rendering failed, try PDF
129
+ try:
130
+ pdf_path = output_path.replace('.png', '.pdf')
131
+ pt.render(score, fmt="pdf", out=pdf_path)
132
+ if os.path.exists(pdf_path):
133
+ # Convert PDF to PNG if possible
134
+ try:
135
+ from pdf2image import convert_from_path
136
+ images = convert_from_path(pdf_path)
137
+ if images:
138
+ images[0].save(output_path, 'PNG')
139
+ return output_path
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
+
147
+ return None
148
+
149
+
150
+ def predict_analysis(
151
+ model: ContinualAnalysisGNN,
152
+ score: pt.score.Score,
153
+ tasks: list
154
+ ) -> Dict[str, np.ndarray]:
155
+ """
156
+ Perform music analysis prediction.
157
+
158
+ Parameters
159
+ ----------
160
+ model : ContinualAnalysisGNN
161
+ The model to use for prediction
162
+ score : pt.score.Score
163
+ The score to analyze
164
+ tasks : list
165
+ List of analysis tasks to perform
166
+
167
+ Returns
168
+ -------
169
+ dict
170
+ Dictionary mapping task names to predictions and confidence scores
171
+ """
172
+ with torch.no_grad():
173
+ # Get predictions from model
174
+ predictions = model.predict(score)
175
+
176
+ # Decode predictions
177
+ decoded_predictions = {}
178
+ for task in tasks:
179
+ if task in predictions:
180
+ pred_tensor = predictions[task]
181
+ if len(pred_tensor.shape) > 1:
182
+ # Get confidence scores (probabilities)
183
+ pred_probs = torch.softmax(pred_tensor, dim=-1)
184
+ pred_onehot = torch.argmax(pred_tensor, dim=-1)
185
+ # Get confidence for the predicted class
186
+ confidence = torch.max(pred_probs, dim=-1)[0]
187
+
188
+ # Store confidence scores
189
+ decoded_predictions[f"{task}_confidence"] = confidence.cpu().numpy()
190
+ else:
191
+ pred_onehot = pred_tensor
192
+
193
+ # Decode using available representations
194
+ if task in available_representations:
195
+ try:
196
+ decoded = available_representations[task].decode(
197
+ pred_onehot.reshape(-1, 1)
198
+ )
199
+ # Convert to numpy array if it's a list
200
+ if isinstance(decoded, list):
201
+ decoded_predictions[task] = np.array(decoded).flatten()
202
+ else:
203
+ decoded_predictions[task] = decoded.flatten()
204
+ except (IndexError, ValueError) as e:
205
+ print(f"Warning: Error decoding {task} predictions: {e}")
206
+ # Fallback to raw indices
207
+ decoded_predictions[task] = pred_onehot.cpu().numpy()
208
+ else:
209
+ decoded_predictions[task] = pred_onehot.cpu().numpy()
210
+
211
+ # Add timing information
212
+ try:
213
+ if "onset" in predictions:
214
+ decoded_predictions["onset_beat"] = predictions["onset"].cpu().numpy()
215
+ else:
216
+ decoded_predictions["onset_beat"] = score.note_array()["onset_beat"]
217
+ except (AttributeError, KeyError, IndexError) as e:
218
+ print(f"Warning: Could not add onset timing: {e}")
219
+
220
+ try:
221
+ if "s_measure" in predictions:
222
+ decoded_predictions["measure"] = predictions["s_measure"].cpu().numpy()
223
+ else:
224
+ decoded_predictions["measure"] = score[0].measure_number_map(score.note_array()["onset_div"])
225
+ except (AttributeError, KeyError, IndexError) as e:
226
+ print(f"Warning: Could not add measure information: {e}")
227
+
228
+ return decoded_predictions
229
+
230
+
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
+
238
+ Parameters
239
+ ----------
240
+ musicxml_file : file
241
+ Uploaded MusicXML file
242
+ selected_tasks : list
243
+ List of selected analysis tasks
244
+
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)
260
+ model = load_model()
261
+
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..."
269
+ print(status_msg)
270
+ with tempfile.NamedTemporaryFile(suffix='.png', delete=False) as tmp_img:
271
+ img_path = tmp_img.name
272
+
273
+ rendered_path = render_score_to_image(score, img_path)
274
+ if rendered_path is None:
275
+ print("Note: Score rendering failed. This requires MuseScore or LilyPond to be installed.")
276
+
277
+ # Perform analysis
278
+ status_msg = "Running analysis..."
279
+ print(status_msg)
280
+ predictions = predict_analysis(model, score, selected_tasks)
281
+
282
+ # Create DataFrame
283
+ if predictions:
284
+ df = pd.DataFrame(predictions)
285
+
286
+ # Add note/event IDs
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')]
293
+ prediction_cols = [col for col in df.columns if col not in timing_cols and col not in confidence_cols]
294
+
295
+ # Interleave predictions with their confidence scores
296
+ ordered_cols = timing_cols.copy()
297
+ for pred_col in prediction_cols:
298
+ ordered_cols.append(pred_col)
299
+ conf_col = f"{pred_col}_confidence"
300
+ if conf_col in confidence_cols:
301
+ ordered_cols.append(conf_col)
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
319
+ AVAILABLE_TASKS = {
320
+ "cadence": "Cadence Detection",
321
+ "localkey": "Local Key",
322
+ "tonkey": "Tonalized Key",
323
+ "quality": "Chord Quality",
324
+ "root": "Chord Root",
325
+ "bass": "Bass Note",
326
+ "inversion": "Chord Inversion",
327
+ "degree1": "Primary Degree",
328
+ "degree2": "Secondary Degree",
329
+ "romanNumeral": "Roman Numeral Analysis",
330
+ "phrase": "Phrase Segmentation",
331
+ "section": "Section Detection",
332
+ }
333
+
334
+ # Create Gradio interface
335
+ with gr.Blocks(title="AnalysisGNN Music Analysis", theme=gr.themes.Soft()) as demo:
336
+ gr.Markdown("""
337
+ # šŸŽµ AnalysisGNN Music Analysis
338
+
339
+ Upload a MusicXML score to perform automatic music analysis using Graph Neural Networks.
340
+
341
+ **Supported Analysis Tasks:**
342
+ - Cadence Detection
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
+ """)
349
+
350
+ with gr.Row():
351
+ with gr.Column(scale=1):
352
+ # Input section
353
+ gr.Markdown("### šŸ“ Input")
354
+ file_input = gr.File(
355
+ label="Upload MusicXML Score",
356
+ file_types=[".musicxml", ".xml", ".mxl"],
357
+ type="filepath"
358
+ )
359
+
360
+ task_selector = gr.CheckboxGroup(
361
+ choices=list(AVAILABLE_TASKS.values()),
362
+ value=["Cadence Detection", "Local Key", "Roman Numeral Analysis"],
363
+ label="Select Analysis Tasks",
364
+ info="Choose which tasks to perform"
365
+ )
366
+
367
+ analyze_btn = gr.Button("šŸŽ¼ Analyze Score", variant="primary", size="lg")
368
+
369
+ gr.Markdown("---")
370
+ example_btn = gr.Button("šŸŽµ Try Example (Mozart K.158)", size="sm")
371
+
372
+ with gr.Column(scale=2):
373
+ # Output section
374
+ gr.Markdown("### šŸ“Š Results")
375
+ status_output = gr.Textbox(
376
+ label="Status",
377
+ lines=2,
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():
392
+ # Analysis results table
393
+ gr.Markdown("### šŸ“ˆ Analysis Results")
394
+ table_output = gr.Dataframe(
395
+ label="Analysis Output",
396
+ wrap=True,
397
+ interactive=False
398
+ )
399
+
400
+ download_btn = gr.Button("šŸ’¾ Download Results as CSV")
401
+ csv_output = gr.File(label="Download CSV")
402
+
403
+ # Example section
404
+ gr.Markdown("""
405
+ ### šŸ’” Tips & Information
406
+
407
+ **Getting Started:**
408
+ - Click "Try Example" to load a Mozart quartet, or upload your own MusicXML file
409
+ - Select the analysis tasks you're interested in
410
+ - Click "Analyze Score" to run the model
411
+
412
+ **Analysis Output:**
413
+ The table shows note-level predictions for all selected tasks:
414
+ - **Onset & Measure**: Timing information
415
+ - **Keys**: Detected key areas (local and tonalized)
416
+ - **Chords**: Harmonic analysis with Roman numerals
417
+ - **Cadences**: Identified cadence points and types
418
+
419
+ **Score Visualization:**
420
+ Requires MuseScore or LilyPond for rendering. If unavailable, analysis will still work.
421
+ """)
422
+
423
+ # Event handlers
424
+ def analyze_wrapper(file, tasks_selected):
425
+ # Convert task names back to internal names
426
+ task_mapping = {v: k for k, v in AVAILABLE_TASKS.items()}
427
+ selected_task_keys = [task_mapping[t] for t in tasks_selected if t in task_mapping]
428
+ return process_musicxml(file, selected_task_keys)
429
+
430
+ def load_example():
431
+ """Load example Mozart score."""
432
+ import urllib.request
433
+
434
+ url = "https://raw.githubusercontent.com/manoskary/humdrum-mozart-quartets/refs/heads/master/musicxml/k158-01.musicxml"
435
+
436
+ # Create artifacts directory if it doesn't exist
437
+ os.makedirs("./artifacts", exist_ok=True)
438
+
439
+ example_path = "./artifacts/k158-01.musicxml"
440
+
441
+ if not os.path.exists(example_path):
442
+ try:
443
+ print(f"Downloading example score from: {url}")
444
+ urllib.request.urlretrieve(url, example_path)
445
+ print(f"Example score saved to: {example_path}")
446
+ except Exception as e:
447
+ return None, f"Error downloading example: {e}"
448
+
449
+ return example_path, "Example loaded! Click 'Analyze Score' to proceed."
450
+
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(
458
+ fn=load_example,
459
+ outputs=[file_input, status_output]
460
+ )
461
+
462
+ def save_csv(df):
463
+ if df is None or len(df) == 0:
464
+ return None
465
+ with tempfile.NamedTemporaryFile(mode='w', suffix='.csv', delete=False) as tmp:
466
+ df.to_csv(tmp.name, index=False)
467
+ return tmp.name
468
+
469
+ download_btn.click(
470
+ fn=save_csv,
471
+ inputs=[table_output],
472
+ outputs=[csv_output]
473
+ )
474
+
475
+ # Launch the app
476
+ if __name__ == "__main__":
477
+ # Pre-load the model at startup for efficiency
478
+ print("=" * 50)
479
+ print("Initializing AnalysisGNN app...")
480
+ print("=" * 50)
481
+ print("Pre-loading model at startup...")
482
+ load_model()
483
+ print("āœ“ Model loaded successfully!")
484
+
485
+ print("=" * 50)
486
+ print("Starting Gradio interface...")
487
+ print("=" * 50)
488
+ demo.launch()
checkpoint/model.ckpt ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:53d106038fea6a5ab3d4c0a19617736dff5f0299deffaf240ebd484c11f91c67
3
+ size 126890023
requirements.txt ADDED
@@ -0,0 +1,38 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # PyTorch (install first, required by other packages)
2
+ torch>=2.0.0
3
+ torchvision>=0.15.0
4
+
5
+ # PyTorch extensions (require torch to be installed)
6
+ pytorch-lightning>=2.0.0
7
+ torch-geometric>=2.3.0
8
+ torch-scatter>=2.1.0
9
+ torch-sparse>=0.6.0
10
+
11
+ # Data processing
12
+ numpy>=1.21.0
13
+ pandas>=1.5.0
14
+
15
+ # Music processing
16
+ partitura>=1.4.0
17
+ music21>=8.0.0
18
+
19
+ # Gradio and Spaces
20
+ gradio>=4.0.0
21
+
22
+ # Wandb for model loading
23
+ wandb>=0.13.0
24
+
25
+ # PDF rendering and image processing
26
+ reportlab>=4.0.0
27
+ pdf2image>=1.16.0
28
+ Pillow>=9.0.0
29
+
30
+ # ML utilities
31
+ scikit-learn>=1.1.0
32
+ torchmetrics>=0.11.0
33
+
34
+ # Other utilities
35
+ tqdm>=4.64.0
36
+
37
+ # Core AnalysisGNN dependency (install last, after all dependencies)
38
+ git+https://github.com/manoskary/analysisGNN.git