Spaces:
Build error
Build error
Download web_visualizer.py from coisini9293/score: direct link, hf CLI and curl.
- Browser
- Download file 8 kB
-
https://huggingface.co/spaces/coisini9293/score/resolve/main/web_visualizer.py
- Command line
-
hf download hf://spaces/coisini9293/score/web_visualizer.py
-
curl -L -o web_visualizer.py https://huggingface.co/spaces/coisini9293/score/resolve/main/web_visualizer.py
8 kB
| #!/usr/bin/env python3 | |
| """ | |
| 创建HTML可视化页面 | |
| """ | |
| import os | |
| import json | |
| import music21 | |
| from music21 import converter | |
| def create_html_visualizer(midi_path, output_dir="output"): | |
| """创建HTML可视化页面""" | |
| print("正在创建HTML可视化页面...") | |
| # 加载MIDI文件 | |
| score = music21.converter.parse(midi_path) | |
| # 提取音符数据 | |
| voice_data = [] | |
| voice_names = ['Soprano', 'Alto', 'Tenor', 'Bass'] | |
| voice_colors = ['#FF6B6B', '#4ECDC4', '#45B7D1', '#96CEB4'] | |
| for i, part in enumerate(score.parts): | |
| if i >= 4: | |
| break | |
| part_notes = [] | |
| for element in part.flat.notes: | |
| if hasattr(element, 'pitch'): | |
| part_notes.append({ | |
| 'pitch': element.pitch.midi, | |
| 'note_name': element.pitch.nameWithOctave, | |
| 'start': float(element.offset), | |
| 'duration': float(element.quarterLength), | |
| 'velocity': element.volume.velocity if hasattr(element.volume, 'velocity') else 80 | |
| }) | |
| elif hasattr(element, 'pitches'): | |
| for p in element.pitches: | |
| part_notes.append({ | |
| 'pitch': p.midi, | |
| 'note_name': p.nameWithOctave, | |
| 'start': float(element.offset), | |
| 'duration': float(element.quarterLength), | |
| 'velocity': element.volume.velocity if hasattr(element.volume, 'velocity') else 80 | |
| }) | |
| voice_data.append({ | |
| 'name': voice_names[i], | |
| 'color': voice_colors[i], | |
| 'notes': part_notes | |
| }) | |
| # 创建HTML内容 | |
| html_content = f""" | |
| <!DOCTYPE html> | |
| <html lang="zh-CN"> | |
| <head> | |
| <meta charset="UTF-8"> | |
| <meta name="viewport" content="width=device-width, initial-scale=1.0"> | |
| <title>四声部可视化</title> | |
| <script src="https://cdn.jsdelivr.net/npm/vega@5"></script> | |
| <script src="https://cdn.jsdelivr.net/npm/vega-lite@5"></script> | |
| <script src="https://cdn.jsdelivr.net/npm/vega-embed@6"></script> | |
| <style> | |
| body {{ | |
| font-family: Arial, sans-serif; | |
| margin: 20px; | |
| background-color: #f5f5f5; | |
| }} | |
| .container {{ | |
| max-width: 1200px; | |
| margin: 0 auto; | |
| background: white; | |
| padding: 20px; | |
| border-radius: 10px; | |
| box-shadow: 0 2px 10px rgba(0,0,0,0.1); | |
| }} | |
| h1 {{ | |
| text-align: center; | |
| color: #333; | |
| margin-bottom: 30px; | |
| }} | |
| .voice-section {{ | |
| margin-bottom: 30px; | |
| padding: 15px; | |
| border: 2px solid #ddd; | |
| border-radius: 8px; | |
| }} | |
| .voice-title {{ | |
| font-size: 18px; | |
| font-weight: bold; | |
| margin-bottom: 10px; | |
| }} | |
| .note-list {{ | |
| display: flex; | |
| flex-wrap: wrap; | |
| gap: 5px; | |
| }} | |
| .note {{ | |
| padding: 5px 10px; | |
| border-radius: 15px; | |
| font-size: 12px; | |
| font-weight: bold; | |
| color: white; | |
| text-shadow: 1px 1px 1px rgba(0,0,0,0.3); | |
| }} | |
| .stats {{ | |
| display: grid; | |
| grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); | |
| gap: 20px; | |
| margin-top: 20px; | |
| }} | |
| .stat-card {{ | |
| background: #f8f9fa; | |
| padding: 15px; | |
| border-radius: 8px; | |
| text-align: center; | |
| }} | |
| .stat-number {{ | |
| font-size: 24px; | |
| font-weight: bold; | |
| color: #007bff; | |
| }} | |
| .stat-label {{ | |
| font-size: 14px; | |
| color: #666; | |
| margin-top: 5px; | |
| }} | |
| </style> | |
| </head> | |
| <body> | |
| <div class="container"> | |
| <h1>🎵 四声部可视化结果</h1> | |
| <div class="stats"> | |
| <div class="stat-card"> | |
| <div class="stat-number" id="total-notes">0</div> | |
| <div class="stat-label">总音符数</div> | |
| </div> | |
| <div class="stat-card"> | |
| <div class="stat-number" id="total-voices">4</div> | |
| <div class="stat-label">声部数量</div> | |
| </div> | |
| <div class="stat-card"> | |
| <div class="stat-number" id="duration">0</div> | |
| <div class="stat-label">时长(拍)</div> | |
| </div> | |
| </div> | |
| <div id="voice-sections"> | |
| {generate_voice_sections(voice_data)} | |
| </div> | |
| <div style="margin-top: 30px; text-align: center;"> | |
| <h3>📁 文件下载</h3> | |
| <p> | |
| <a href="four_voice_score.xml" download style="margin: 0 10px; padding: 10px 20px; background: #007bff; color: white; text-decoration: none; border-radius: 5px;">下载 MusicXML</a> | |
| <a href="four_voice_score.mid" download style="margin: 0 10px; padding: 10px 20px; background: #28a745; color: white; text-decoration: none; border-radius: 5px;">下载 MIDI</a> | |
| </p> | |
| </div> | |
| </div> | |
| <script> | |
| const voiceData = {json.dumps(voice_data, ensure_ascii=False, indent=2)}; | |
| // 计算统计信息 | |
| let totalNotes = 0; | |
| let maxDuration = 0; | |
| voiceData.forEach(voice => {{ | |
| totalNotes += voice.notes.length; | |
| voice.notes.forEach(note => {{ | |
| maxDuration = Math.max(maxDuration, note.start + note.duration); | |
| }}); | |
| }}); | |
| document.getElementById('total-notes').textContent = totalNotes; | |
| document.getElementById('duration').textContent = maxDuration.toFixed(1); | |
| // 为每个音符添加悬停效果 | |
| document.querySelectorAll('.note').forEach(note => {{ | |
| note.addEventListener('mouseenter', function() {{ | |
| this.style.transform = 'scale(1.1)'; | |
| this.style.transition = 'transform 0.2s'; | |
| }}); | |
| note.addEventListener('mouseleave', function() {{ | |
| this.style.transform = 'scale(1)'; | |
| }}); | |
| }}); | |
| </script> | |
| </body> | |
| </html> | |
| """ | |
| # 保存HTML文件 | |
| html_path = os.path.join(output_dir, "visualization.html") | |
| with open(html_path, 'w', encoding='utf-8') as f: | |
| f.write(html_content) | |
| print(f"HTML可视化页面已保存到: {html_path}") | |
| return html_path | |
| def generate_voice_sections(voice_data): | |
| """生成声部HTML部分""" | |
| html = "" | |
| for voice in voice_data: | |
| html += f""" | |
| <div class="voice-section" style="border-color: {voice['color']};"> | |
| <div class="voice-title" style="color: {voice['color']};"> | |
| {voice['name']} ({len(voice['notes'])} 个音符) | |
| </div> | |
| <div class="note-list"> | |
| """ | |
| # 显示前50个音符 | |
| for note in voice['notes'][:50]: | |
| html += f'<div class="note" style="background-color: {voice["color"]};" title="音高: {note["note_name"]}, 开始: {note["start"]:.1f}, 时长: {note["duration"]:.1f}">{note["note_name"]}</div>' | |
| if len(voice['notes']) > 50: | |
| html += f'<div class="note" style="background-color: #666;">...还有{len(voice["notes"]) - 50}个音符</div>' | |
| html += """ | |
| </div> | |
| </div> | |
| """ | |
| return html | |
| def main(): | |
| output_dir = "output" | |
| midi_path = os.path.join(output_dir, "four_voice_score.mid") | |
| if not os.path.exists(midi_path): | |
| print(f"错误:MIDI文件 {midi_path} 不存在") | |
| return | |
| html_path = create_html_visualizer(midi_path, output_dir) | |
| print(f"\n✅ 可视化完成!") | |
| print(f"在浏览器中打开: file://{os.path.abspath(html_path)}") | |
| if __name__ == "__main__": | |
| main() | |