judith.ibanez commited on
Commit
11a8c1f
·
1 Parent(s): 43f7000

add some modifications

Browse files
Files changed (4) hide show
  1. Dockerfile +7 -5
  2. README.md +3 -2
  3. app.py +382 -26
  4. requirements.txt +3 -0
Dockerfile CHANGED
@@ -10,7 +10,6 @@ RUN rm -rf /var/lib/apt/lists/* && \
10
  apt-get clean
11
 
12
  ## Download and install Audiveris deb package once patched
13
-
14
  RUN wget https://github.com/Audiveris/audiveris/releases/download/5.6.1/Audiveris-5.6.1-ubuntu24.04-x86_64.deb
15
 
16
  ## Patch the deb package to work without GUI
@@ -27,13 +26,14 @@ RUN dpkg-deb -b /tmp/audiveris /tmp/audiveris-fixed.deb
27
  # Install the modified deb
28
  RUN apt-get install --fix-missing -y /tmp/audiveris-fixed.deb || apt-get install --fix-missing -y -f
29
 
30
-
31
  RUN rm Audiveris-5.6.1-ubuntu24.04-x86_64.deb
32
 
33
- ## Install Gradio MCP
34
  RUN apt-get install --fix-missing -y python3 python3-pip || apt-get install -y -f
35
- # TODO: use a python virtual environment
36
- RUN pip install --break-system-packages gradio[mcp]
 
 
37
 
38
  ## Clean
39
  RUN apt-get clean
@@ -42,4 +42,6 @@ RUN apt-get clean
42
  COPY app.py /app/app.py
43
  WORKDIR /app
44
 
 
 
45
  CMD ["python3", "app.py"]
 
10
  apt-get clean
11
 
12
  ## Download and install Audiveris deb package once patched
 
13
  RUN wget https://github.com/Audiveris/audiveris/releases/download/5.6.1/Audiveris-5.6.1-ubuntu24.04-x86_64.deb
14
 
15
  ## Patch the deb package to work without GUI
 
26
  # Install the modified deb
27
  RUN apt-get install --fix-missing -y /tmp/audiveris-fixed.deb || apt-get install --fix-missing -y -f
28
 
 
29
  RUN rm Audiveris-5.6.1-ubuntu24.04-x86_64.deb
30
 
31
+ ## Install Python and dependencies
32
  RUN apt-get install --fix-missing -y python3 python3-pip || apt-get install -y -f
33
+
34
+ RUN pip install --break-system-packages \
35
+ gradio[mcp] \
36
+ python-multipart
37
 
38
  ## Clean
39
  RUN apt-get clean
 
42
  COPY app.py /app/app.py
43
  WORKDIR /app
44
 
45
+ EXPOSE 7860
46
+
47
  CMD ["python3", "app.py"]
README.md CHANGED
@@ -1,6 +1,6 @@
1
  ---
2
  title: Scores
3
- emoji: 💻
4
  colorFrom: pink
5
  colorTo: red
6
  sdk: docker
@@ -10,6 +10,7 @@ pinned: false
10
  license: apache-2.0
11
  short_description: Analyze music scores from PDF files
12
  app_port: 7860
 
13
  ---
14
 
15
- Check out the configuration reference at https://huggingface.co/docs/hub/spaces-config-reference
 
1
  ---
2
  title: Scores
3
+ emoji: 🎼
4
  colorFrom: pink
5
  colorTo: red
6
  sdk: docker
 
10
  license: apache-2.0
11
  short_description: Analyze music scores from PDF files
12
  app_port: 7860
13
+ tags: [mcp-server-track]
14
  ---
15
 
16
+ Check out the configuration reference at <https://huggingface.co/docs/hub/spaces-config-reference>
app.py CHANGED
@@ -1,38 +1,394 @@
1
  import gradio as gr
2
  import subprocess
3
  import os
 
 
 
 
 
4
 
5
- def recognize_music(pdf_file):
6
- """Converts a PDF file to a Music score
7
-
8
- Args:
9
- pdf_file: the file with the PDF file
10
-
11
- Returns:
12
- the path were the MusicXML file is generated
13
- """
14
- audiveris = "/opt/audiveris/bin/Audiveris"
15
- output_dir = "/tmp/output"
16
- pdf_file_path = pdf_file.name
17
- pdf_file_name = os.path.basename(pdf_file.name)
18
- musicXml_file_name = os.path.splitext(pdf_file_name)[0]+".mxl"
19
- output_file = output_dir + "/" + musicXml_file_name
20
-
21
- cmd = [
22
- audiveris, "-batch", "-export", "-output", output_dir, pdf_file_path
23
- ]
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
24
 
25
- result = subprocess.run(cmd, capture_output=True, text=True)
 
 
26
 
27
- print (f"Done. Music score file saved to {output_file}\n\n{result.stdout}\n{result.stderr}")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
28
 
29
- return output_file
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
30
 
31
- audiveris = gr.Interface(
32
- fn=recognize_music,
 
33
  inputs=gr.File(file_types=[".pdf"], label="Upload PDF music score"),
34
  outputs=gr.File(label="Download MusicXML file"),
35
- description="Upload a PDF music socre and create a MusicXML file from it.",
 
36
  )
37
- audiveris.launch(server_name="0.0.0.0", mcp_server=True)
38
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
  import gradio as gr
2
  import subprocess
3
  import os
4
+ import base64
5
+ from typing import List, Dict, Any
6
+ import threading
7
+ from pathlib import Path
8
+ from datetime import datetime
9
 
10
+ class MusicRecognitionMCPServer:
11
+ def __init__(self, allowed_directories: List[str] = None):
12
+ """Initialize MCP Server with configurable file access"""
13
+ self.allowed_directories = allowed_directories or ["/tmp", "uploads", "output"]
14
+ self.processed_files = {} # Cache of processed results
15
+
16
+ # Convert relative paths to absolute and check access
17
+ abs_directories = []
18
+ for directory in self.allowed_directories:
19
+ # Convert relative paths to absolute
20
+ if not os.path.isabs(directory):
21
+ directory = os.path.abspath(directory)
22
+
23
+ abs_directories.append(directory)
24
+ print(f"Checking directory: {directory}")
25
+
26
+ # Only try to create if it doesn't exist and we have permission
27
+ if not os.path.exists(directory):
28
+ try:
29
+ os.makedirs(directory, exist_ok=True)
30
+ os.chmod(directory, 0o755)
31
+ print(f"✅ Directory created: {directory}")
32
+ except PermissionError:
33
+ print(f"⚠️ Directory doesn't exist but can't create: {directory}")
34
+ print(f" This is normal for system directories like /tmp")
35
+ except Exception as e:
36
+ print(f"⚠️ Warning creating {directory}: {e}")
37
+ else:
38
+ print(f"✅ Directory exists: {directory}")
39
+
40
+ self.allowed_directories = abs_directories
41
+ print(f"Final allowed directories: {self.allowed_directories}")
42
+
43
+ # Test Audiveris installation
44
+ self._test_audiveris()
45
+
46
+ def list_resources(self) -> List[Dict[str, Any]]:
47
+ """List available resources following MCP patterns"""
48
+ resources = []
49
+
50
+ # Add processed files as resources
51
+ for file_id, file_info in self.processed_files.items():
52
+ resources.append({
53
+ "uri": f"musicxml://{file_id}",
54
+ "name": file_info["original_name"],
55
+ "description": f"MusicXML file converted from {file_info['original_name']} on {file_info['processed_at']}",
56
+ "mimeType": "application/vnd.recordare.musicxml+xml"
57
+ })
58
+
59
+ # Add available PDF files in allowed directories
60
+ for directory in self.allowed_directories:
61
+ if os.path.exists(directory):
62
+ for file_path in Path(directory).rglob("*.pdf"):
63
+ if self._is_file_accessible(str(file_path)):
64
+ resources.append({
65
+ "uri": f"file://{file_path}",
66
+ "name": file_path.name,
67
+ "description": f"PDF music score available for processing: {file_path.name}",
68
+ "mimeType": "application/pdf"
69
+ })
70
+
71
+ return resources
72
+
73
+ def read_resource(self, uri: str) -> Dict[str, Any]:
74
+ """Read resource content following MCP patterns"""
75
+ if uri.startswith("musicxml://"):
76
+ # Return processed MusicXML file
77
+ file_id = uri.replace("musicxml://", "")
78
+ if file_id in self.processed_files:
79
+ file_info = self.processed_files[file_id]
80
+ try:
81
+ with open(file_info["output_path"], "rb") as f:
82
+ content = base64.b64encode(f.read()).decode()
83
+
84
+ return {
85
+ "contents": [{
86
+ "type": "resource",
87
+ "resource": {
88
+ "uri": uri,
89
+ "text": content,
90
+ "mimeType": "application/vnd.recordare.musicxml+xml"
91
+ }
92
+ }]
93
+ }
94
+ except FileNotFoundError:
95
+ raise Exception(f"MusicXML file not found: {file_info['output_path']}")
96
+ else:
97
+ raise Exception(f"Resource not found: {uri}")
98
+
99
+ elif uri.startswith("file://"):
100
+ # Return PDF file content
101
+ file_path = uri.replace("file://", "")
102
+ if not self._is_file_accessible(file_path):
103
+ raise Exception(f"File access denied: {file_path}")
104
+
105
+ try:
106
+ with open(file_path, "rb") as f:
107
+ content = base64.b64encode(f.read()).decode()
108
+
109
+ return {
110
+ "contents": [{
111
+ "type": "resource",
112
+ "resource": {
113
+ "uri": uri,
114
+ "text": content,
115
+ "mimeType": "application/pdf"
116
+ }
117
+ }]
118
+ }
119
+ except FileNotFoundError:
120
+ raise Exception(f"File not found: {file_path}")
121
+
122
+ else:
123
+ raise Exception(f"Unsupported URI scheme: {uri}")
124
+
125
+ def _is_file_accessible(self, file_path: str) -> bool:
126
+ """Check if file is within allowed directories"""
127
+ abs_path = os.path.abspath(file_path)
128
+ return any(abs_path.startswith(os.path.abspath(d)) for d in self.allowed_directories)
129
+
130
+ def recognize_music_tool(self, pdf_uri: str, output_dir: str = None) -> Dict[str, Any]:
131
+ """Tool for music recognition following MCP patterns"""
132
+ # Handle different URI formats
133
+ if pdf_uri.startswith("file://"):
134
+ pdf_path = pdf_uri.replace("file://", "")
135
+ elif pdf_uri.startswith("data:"):
136
+ # Handle data URIs (base64 encoded)
137
+ return self._process_data_uri(pdf_uri, output_dir)
138
+ else:
139
+ # Assume it's a direct file path
140
+ pdf_path = pdf_uri
141
+
142
+ if not self._is_file_accessible(pdf_path):
143
+ raise Exception(f"File access denied: {pdf_path}")
144
+
145
+ if not os.path.exists(pdf_path):
146
+ raise Exception(f"PDF file not found: {pdf_path}")
147
+
148
+ try:
149
+ output_file = self._recognize_music_core(pdf_path, output_dir)
150
+
151
+ # Store in processed files cache
152
+ file_id = f"music_{len(self.processed_files) + 1}_{int(datetime.now().timestamp())}"
153
+ self.processed_files[file_id] = {
154
+ "original_name": os.path.basename(pdf_path),
155
+ "original_path": pdf_path,
156
+ "output_path": output_file,
157
+ "processed_at": datetime.now().isoformat(),
158
+ "file_id": file_id
159
+ }
160
+
161
+ # Return MCP-compliant response
162
+ return {
163
+ "content": [{
164
+ "type": "text",
165
+ "text": f"✅ Successfully converted '{os.path.basename(pdf_path)}' to MusicXML.\n\n"
166
+ f"📁 Output file: {output_file}\n"
167
+ f"🔗 Resource URI: musicxml://{file_id}\n"
168
+ f"📊 File size: {os.path.getsize(output_file)} bytes\n\n"
169
+ f"You can now access this MusicXML file as a resource using the URI: `musicxml://{file_id}`"
170
+ }],
171
+ "isError": False
172
+ }
173
+
174
+ except Exception as e:
175
+ return {
176
+ "content": [{
177
+ "type": "text",
178
+ "text": f"❌ Music recognition failed: {str(e)}"
179
+ }],
180
+ "isError": True
181
+ }
182
+
183
+ def _process_data_uri(self, data_uri: str, output_dir: str = None) -> Dict[str, Any]:
184
+ """Process base64 encoded data URI"""
185
+ try:
186
+ # Parse data URI: data:application/pdf;base64,<data>
187
+ header, data = data_uri.split(',', 1)
188
+ mime_type = header.split(';')[0].replace('data:', '')
189
+
190
+ if mime_type != 'application/pdf':
191
+ raise Exception(f"Unsupported MIME type: {mime_type}")
192
+
193
+ # Fix base64 padding if needed
194
+ data = self._fix_base64_padding(data)
195
+
196
+ # Decode base64 data
197
+ pdf_data = base64.b64decode(data)
198
+
199
+ # Save to temporary file
200
+ temp_dir = output_dir or "/tmp"
201
+ temp_pdf = os.path.join(temp_dir, f"temp_{int(datetime.now().timestamp())}.pdf")
202
+
203
+ with open(temp_pdf, 'wb') as f:
204
+ f.write(pdf_data)
205
+
206
+ # Process the file
207
+ return self.recognize_music_tool(f"file://{temp_pdf}", output_dir)
208
+
209
+ except Exception as e:
210
+ raise Exception(f"Failed to process data URI: {str(e)}")
211
+
212
+ def _fix_base64_padding(self, data: str) -> str:
213
+ """Fix base64 padding to make it valid"""
214
+ # Remove any whitespace
215
+ data = data.strip().replace('\n', '').replace('\r', '').replace(' ', '')
216
+
217
+ # Add padding if needed
218
+ missing_padding = len(data) % 4
219
+ if missing_padding:
220
+ data += '=' * (4 - missing_padding)
221
+
222
+ return data
223
+
224
+ def _recognize_music_core(self, pdf_file_path: str, output_dir: str = None) -> str:
225
+ """Core music recognition function"""
226
+ audiveris = "/opt/audiveris/bin/Audiveris"
227
+
228
+ if output_dir is None:
229
+ output_dir = "/tmp/output"
230
+
231
+ # Ensure output directory exists with proper permissions
232
+ os.makedirs(output_dir, exist_ok=True)
233
+ try:
234
+ os.chmod(output_dir, 0o755)
235
+ except Exception as e:
236
+ print(f"Warning: Could not set permissions for {output_dir}: {e}")
237
+
238
+ if not self._is_file_accessible(output_dir):
239
+ raise Exception(f"Output directory access denied: {output_dir}")
240
+
241
+ # Verify input file exists
242
+ if not os.path.exists(pdf_file_path):
243
+ raise Exception(f"Input PDF file not found: {pdf_file_path}")
244
+
245
+ pdf_file_name = os.path.basename(pdf_file_path)
246
+ pdf_name_without_ext = os.path.splitext(pdf_file_name)[0]
247
+
248
+ # Try both possible extensions
249
+ possible_extensions = [".mxl", ".xml", ".musicxml"]
250
+ output_files = [os.path.join(output_dir, f"{pdf_name_without_ext}{ext}") for ext in possible_extensions]
251
 
252
+ cmd = [
253
+ audiveris, "-batch", "-export", "-output", output_dir, pdf_file_path
254
+ ]
255
 
256
+ print(f"Running Audiveris command: {' '.join(cmd)}")
257
+ result = subprocess.run(cmd, capture_output=True, text=True)
258
+
259
+ print(f"Audiveris stdout: {result.stdout}")
260
+ print(f"Audiveris stderr: {result.stderr}")
261
+ print(f"Audiveris return code: {result.returncode}")
262
+
263
+ # List files in output directory for debugging
264
+ if os.path.exists(output_dir):
265
+ files_in_output = os.listdir(output_dir)
266
+ print(f"Files in output directory: {files_in_output}")
267
+
268
+ # Check if any of the possible output files exist
269
+ existing_output = None
270
+ for output_file in output_files:
271
+ if os.path.exists(output_file):
272
+ existing_output = output_file
273
+ break
274
+
275
+ if existing_output:
276
+ print(f"Found output file: {existing_output}")
277
+ return existing_output
278
+
279
+ # If no output file found, provide detailed error
280
+ error_msg = f"Audiveris processing failed.\n"
281
+ error_msg += f"Return code: {result.returncode}\n"
282
+ error_msg += f"Stdout: {result.stdout}\n"
283
+ error_msg += f"Stderr: {result.stderr}\n"
284
+ error_msg += f"Expected files: {output_files}\n"
285
+ error_msg += f"Files in output dir: {os.listdir(output_dir) if os.path.exists(output_dir) else 'Directory does not exist'}\n"
286
+
287
+ raise Exception(error_msg)
288
+
289
+ def _test_audiveris(self):
290
+ """Test if Audiveris is properly installed"""
291
+ audiveris = "/opt/audiveris/bin/Audiveris"
292
+
293
+ if not os.path.exists(audiveris):
294
+ print(f"⚠️ Warning: Audiveris not found at {audiveris}")
295
+ return False
296
+
297
+ try:
298
+ # Test Audiveris with help command
299
+ result = subprocess.run([audiveris, "-help"], capture_output=True, text=True, timeout=10)
300
+ if "Audiveris" in result.stdout or "Audiveris" in result.stderr:
301
+ print("✅ Audiveris installation verified")
302
+ return True
303
+ else:
304
+ print(f"⚠️ Warning: Audiveris may not be working properly")
305
+ print(f"Output: {result.stdout}")
306
+ print(f"Error: {result.stderr}")
307
+ return False
308
+ except Exception as e:
309
+ print(f"⚠️ Warning: Could not test Audiveris: {e}")
310
+ return False
311
+
312
+ # Initialize MCP Server
313
+ mcp_server = MusicRecognitionMCPServer(["/tmp", "uploads", "output"])
314
 
315
+ def recognize_music_gradio(pdf_file):
316
+ """Gradio wrapper for music recognition"""
317
+ try:
318
+ print(f"Processing file: {pdf_file.name}")
319
+ result = mcp_server.recognize_music_tool(f"file://{pdf_file.name}")
320
+
321
+ if result.get("isError"):
322
+ error_msg = result["content"][0]["text"]
323
+ print(f"Error in music recognition: {error_msg}")
324
+ return None
325
+
326
+ # Extract file ID from the response
327
+ response_text = result["content"][0]["text"]
328
+ print(f"Response text: {response_text}")
329
+
330
+ if "musicxml://" in response_text:
331
+ file_id = response_text.split("musicxml://")[1].split("`")[0]
332
+ print(f"Extracted file ID: {file_id}")
333
+
334
+ if file_id in mcp_server.processed_files:
335
+ file_info = mcp_server.processed_files[file_id]
336
+ output_path = file_info["output_path"]
337
+ print(f"Output path from cache: {output_path}")
338
+
339
+ if os.path.exists(output_path):
340
+ print(f"✅ File exists: {output_path}")
341
+ return output_path
342
+ else:
343
+ print(f"❌ File not found: {output_path}")
344
+
345
+ # If the above doesn't work, try to find the file directly
346
+ pdf_basename = os.path.splitext(os.path.basename(pdf_file.name))[0]
347
+ possible_files = [
348
+ f"/tmp/output/{pdf_basename}.mxl",
349
+ f"/tmp/output/{pdf_basename}.xml",
350
+ f"/tmp/output/{pdf_basename}.musicxml"
351
+ ]
352
+
353
+ for file_path in possible_files:
354
+ print(f"Checking: {file_path}")
355
+ if os.path.exists(file_path):
356
+ print(f"✅ Found file: {file_path}")
357
+ return file_path
358
+
359
+ print("❌ No output file found in any expected location")
360
+ print(f"Files in /tmp/output: {os.listdir('/tmp/output') if os.path.exists('/tmp/output') else 'Directory not found'}")
361
+ return None
362
+
363
+ except Exception as e:
364
+ print(f"Exception in Gradio wrapper: {str(e)}")
365
+ import traceback
366
+ traceback.print_exc()
367
+ return None
368
 
369
+ # Create Gradio interface
370
+ gradio_interface = gr.Interface(
371
+ fn=recognize_music_gradio,
372
  inputs=gr.File(file_types=[".pdf"], label="Upload PDF music score"),
373
  outputs=gr.File(label="Download MusicXML file"),
374
+ title="Music Score Recognition",
375
+ description="Upload a PDF music score and create a MusicXML file from it.",
376
  )
 
377
 
378
+ def run_gradio():
379
+ """Run Gradio in a separate thread"""
380
+ gradio_interface.launch(
381
+ server_name="0.0.0.0",
382
+ server_port=7860,
383
+ share=True,
384
+ mcp_server=True,
385
+ prevent_thread_lock=True
386
+ )
387
+
388
+ if __name__ == "__main__":
389
+ # Start Gradio in a separate thread
390
+ gradio_thread = threading.Thread(target=run_gradio, daemon=True)
391
+ gradio_thread.start()
392
+
393
+ print("🎵 MCP-Compliant Music Recognition Service Starting...")
394
+ print("📱 Gradio UI: http://localhost:7860")
requirements.txt ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ gradio[mcp]>=4.0.0
2
+ python-multipart>=0.0.6
3
+ requests>=2.31.0