File size: 8,498 Bytes
d4bbd3b
47de94c
f132626
ad5f41f
b50a375
 
 
 
 
 
 
 
 
 
 
d4bbd3b
 
c5184df
47de94c
c5184df
87093fc
 
c5184df
12a0c2c
0124194
 
87093fc
 
0124194
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
f132626
b50a375
f132626
12a0c2c
b50a375
 
12a0c2c
 
 
 
 
 
 
 
 
 
b50a375
12a0c2c
 
f132626
b50a375
f132626
12a0c2c
b50a375
 
12a0c2c
 
 
 
 
 
 
 
 
 
 
b50a375
d4bbd3b
0124194
 
 
 
 
47de94c
f132626
 
 
d4bbd3b
1581f07
 
d4bbd3b
a90133c
f132626
 
1581f07
d4bbd3b
 
 
 
 
47de94c
d4bbd3b
 
 
 
 
 
 
 
f132626
d4bbd3b
 
 
 
 
 
 
 
87093fc
d4bbd3b
 
 
c5184df
47de94c
c5184df
 
87093fc
d4bbd3b
87093fc
d4bbd3b
 
 
 
 
1581f07
 
d4bbd3b
 
b50a375
d4bbd3b
b50a375
d4bbd3b
f132626
d4bbd3b
 
 
 
 
 
c5184df
 
 
87093fc
c5184df
 
d4bbd3b
 
12a0c2c
f132626
12a0c2c
 
d4bbd3b
 
 
12a0c2c
f132626
12a0c2c
 
d4bbd3b
 
12a0c2c
 
 
f132626
12a0c2c
 
d4bbd3b
12a0c2c
 
 
f132626
12a0c2c
 
 
0124194
 
 
 
 
 
 
 
d4bbd3b
 
 
 
 
 
d907a5f
87093fc
d4bbd3b
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
import gradio as gr
import cfg
from setup import analyze_chord_sequence_text, analyze_music_file

# Gradio 5.49 bug: monitoring/summary serialises function stats with integer keys,
# which orjson rejects. Patch _render to allow non-string keys.
import orjson
import gradio.routes as _gr_routes

@staticmethod
def _patched_render(content):
    return orjson.dumps(content, option=orjson.OPT_NON_STR_KEYS | orjson.OPT_SERIALIZE_NUMPY)

_gr_routes.ORJSONResponse._render = _patched_render



# Load how it works content
with open(cfg.HOW_IT_WORKS_MD_LOCATION, 'r') as f:
    how_it_works_content = f.read()
with open(cfg.HOW_IT_WORKS_SVG_LOCATION, 'r') as f:
    how_it_works_svg = f.read()


def _get_supported_chord_formats() -> str:
    return (
        "Roots: A, Ab, B, Bb, C, C#, D, Eb, E, F, F#, G  \n"
        "Modifiers: m, m7, maj7, m7b5, aug, dim, 6, 7, and slash chords (e.g. C/Bb, C/G)  \n"
        "Example chords for C: C, C/Bb, C/D, C/E, C/G, C6, C7, C7/E, Caug, Cdim, Cmaj7, Cm, Cm6, Cm7, Cm7b5, Cm7b5/Bb"
    )


@gr.mcp.resource("chords://supported-formats")
def supported_chord_formats() -> str:
    """Supported chord roots and modifiers for use with the analyze_chord_sequence_text tool."""
    return _get_supported_chord_formats()



def get_supported_chord_formats() -> str:
    """Returns supported chord roots and modifiers for use with the analyze_chord_sequence_text tool."""
    return _get_supported_chord_formats()


def _format_chord_analysis_for_ui(chord_text, limit):
    yield "⏳ Analysing...", ""
    score, neighbours = analyze_chord_sequence_text(chord_text, limit=int(limit))
    if score is None:
        yield "Please enter some chords!", ""
        return
    scores_text = f"**Originality Score:** {score:.4f}"
    neighbours_text = "**Similar Songs:**\n"
    if neighbours:
        for i, neighbor in enumerate(neighbours, 1):
            title = neighbor['title']
            artist = neighbor['artist']
            similarity = neighbor['similarity']
            neighbours_text += f"{i}. {title} by {artist} (similarity: {similarity:.3f})\n"
    else:
        neighbours_text += "No similar songs found."
    yield scores_text, neighbours_text


def _format_music_analysis_for_ui(audio_file, limit):
    yield "", "⏳ Analysing...", ""
    file_info, score, neighbours = analyze_music_file(audio_file, limit=int(limit))
    if score is None:
        yield "Please upload a music file!", "", ""
        return
    scores_text = f"**Originality Score:** {score:.4f}"
    neighbours_text = "**Similar Songs:**\n"
    if neighbours:
        for i, neighbor in enumerate(neighbours, 1):
            title = neighbor['title']
            artist = neighbor['artist']
            similarity = neighbor['similarity']
            neighbours_text += f"{i}. {title} by {artist} (similarity: {similarity:.3f})\n"
    else:
        neighbours_text += "No similar songs found."
    file_info_text = f"**File analyzed:** {file_info}" if file_info else ""
    yield file_info_text, scores_text, neighbours_text

_preamble = (
    "Enter chords separated by commas or spaces. "
    "Chords are expressed with a root note followed by a modifier without a space.\n\n"
    + _get_supported_chord_formats()
)

