codel / app.py
baouws's picture
Update app.py
d5f2e8a verified
raw
history blame
23.3 kB
import gradio as gr
import requests
import json
import tempfile
import os
from typing import Optional
import random
# Free Hugging Face LLM model for text generation
MODEL_NAME = "microsoft/DialoGPT-medium"
HF_API_URL = f"https://api-inference.huggingface.co/models/{MODEL_NAME}"
def generate_strudel_code(music_style: str, api_token: Optional[str] = None) -> str:
"""Generate Strudel code using expanded pattern library and optional LLM"""
# Expanded Strudel examples with more variety
strudel_examples = {
"jazz": [
'note("c3 eb3 g3 bb3").slow(2).gain(0.8)',
'note("f3 a3 c4 e4").slow(3).lpf(800)',
'note("bb2 d3 f3 a3").slow(2.5).room(0.3)'
],
"techno": [
's("bd hh").fast(2).gain(0.9)',
's("bd*2 ~ bd ~").stack(s("~ hh*4")).lpf(2000)',
's("bd ~ ~ bd").stack(s("hh*8")).distort(0.1)'
],
"ambient": [
'note("c4 d4 e4 f4").slow(4).lpf(400).room(0.8)',
'note("g3 bb3 d4 f4").slow(6).gain(0.6).delay(0.3)',
'sine(200).slow(8).lpf(300).room(0.9)'
],
"drum and bass": [
's("bd*2 [~ sn] bd sn").fast(4)',
's("bd ~ bd sn").fast(3).stack(s("hh*16").gain(0.3))',
's("bd*2 ~ sn ~").fast(4).lpf(1200)'
],
"house": [
's("bd ~ ~ ~ bd ~ ~ ~").stack(s("~ hh ~ hh"))',
's("bd*2 ~ ~ bd ~ ~ ~").stack(s("hh*4")).lpf(800)',
's("bd ~ bd ~").stack(s("~ hh*2")).gain(0.8)'
],
"classical": [
'note("c4 d4 e4 f4 g4 a4 b4 c5").slow(8)',
'note("g3 b3 d4 g4").slow(4).stack(note("d2 g2").slow(2))',
'note("c4 e4 g4 c5").slow(6).gain(0.7)'
],
"hip hop": [
's("bd sn bd sn").stack(s("hh*8"))',
's("bd*2 ~ sn ~").stack(s("hh ~ hh ~")).slow(0.8)',
's("bd ~ sn bd").stack(s("hh*4")).lpf(1000)'
],
"rock": [
's("bd ~ sn ~").stack(note("e2 ~ g2 ~"))',
's("bd bd sn ~").stack(note("a2 c3 e3").slow(2))',
's("bd ~ sn bd").stack(note("e2 g2").slow(1.5))'
],
"blues": [
'note("c3 eb3 f3 g3 bb3").slow(3)',
'note("e2 a2 b2 e3").slow(4).gain(0.8)',
'note("g2 bb2 d3 f3").slow(3.5).room(0.2)'
],
"reggae": [
's("~ bd ~ sn").slow(2)',
's("~ bd*2 ~ sn").stack(s("hh ~ hh ~")).slow(1.5)',
's("~ bd ~ sn ~").slow(2.5).gain(0.7)'
],
"electronic": [
'square(220).slow(4).lpf(800)',
's("bd*4").stack(sawtooth(110).slow(2))',
'sine(440).fast(2).lpf(1000).delay(0.2)'
],
"minimal": [
's("bd ~ ~ ~").slow(2)',
'note("c4").slow(8).gain(0.5)',
's("~ ~ bd ~").stack(sine(200).slow(16))'
],
"funk": [
's("bd ~ sn bd").stack(note("e2 ~ a2 ~"))',
's("bd*2 sn ~ bd").fast(1.2).stack(s("hh*8"))',
'note("a2 c3 e3").slow(2).stack(s("bd sn"))'
]
}
# Find matching style
best_match = None
for style, patterns in strudel_examples.items():
if style.lower() in music_style.lower():
best_match = random.choice(patterns)
break
# If no direct match, try partial matches
if not best_match:
for style, patterns in strudel_examples.items():
if any(word in music_style.lower() for word in style.split()):
best_match = random.choice(patterns)
break
# Try API generation if token provided and no pattern match
if not best_match and api_token:
try:
headers = {
"Authorization": f"Bearer {api_token}",
"Content-Type": "application/json"
}
prompt = f"Create Strudel live coding music pattern for {music_style}. Use functions: note(), s(), sound(), sine(), square(), sawtooth(), slow(), fast(), stack(), lpf(), gain(), room(), delay(). Example: s('bd hh').fast(2)"
payload = {
"inputs": prompt,
"parameters": {
"max_length": 80,
"temperature": 0.8,
"return_full_text": False,
"num_return_sequences": 1
}
}
response = requests.post(HF_API_URL, headers=headers, json=payload, timeout=15)
if response.status_code == 200:
result = response.json()
if isinstance(result, list) and len(result) > 0:
generated_text = result[0].get('generated_text', '').strip()
# Validate that it contains Strudel functions
if generated_text and any(func in generated_text for func in
['note(', 's(', 'sound(', 'sine(', 'square(', 'sawtooth(']):
return generated_text
except Exception as e:
print(f"API Error: {e}")
# Return best match or default
return best_match or 'note("c4 d4 e4 f4").slow(2).gain(0.7)'
def create_strudel_html(strudel_code: str) -> str:
"""Create simplified HTML with Strudel player that works better in HF Spaces"""
return f"""
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Strudel Music Player</title>
<script src="https://unpkg.com/tone@latest/build/Tone.js"></script>
<style>
* {{
box-sizing: border-box;
margin: 0;
padding: 0;
}}
body {{
font-family: 'Courier New', Arial, sans-serif;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
min-height: 100vh;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 20px;
}}
.container {{
background: rgba(0, 0, 0, 0.3);
backdrop-filter: blur(10px);
border-radius: 15px;
padding: 30px;
max-width: 600px;
width: 100%;
text-align: center;
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.3);
}}
h1 {{
margin-bottom: 20px;
font-size: 2.5em;
text-shadow: 2px 2px 4px rgba(0, 0, 0, 0.5);
}}
.code-display {{
background: rgba(0, 0, 0, 0.6);
padding: 20px;
border-radius: 10px;
margin: 20px 0;
font-family: 'Courier New', monospace;
font-size: 16px;
border-left: 4px solid #4CAF50;
word-break: break-all;
text-align: left;
}}
.controls {{
margin: 30px 0;
display: flex;
gap: 15px;
justify-content: center;
flex-wrap: wrap;
}}
button {{
padding: 12px 24px;
font-size: 16px;
font-weight: bold;
background: linear-gradient(45deg, #4CAF50, #45a049);
color: white;
border: none;
border-radius: 25px;
cursor: pointer;
transition: all 0.3s ease;
box-shadow: 0 4px 15px rgba(76, 175, 80, 0.3);
min-width: 100px;
}}
button:hover:not(:disabled) {{
transform: translateY(-2px);
box-shadow: 0 6px 20px rgba(76, 175, 80, 0.4);
}}
button:disabled {{
background: #666;
cursor: not-allowed;
transform: none;
box-shadow: none;
}}
.status {{
font-size: 18px;
font-weight: bold;
margin: 20px 0;
padding: 10px;
border-radius: 5px;
transition: all 0.3s ease;
}}
.playing {{
background: rgba(76, 175, 80, 0.2);
color: #4CAF50;
}}
.stopped {{
background: rgba(244, 67, 54, 0.2);
color: #f44336;
}}
.info {{
margin-top: 20px;
font-size: 14px;
opacity: 0.8;
line-height: 1.5;
}}
.error {{
background: rgba(244, 67, 54, 0.2);
color: #f44336;
padding: 10px;
border-radius: 5px;
margin: 10px 0;
}}
</style>
</head>
<body>
<div class="container">
<h1>🎡 Strudel Player</h1>
<div class="code-display">
<strong>Generated Pattern:</strong><br>
<code id="codeDisplay">{strudel_code}</code>
</div>
<div class="controls">
<button id="playBtn" onclick="playMusic()">▢️ Play</button>
<button id="pauseBtn" onclick="pauseMusic()" disabled>⏸️ Pause</button>
<button id="stopBtn" onclick="stopMusic()" disabled>⏹️ Stop</button>
</div>
<div id="status" class="status stopped">Click Play to start audio</div>
<div class="info">
<p>🎧 Use headphones for best experience</p>
<p>⚑ Powered by Web Audio API & Tone.js</p>
</div>
</div>
<script>
// Simple audio player using Tone.js as fallback
let isPlaying = false;
let audioContext = null;
let currentPattern = null;
// Initialize audio context on first user interaction
async function initAudio() {{
try {{
if (!audioContext) {{
await Tone.start();
audioContext = Tone.getContext();
updateStatus('Audio initialized - Ready to play', 'stopped');
}}
return true;
}} catch (error) {{
console.error('Audio initialization failed:', error);
updateStatus('Audio initialization failed: ' + error.message, 'error');
return false;
}}
}}
// Simple pattern interpreter (basic Strudel-inspired)
function interpretPattern(code) {{
try {{
// Very basic pattern parsing - in a real implementation you'd want full Strudel
const synth = new Tone.Synth().toDestination();
const player = new Tone.Player().toDestination();
// Simple note pattern recognition
const noteMatch = code.match(/note\\(["']([^"']+)["']\\)/);
if (noteMatch) {{
const notes = noteMatch[1].split(' ');
let time = 0;
const interval = 0.5; // Default timing
// Schedule notes
notes.forEach((note, index) => {{
if (note.trim()) {{
Tone.Transport.schedule((time) => {{
synth.triggerAttackRelease(note + '4', '8n', time);
}}, time);
time += interval;
}}
}});
return {{ duration: time, type: 'notes' }};
}}
// Simple drum pattern recognition
const drumMatch = code.match(/s\\(["']([^"']+)["']\\)/);
if (drumMatch) {{
// Simulate basic drum sounds with synth
const kicks = drumMatch[1].split(' ');
let time = 0;
const interval = 0.25;
kicks.forEach((drum, index) => {{
if (drum.includes('bd')) {{
Tone.Transport.schedule((time) => {{
synth.triggerAttackRelease('C2', '8n', time);
}}, time);
}} else if (drum.includes('sn')) {{
Tone.Transport.schedule((time) => {{
synth.triggerAttackRelease('E4', '16n', time);
}}, time);
}} else if (drum.includes('hh')) {{
Tone.Transport.schedule((time) => {{
synth.triggerAttackRelease('A5', '32n', time);
}}, time);
}}
time += interval;
}});
return {{ duration: time, type: 'drums' }};
}}
// Fallback - play a simple melody
const melody = ['C4', 'D4', 'E4', 'F4'];
melody.forEach((note, index) => {{
Tone.Transport.schedule((time) => {{
synth.triggerAttackRelease(note, '4n', time);
}}, index * 0.5);
}});
return {{ duration: 2, type: 'fallback' }};
}} catch (error) {{
console.error('Pattern interpretation error:', error);
updateStatus('Pattern error: ' + error.message, 'error');
return null;
}}
}}
async function playMusic() {{
try {{
const audioReady = await initAudio();
if (!audioReady) return;
if (isPlaying) return;
updateStatus('🎡 Playing pattern...', 'playing');
const code = document.getElementById('codeDisplay').textContent;
currentPattern = interpretPattern(code);
if (currentPattern) {{
Tone.Transport.start();
isPlaying = true;
updateControls();
// Auto-stop after pattern duration
setTimeout(() => {{
if (isPlaying) {{
stopMusic();
}}
}}, (currentPattern.duration + 1) * 1000);
}} else {{
updateStatus('Failed to parse pattern', 'error');
}}
}} catch (error) {{
console.error('Playback error:', error);
updateStatus('Playback error: ' + error.message, 'error');
}}
}}
function pauseMusic() {{
if (isPlaying) {{
Tone.Transport.pause();
isPlaying = false;
updateStatus('⏸️ Paused', 'stopped');
updateControls();
}}
}}
function stopMusic() {{
Tone.Transport.stop();
Tone.Transport.cancel();
isPlaying = false;
currentPattern = null;
updateStatus('⏹️ Stopped', 'stopped');
updateControls();
}}
function updateControls() {{
document.getElementById('playBtn').disabled = isPlaying;
document.getElementById('pauseBtn').disabled = !isPlaying;
document.getElementById('stopBtn').disabled = !isPlaying;
}}
function updateStatus(message, type) {{
const status = document.getElementById('status');
status.textContent = message;
status.className = `status ${{type}}`;
}}
// Cleanup on page unload
window.addEventListener('beforeunload', () => {{
if (isPlaying) {{
stopMusic();
}}
}});
// Handle visibility changes
document.addEventListener('visibilitychange', () => {{
if (document.hidden && isPlaying) {{
pauseMusic();
}}
}});
console.log('Strudel player initialized');
</script>
</body>
</html>"""
def generate_and_play(music_style: str, api_token: Optional[str] = None):
"""Generate Strudel code and return playable HTML"""
if not music_style.strip():
return "❌ Please enter a music style description.", "<p>Enter a music style to generate audio</p>"
try:
# Generate the Strudel code
strudel_code = generate_strudel_code(music_style, api_token)
# Create HTML file
html_content = create_strudel_html(strudel_code)
# Save to temporary file for Gradio
with tempfile.NamedTemporaryFile(mode='w', suffix='.html', delete=False) as f:
f.write(html_content)
html_file_path = f.name
success_msg = f"βœ… Generated Strudel pattern: `{strudel_code}`\n\n🎡 Click Play in the player below to hear your music!"
return success_msg, html_content
except Exception as e:
error_msg = f"❌ Error generating music: {str(e)}"
return error_msg, "<p>Error generating audio player</p>"
def create_interface():
"""Create the Gradio interface"""
# Custom CSS for better appearance
custom_css = """
.gradio-container {
max-width: 900px !important;
margin: auto !important;
}
.main-header {
text-align: center;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
font-size: 2.5em;
font-weight: bold;
margin-bottom: 20px;
}
.examples-section {
background: #f8f9fa;
border-radius: 10px;
padding: 15px;
margin: 10px 0;
}
"""
with gr.Blocks(
title="🎡 Strudel Music Generator",
theme=gr.themes.Soft(primary_hue="blue", secondary_hue="purple"),
css=custom_css
) as demo:
gr.HTML("""
<div class="main-header">
🎡 Strudel Music Generator
</div>
""")
gr.Markdown("""
Generate algorithmic music patterns using [Strudel.cc](https://strudel.cc/) syntax!
**How to use:**
1. 🎼 Describe your desired music style
2. πŸ”‘ (Optional) Add your Hugging Face API token for AI-generated patterns
3. 🎡 Click "Generate Music" to create your pattern
4. ▢️ Use the audio player controls to play your music
""")
with gr.Row():
with gr.Column(scale=2):
music_input = gr.Textbox(
label="🎼 Music Style Description",
placeholder="Try: 'jazz piano', 'techno beats', 'ambient soundscape', 'funky bass'...",
lines=2,
max_lines=3
)
api_token_input = gr.Textbox(
label="πŸ”‘ Hugging Face API Token (Optional)",
placeholder="hf_... (for AI-generated patterns)",
type="password",
info="Get your free token at https://huggingface.co/settings/tokens"
)
generate_btn = gr.Button(
"🎡 Generate Music",
variant="primary",
size="lg"
)
with gr.Column(scale=1):
gr.Markdown("""
<div class="examples-section">
### 🎨 Style Examples:
- **Jazz**: swing, bebop, smooth
- **Electronic**: techno, house, ambient
- **Classical**: piano, strings, orchestral
- **Urban**: hip hop, reggae, funk
- **Rock**: blues, metal, indie
- **Experimental**: minimal, drone, glitch
</div>
""")
with gr.Row():
code_output = gr.Textbox(
label="πŸ“ Generated Code & Status",
interactive=False,
lines=4,
show_copy_button=True
)
with gr.Row():
audio_player = gr.HTML(
label="🎡 Interactive Audio Player",
value="<div style='text-align: center; padding: 40px; background: #f0f0f0; border-radius: 10px;'><h3>🎼 Generate music to see the player</h3><p>Your interactive audio player will appear here</p></div>",
elem_id="audio-player"
)
# Event handlers
generate_btn.click(
fn=generate_and_play,
inputs=[music_input, api_token_input],
outputs=[code_output, audio_player],
show_progress=True
)
# Enter key support
music_input.submit(
fn=generate_and_play,
inputs=[music_input, api_token_input],
outputs=[code_output, audio_player]
)
# Example interactions
gr.Examples(
examples=[
["jazz piano with swing rhythm"],
["minimal techno beat"],
["ambient electronic soundscape"],
["funky bass line groove"],
["classical piano arpeggios"],
["drum and bass breakbeat"],
["reggae dub rhythm"],
["experimental glitch sounds"]
],
inputs=[music_input],
label="🎯 Quick Start Examples"
)
gr.Markdown("""
---
### πŸŽ“ About Strudel
**Strudel** is a live coding language for algorithmic music composition. This app generates Strudel patterns and plays them using Web Audio API.
**Key Functions:**
- `note("c d e f")` - Play musical notes
- `s("bd hh sn")` - Play drum samples (bd=bass drum, hh=hi-hat, sn=snare)
- `sound("piano")` - Use instrument sounds
- `.slow(2)` / `.fast(2)` - Change tempo
- `.stack()` - Layer multiple patterns
- `.lpf(800)` - Low-pass filter
- `.gain(0.5)` - Volume control
**πŸ”§ Technical Notes:**
- Audio runs entirely in your browser
- No audio files needed - everything is synthesized
- Works best with modern browsers and headphones
- Patterns are simplified for web compatibility
**πŸš€ Want full Strudel?** Visit [strudel.cc](https://strudel.cc/) for the complete live coding environment!
""")
return demo
# Launch configuration
if __name__ == "__main__":
demo = create_interface()
demo.launch(
server_name="0.0.0.0",
server_port=7860,
share=False,
show_error=True
)