midiCP / app.py
matthewbarberdev's picture
Update app.py
4b16ae7 verified
raw
history blame
6.26 kB
import gradio as gr
import tempfile
import random
import json
import re
import pretty_midi
import subprocess
import os
from openai import OpenAI
# Audio playback support
try:
import pygame
pygame.mixer.init()
PYGAME_AVAILABLE = True
except Exception as e:
print(f"[WARNING] pygame mixer init failed: {e}")
PYGAME_AVAILABLE = False
# === LLM APIs ===
def query_llm(prompt, model_name=None):
if model_name and model_name != "OpenAI":
import requests
response = requests.post("http://localhost:11434/api/generate", json={"model": model_name, "prompt": prompt, "stream": False})
return response.json().get("response", "")
else:
# Replace or load from environment
client = OpenAI(
base_url="https://api.studio.nebius.com/v1/",
api_key=os.environ.get("NEBIUS_API_KEY")
)
response = client.chat.completions.create(
model="meta-llama/Llama-3.3-70B-Instruct",
max_tokens=512,
temperature=0.6,
top_p=0.9,
extra_body={
"top_k": 50
},
messages=[]
)
return response["choices"][0]["message"]["content"]
# === Step 1: Parse intent ===
def get_intent_from_prompt(prompt, model_name):
system_prompt = f"""
Extract the musical intent from this prompt.
Return JSON with keys: tempo (int), key (A-G#), scale (major/minor), genre (e.g., lo-fi, trap), emotion, instrument.
Prompt: '{prompt}'
"""
response = query_llm(system_prompt, model_name)
match = re.search(r'\{.*\}', response, re.DOTALL)
if match:
try:
return json.loads(match.group(0))
except json.JSONDecodeError:
return {"tempo": 120, "key": "C", "scale": "major", "genre": "default", "emotion": "neutral", "instrument": "piano"}
return {"tempo": 120, "key": "C", "scale": "major", "genre": "default", "emotion": "neutral", "instrument": "piano"}
# === Step 2: Melody planning ===
def get_melody_from_intent(intent, model_name):
melody_prompt = f"""
You are a music composer.
Based on this musical intent:
{json.dumps(intent)}
Generate a melody plan using a list of 16 notes with pitch (A-G#), octave (3-6), and duration (0.25 to 1.0 seconds).
Output ONLY valid JSON like:
[
{{"note": "D", "octave": 4, "duration": 0.5}},
{{"note": "F", "octave": 4, "duration": 1.0}}
]
"""
response = query_llm(melody_prompt, model_name)
print(f"\n[DEBUG] LLM Response for melody:\n{response}\n")
match = re.search(r'\[.*\]', response, re.DOTALL)
if match:
try:
melody = json.loads(match.group(0))
if isinstance(melody, list) and len(melody) > 0:
return melody
except json.JSONDecodeError as e:
print(f"[ERROR] Melody JSON decode error: {e}")
print("[WARNING] Using fallback melody.")
return [
{"note": "C", "octave": 4, "duration": 0.5},
{"note": "E", "octave": 4, "duration": 0.5},
{"note": "G", "octave": 4, "duration": 0.5},
{"note": "B", "octave": 4, "duration": 0.5},
]
# === Step 3: MIDI generation ===
def midi_from_plan(melody, tempo):
midi = pretty_midi.PrettyMIDI()
instrument = pretty_midi.Instrument(program=0)
time = 0.0
seconds_per_beat = 60.0 / tempo
note_map = {"C": 0, "C#": 1, "D": 2, "D#": 3, "E": 4, "F": 5, "F#": 6,
"G": 7, "G#": 8, "A": 9, "A#": 10, "B": 11}
for note_info in melody:
try:
pitch = 12 * (note_info["octave"] + 1) + note_map[note_info["note"].upper()]
duration = float(note_info["duration"])
start = time
end = time + duration
instrument.notes.append(pretty_midi.Note(
velocity=100, pitch=pitch, start=start, end=end
))
time = end
except:
continue
midi.instruments.append(instrument)
return midi
# === Generate audio preview from MIDI ===
def midi_to_wav(midi_path):
try:
import tempfile
import subprocess
import os
# Convert MIDI to WAV using FluidSynth if installed, else fallback to empty
wav_path = tempfile.NamedTemporaryFile(delete=False, suffix=".wav").name
# Use fluidsynth if available, else skip audio preview
fluidsynth_cmd = ["fluidsynth", "-ni", "/usr/share/sounds/sf2/FluidR3_GM.sf2", midi_path, "-F", wav_path, "-r", "44100"]
result = subprocess.run(fluidsynth_cmd, capture_output=True)
if result.returncode != 0:
print("[WARNING] FluidSynth conversion failed or is not installed.")
return None
return wav_path
except Exception as e:
print(f"[ERROR] midi_to_wav failed: {e}")
return None
# === Main function to generate MIDI and audio preview ===
def generate_midi_and_audio(prompt, model_name):
intent = get_intent_from_prompt(prompt, model_name)
melody = get_melody_from_intent(intent, model_name)
midi = midi_from_plan(melody, intent.get("tempo", 120))
with tempfile.NamedTemporaryFile(delete=False, suffix=".mid") as tmp:
midi.write(tmp.name)
midi_path = tmp.name
audio_path = None
if PYGAME_AVAILABLE:
audio_path = midi_path # We'll use pygame to play midi directly if possible
return midi_path, audio_path
# === Get Ollama models ===
def get_ollama_models():
try:
result = subprocess.run(["ollama", "list"], capture_output=True, text=True)
models = [line.split()[0] for line in result.stdout.strip().splitlines()[1:]]
return ["OpenAI"] + models
except Exception as e:
return ["OpenAI"]
# === Gradio UI ===
models = get_ollama_models()
demo = gr.Interface(
fn=generate_midi_and_audio,
inputs=[
gr.Textbox(label="Music Prompt"),
gr.Dropdown(choices=models, label="LLM Model", value=models[0])
],
outputs=[
gr.File(label="🎵 Download MIDI File"),
gr.Audio(label="🎧 Audio Preview (MIDI Playback, if supported)", type="filepath")
],
title="🎼 Music Command Prompt (MCP Agent)",
description="Describe your music idea and download a generated MIDI file. Choose from local or OpenAI LLMs."
)
demo.launch(mcp_server=True)