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'
{how_it_works_svg}
') 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, )