def _similar_songs_limit_input():
    return gr.Number(label="Max similar songs", value=5, minimum=1, maximum=cfg.MAX_SIMILAR_SONGS, step=1, scale=0)

# Create Gradio interface
with gr.Blocks(title="Harmonic Analysis Tool", theme=gr.themes.Soft(),
               css=".gradio-container { max-width: none !important; padding-left: 2rem !important; padding-right: 2rem !important; }") as app:
    gr.Markdown("# 🎵 Harmonic Analysis Tool")
    gr.Markdown("Inspect chord progressions for originality and find similar songs in a dataset (or songs containing similar segments). Input chords as text or upload a music file.")
    gr.Markdown("Originality score ranges from 0 (many similar songs harmonically) to 1 (no similar songs harmonically).  "
                "Conversely, for each similar song, a similarity score is given where 1 means most similar. "
                "Score and similar songs are formed using data from the [lmd_chord dataset](https://huggingface.co/datasets/ohollo/lmd_chords).")
    
    with gr.Tabs():
        # Tab 1: Text Input
        with gr.TabItem("Text Input"):
            gr.Markdown("### Enter Chord Sequence")
            gr.Markdown(_preamble)
            
            with gr.Row():
                with gr.Column(scale=2):
                    chord_input = gr.Textbox(
                        label="Chord Sequence",
                        placeholder="C, Am, F, G",
                        lines=2
                    )
                    similar_songs_limit = _similar_songs_limit_input()
                    analyze_btn = gr.Button("Analyze Chords", variant="primary")
                
                with gr.Column(scale=3):
                    scores_output = gr.Markdown(label="Analysis Results")
                    neighbours_output = gr.Markdown(label="Similar Songs")
            
            # Example chord progressions
            gr.Markdown("### Example Chord Progressions")
            gr.Examples(
                examples=[
                    ["C, Am, F, G"],
                    ["D, A, Bm, G"],
                    ["F, C, Dm, Bb"],
                    ["C, C/Bb, C/D, C/E, C/G, C6, C7, C7/E, Caug, Cdim, Cmaj7, Cm, Cm6, Cm7, Cm7b5, Cm7b5/Bb"],
                    ["A7, D7, A7, E7, D7, A7"],
                    ["Am, F, C, G, Am, F, C, G, C, G, Am, F, C, G, Am, F, "
                     "Am, F, C, G, Am, F, C, G, C, G, Am, F, C, G, Am, F"],
                ],
                inputs=[chord_input],
            )
        
        # Tab 2: File Upload
        with gr.TabItem("File Upload"):
            gr.Markdown("### Upload Music File")
            gr.Markdown("Upload an audio file to extract and inspect its chord progression.")
            gr.Markdown("This feature is very experimental and works best with MIDI files but other audio formats including .mp3, .wav and .ogg are also accepted.")
            with gr.Row():
                with gr.Column(scale=1):
                    audio_input = gr.File(
                        label="Upload Audio File",
                        file_types=["audio", ".mid", ".midi"],
                    )
                    audio_similar_songs_limit = _similar_songs_limit_input()
                    upload_btn = gr.Button("Analyze Audio", variant="primary")
                
                with gr.Column(scale=2):
                    file_display = gr.Markdown(label="File Info")
                    audio_scores_output = gr.Markdown(label="Analysis Results")
                    audio_neighbours_output = gr.Markdown(label="Similar Songs")

        # Tab 3: How It Works
        with gr.TabItem("How It Works"):
            gr.HTML(f'<div style="text-align:center;margin:2em 0">{how_it_works_svg}</div>')
            gr.Markdown(how_it_works_content)

    # Event handlers
    analyze_btn.click(
        fn=_format_chord_analysis_for_ui,
        inputs=[chord_input, similar_songs_limit],
        outputs=[scores_output, neighbours_output],
        api_name=False
    )
    
    upload_btn.click(
        fn=_format_music_analysis_for_ui,
        inputs=[audio_input, audio_similar_songs_limit],
        outputs=[file_display, audio_scores_output, audio_neighbours_output],
        api_name=False
    )
    
    # Register API-only endpoints using dummy hidden components
    gr.Textbox(visible=False).change(
        fn=analyze_chord_sequence_text,
        inputs=[gr.Textbox(visible=False), gr.Number(visible=False, value=10)],
        outputs=[gr.Number(visible=False), gr.JSON(visible=False)],
        api_name="analyze_chord_sequence_text"
    )
    
    gr.Audio(visible=False, type="filepath").change(
        fn=analyze_music_file,
        inputs=[gr.Audio(visible=False, type="filepath"), gr.Number(visible=False, value=10)],
        outputs=[gr.Textbox(visible=False), gr.Number(visible=False), gr.JSON(visible=False)],
        api_name="analyze_music_file"
    )

    gr.Textbox(visible=False).change(
        fn=get_supported_chord_formats,
        inputs=[],
        outputs=[gr.Textbox(visible=False)],
        api_name="get_supported_chord_formats"
    )


if __name__ == "__main__":
    app.launch(
        server_name="0.0.0.0",
        server_port=7860,
        share=False,
        show_error=True,
        mcp_server=True,
    )