ohollo's picture
Tweak intro
a90133c
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,
)