Spaces:
Sleeping
Sleeping
import gradio as gr | |
import random | |
import re | |
import json | |
# --- Strudel Code Generation and Playback Functions --- | |
def generate_simple_beat(): | |
"""Generates a very simple Strudel beat as a starting point.""" | |
code = '''// ๐ต Simple Voice-Generated Beat | |
// Try saying: "Make a beat" or "Play music" | |
// Then click PLAY or press Ctrl+Enter! | |
stack( | |
sound("bd").struct("x ~ x ~").gain(0.8), | |
sound("sd").struct("~ x ~ x").gain(0.7), | |
sound("hh").struct("x x x x").gain(0.4) | |
).cpm(120)''' | |
return "โ Simple beat generated!", code | |
def clear_code(): | |
"""Clears the Strudel code editor to a default state.""" | |
initial_code = '''// ๐ต Voice Strudel Synth | |
// Speak a command to generate music! | |
// Try: "Make a beat" | |
// Then: "Play music" (or Ctrl+Enter) | |
stack( | |
// Your generated code will appear here... | |
).cpm(120) // Master tempo | |
''' | |
return "๐๏ธ Code cleared - Ready for new ideas!", initial_code | |
# --- Voice Command Processing --- | |
def process_voice_command(command_text, current_code): | |
""" | |
Processes a voice command to generate or control Strudel code. | |
This version is highly simplified for bare-minimum functionality. | |
""" | |
if not command_text: | |
return "โ No voice command received.", current_code | |
command_lower = command_text.lower() | |
if any(word in command_lower for word in ["make a beat", "generate beat", "create music", "new beat"]): | |
return generate_simple_beat() | |
elif "play music" in command_lower or "start music" in command_lower: | |
# This message will instruct the user to use the play button/shortcut | |
return "โถ๏ธ Click PLAY MUSIC or press Ctrl+Enter to hear!", current_code | |
elif "stop music" in command_lower: | |
# This message will instruct the user to use the stop button/shortcut | |
return "โน๏ธ Click STOP MUSIC or press Ctrl+Space!", current_code | |
elif "clear code" in command_lower or "reset" in command_lower or "start over" in command_lower: | |
return clear_code() | |
else: | |
return "๐ค Command not recognized. Try: 'Make a beat', 'Play music', 'Stop music', 'Clear code'.", current_code | |
# --- Gradio Interface Setup --- | |
def create_interface(): | |
# --- Simplified CSS for a clean, futuristic look --- | |
custom_css = """ | |
@import url('https://fonts.googleapis.com/css2?family=Share+Tech+Mono&display=swap'); | |
@import url('https://fonts.googleapis.com/css2?family=Inter:wght@400;700&display=swap'); | |
body { | |
font-family: 'Inter', sans-serif; | |
overflow: hidden; /* Hide scrollbars due to matrix rain */ | |
background: #000; | |
} | |
.gradio-container { | |
background: linear-gradient(135deg, #0a0a0a, #1a1a1a); | |
color: #00ff00; | |
font-family: 'Share Tech Mono', monospace !important; | |
min-height: 100vh; | |
border-radius: 15px; | |
box-shadow: 0 0 50px rgba(0, 255, 0, 0.3); | |
padding: 20px; | |
position: relative; | |
z-index: 1; | |
display: flex; | |
flex-direction: column; | |
align-items: center; | |
justify-content: center; | |
} | |
h1 { | |
font-size: 3.5em; | |
color: #00ff00; | |
text-shadow: 0 0 15px rgba(0, 255, 0, 0.7); | |
animation: neon-flicker 1.5s infinite alternate; | |
text-align: center; | |
margin-bottom: 5px; | |
} | |
p { | |
color: #00ee00; | |
font-size: 1.5em; | |
text-shadow: 0 0 10px #00ff00; | |
text-align: center; | |
margin-top: 0; | |
margin-bottom: 30px; | |
} | |
@keyframes neon-flicker { | |
0% { opacity: 1; text-shadow: 0 0 15px rgba(0, 255, 0, 0.7); } | |
100% { opacity: 0.9; text-shadow: 0 0 20px rgba(0, 255, 0, 0.9), 0 0 30px rgba(0, 255, 0, 0.5); } | |
} | |
.gr-textbox, .gr-code { | |
background: rgba(0, 10, 0, 0.7) !important; | |
border: 1px solid #00ff00 !important; | |
color: #00ff41 !important; | |
border-radius: 10px !important; | |
box-shadow: inset 0 0 8px rgba(0, 255, 0, 0.3); | |
padding: 15px; | |
margin-bottom: 20px; | |
width: 100%; | |
} | |
.gr-code textarea { | |
background: rgba(0, 0, 0, 0.9) !important; | |
color: #00ff41 !important; | |
font-family: 'Fira Code', 'Share Tech Mono', monospace !important; | |
font-size: 15px !important; | |
line-height: 1.4; | |
} | |
.gr-button { | |
background: linear-gradient(45deg, #008800, #00bb00) !important; | |
border: 2px solid #33ff33 !important; | |
color: #00ff00 !important; | |
font-weight: bold !important; | |
text-shadow: 0 0 10px #00ff00 !important; | |
border-radius: 8px !important; | |
transition: all 0.2s ease-in-out !important; | |
box-shadow: 0 0 10px rgba(51, 255, 51, 0.7); | |
padding: 12px 25px; | |
text-transform: uppercase; | |
letter-spacing: 1px; | |
font-size: 1.1em; | |
margin: 5px; | |
} | |
.gr-button:hover { | |
background: linear-gradient(45deg, #00bb00, #00ee00) !important; | |
box-shadow: 0 0 30px rgba(51, 255, 51, 1); | |
transform: translateY(-2px) scale(1.02); | |
} | |
#voice-status { | |
background: rgba(0, 30, 0, 0.5) !important; | |
border: 2px solid #00ff00 !important; | |
border-radius: 12px !important; | |
padding: 15px !important; | |
text-align: center; | |
font-size: 1.2em; | |
font-weight: bold; | |
box-shadow: 0 0 15px rgba(0, 255, 0, 0.5); | |
animation: pulse 2s infinite alternate; | |
} | |
.listening { | |
animation: listening-pulse 1s infinite alternate !important; | |
background: rgba(100, 0, 0, 0.3) !important; | |
border-color: #ff0000 !important; | |
box-shadow: 0 0 25px rgba(255, 0, 0, 0.7) !important; | |
} | |
@keyframes pulse { | |
0% { opacity: 1; box-shadow: 0 0 15px rgba(0, 255, 0, 0.5); } | |
100% { opacity: 0.8; box-shadow: 0 0 25px rgba(0, 255, 0, 0.8); } | |
} | |
@keyframes listening-pulse { | |
0% { box-shadow: 0 0 15px #ff0000; } | |
100% { box-shadow: 0 0 40px #ff0000; } | |
} | |
.instructions { | |
background: rgba(0, 15, 0, 0.6) !important; | |
border: 1px dashed #00ff00 !important; | |
border-radius: 15px !important; | |
padding: 25px !important; | |
margin: 30px auto; /* Centered */ | |
max-width: 800px; | |
box-shadow: 0 0 20px rgba(0, 255, 0, 0.2); | |
} | |
.instructions h3 { | |
color: #33ff33; | |
text-shadow: none; | |
text-align: center; | |
margin-bottom: 15px; | |
} | |
.instructions ul { | |
list-style: none; | |
padding-left: 0; | |
margin-bottom: 15px; | |
} | |
.instructions ul li::before { | |
content: 'ยป '; | |
color: #00ff00; | |
font-weight: bold; | |
margin-right: 5px; | |
} | |
.instructions p { | |
font-size: 1em; | |
color: #ccffcc; | |
text-shadow: none; | |
} | |
#matrix-canvas { | |
position: fixed; | |
top: 0; | |
left: 0; | |
width: 100vw; | |
height: 100vh; | |
background: radial-gradient(ellipse at center, rgba(0,20,0,0.8) 0%, rgba(0,0,0,0.9) 100%); | |
z-index: -2; | |
pointer-events: none; | |
} | |
.gr-row, .gr-column { | |
width: 100%; /* Make rows/columns take full width */ | |
max-width: 900px; /* Constrain main content width */ | |
margin: 0 auto; /* Center content */ | |
} | |
.main-controls { | |
display: flex; | |
flex-direction: column; | |
align-items: center; | |
margin-bottom: 20px; | |
} | |
.control-buttons { | |
display: flex; | |
justify-content: center; | |
width: 100%; | |
} | |
""" | |
# --- JavaScript for Voice Recognition and Strudel.js Integration --- | |
voice_strudel_js = """ | |
function() { | |
console.log('๐ต Initializing Voice Recognition and Strudel.js...'); | |
let recognition = null; | |
let isListening = false; | |
let strudelPlayer = null; | |
let strudelReady = false; | |
// Initialize Strudel.js Player | |
async function initStrudel() { | |
if (strudelReady) { | |
console.log("๐ถ Strudel.js already initialized."); | |
return true; | |
} | |
try { | |
// Using a direct import from unpkg for the 'strudel' package itself | |
const { default: Strudel } = await import('https://unpkg.com/strudel/strudel.js'); | |
// Create or resume AudioContext | |
if (!window.audioContext || window.audioContext.state === 'closed') { | |
window.audioContext = new (window.AudioContext || window.webkitAudioContext)(); | |
console.log("New AudioContext created."); | |
} | |
// Attempt to resume audio context, crucial for autoplay policies | |
if (window.audioContext.state === 'suspended') { | |
await window.audioContext.resume(); | |
console.log("AudioContext resumed during initialization."); | |
} | |
strudelPlayer = Strudel.Player({ audioContext: window.audioContext }); // Pass the existing context | |
strudelReady = true; | |
console.log("๐ถ Strudel.js player created and initialized successfully!"); | |
return true; | |
} catch (error) { | |
console.error("Failed to load or initialize Strudel.js:", error); | |
const statusElement = document.querySelector('#audio-status textarea'); | |
if (statusElement) { | |
statusElement.value = `โ Audio Engine Error: ${error.message}. Check browser console for details.`; | |
} | |
return false; | |
} | |
} | |
// Check if browser supports speech recognition | |
if ('webkitSpeechRecognition' in window || 'SpeechRecognition' in window) { | |
const SpeechRecognition = window.SpeechRecognition || window.webkitSpeechRecognition; | |
recognition = new SpeechRecognition(); | |
recognition.continuous = false; | |
recognition.interimResults = false; | |
recognition.lang = 'en-US'; | |
recognition.maxAlternatives = 1; | |
console.log('โ Speech Recognition Available'); | |
} else { | |
console.log('โ Speech Recognition Not Available. Please use a Chromium-based browser (Chrome, Edge).'); | |
} | |
// Matrix rain effect (simplified and integrated) | |
function createMatrixRain() { | |
const existingCanvas = document.querySelector('#matrix-canvas'); | |
if (existingCanvas) existingCanvas.remove(); | |
const canvas = document.createElement('canvas'); | |
canvas.id = 'matrix-canvas'; | |
canvas.style.cssText = ` | |
position: fixed; top: 0; left: 0; width: 100vw; height: 100vh; | |
background: radial-gradient(ellipse at center, rgba(0,20,0,0.8) 0%, rgba(0,0,0,0.9) 100%); | |
z-index: -2; pointer-events: none; | |
`; | |
document.body.appendChild(canvas); | |
const ctx = canvas.getContext('2d'); | |
const chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789โชโซโฌโฉ๐ต๐ถโก๐๐ค๐ง'; | |
const fontSize = 16; | |
let columns, drops; | |
function resizeCanvas() { | |
canvas.width = window.innerWidth; | |
canvas.height = window.innerHeight; | |
columns = Math.floor(canvas.width / fontSize); | |
drops = Array(columns).fill(1); | |
} | |
resizeCanvas(); | |
window.addEventListener('resize', resizeCanvas); | |
function draw() { | |
ctx.fillStyle = 'rgba(0, 0, 0, 0.08)'; | |
ctx.fillRect(0, 0, canvas.width, canvas.height); | |
const gradient = ctx.createLinearGradient(0, 0, 0, canvas.height); | |
gradient.addColorStop(0, '#00ff41'); | |
gradient.addColorStop(0.5, '#00aa00'); | |
gradient.addColorStop(1, 'rgba(0, 255, 0, 0.1)'); | |
ctx.fillStyle = gradient; | |
ctx.font = `${fontSize}px 'Share Tech Mono', monospace`; | |
for (let i = 0; i < drops.length; i++) { | |
const text = chars[Math.floor(Math.random() * chars.length)]; | |
const x = i * fontSize; | |
const y = drops[i] * fontSize; | |
if (text.match(/[โชโซโฌโฉ๐ต๐ถโก๐๐ค๐ง]/)) { | |
ctx.shadowColor = '#00ff41'; ctx.shadowBlur = 20; | |
} else { ctx.shadowBlur = 0; } | |
ctx.fillText(text, x, y); | |
if (y * fontSize > canvas.height && Math.random() > 0.975) { | |
drops[i] = 0; | |
} | |
drops[i]++; | |
} | |
} | |
setInterval(draw, 60); | |
} | |
createMatrixRain(); // Initialize matrix effect | |
// Start voice recognition | |
window.startVoiceRecognition = function() { | |
if (!recognition) { | |
return "โ Speech recognition not supported in this browser. Try Chrome/Edge."; | |
} | |
if (isListening) { | |
return "๐ค Already listening..."; | |
} | |
isListening = true; | |
recognition.onstart = function() { | |
console.log('๐ค Voice recognition started'); | |
const statusElement = document.querySelector('#voice-status textarea'); | |
if (statusElement) { | |
statusElement.value = "๐ค LISTENING... Speak your command now!"; | |
statusElement.parentElement.classList.add('listening'); | |
} | |
}; | |
recognition.onresult = function(event) { | |
const transcript = event.results[0][0].transcript; | |
console.log('๐ฃ๏ธ Voice command:', transcript); | |
const voiceInputs = document.querySelectorAll('textarea'); | |
for (let input of voiceInputs) { | |
if (input.placeholder && input.placeholder.includes('Voice commands')) { | |
input.value = transcript; | |
input.dispatchEvent(new Event('input', { bubbles: true })); | |
break; | |
} | |
} | |
setTimeout(() => { | |
const processBtn = Array.from(document.querySelectorAll('button')).find( | |
button => button.textContent.includes('PROCESS') || button.querySelector('span')?.textContent.includes('PROCESS') | |
); | |
if (processBtn) processBtn.click(); | |
}, 100); | |
}; | |
recognition.onerror = function(event) { | |
console.error('๐ซ Voice recognition error:', event.error); | |
const statusElement = document.querySelector('#voice-status textarea'); | |
if (statusElement) { | |
statusElement.value = `โ Error: ${event.error}. Please try again.`; | |
statusElement.parentElement.classList.remove('listening'); | |
} | |
isListening = false; | |
}; | |
recognition.onend = function() { | |
console.log('๐ค Voice recognition ended'); | |
const statusElement = document.querySelector('#voice-status textarea'); | |
if (statusElement) { | |
if (statusElement.value.includes('LISTENING')) { | |
statusElement.value = "โ Voice command captured! Processing..."; | |
} | |
statusElement.parentElement.classList.remove('listening'); | |
} | |
isListening = false; | |
}; | |
try { | |
recognition.start(); | |
return "๐ค Voice recognition started - speak now!"; | |
} catch (error) { | |
console.error('Failed to start recognition:', error); | |
isListening = false; | |
return "โ Failed to start voice recognition"; | |
} | |
}; | |
// Stop voice recognition | |
window.stopVoiceRecognition = function() { | |
if (recognition && isListening) { | |
recognition.stop(); | |
isListening = false; | |
return "โน๏ธ Voice recognition stopped"; | |
} | |
return "โน๏ธ Voice recognition not active"; | |
}; | |
// --- Strudel Playback Functions --- | |
window.playStrudelCode = async function(code) { | |
const isInitialized = await initStrudel(); | |
if (!isInitialized) { | |
return "โ Strudel audio engine could not be initialized."; | |
} | |
try { | |
if (strudelPlayer && strudelPlayer.stop) { | |
strudelPlayer.stop(); | |
console.log("Previous Strudel pattern stopped."); | |
} | |
if (window.audioContext && window.audioContext.state === 'suspended') { | |
await window.audioContext.resume(); | |
console.log("AudioContext resumed on user interaction for playback."); | |
} | |
if (strudelPlayer && strudelPlayer.setSynth && strudelPlayer.play) { | |
await strudelPlayer.setSynth(code); | |
strudelPlayer.play(); | |
console.log("โถ๏ธ Playing Strudel code."); | |
return "โถ๏ธ Playing music..."; | |
} else { | |
console.error("Strudel player methods (setSynth, play) not available."); | |
return "โ Strudel player not fully ready for playback."; | |
} | |
} catch (error) { | |
console.error("Error playing Strudel code:", error); | |
let errorMessage = `โ Audio Error: ${error.message}.`; | |
if (error.message.includes("Unexpected token") || error.message.includes("SyntaxError")) { | |
errorMessage = "โ Strudel code syntax error. Check code!"; | |
} else if (error.message.includes("Failed to load module")) { | |
errorMessage = "โ Strudel.js library failed to load. Check browser console for network/CORS issues."; | |
} else if (error.message.includes("AudioContext") || error.message.includes("Web Audio API")) { | |
errorMessage = "โ Browser audio engine issue. Try refreshing."; | |
} | |
return errorMessage; | |
} | |
}; | |
window.stopStrudelCode = function() { | |
if (strudelPlayer && strudelReady && strudelPlayer.stop) { | |
strudelPlayer.stop(); | |
console.log("โน๏ธ Strudel music stopped."); | |
return "โน๏ธ Music stopped"; | |
} | |
return "โน๏ธ Music not active"; | |
}; | |
// Add keyboard shortcuts | |
document.addEventListener('keydown', function(e) { | |
if (e.ctrlKey || e.metaKey) { // Ctrl for Windows/Linux, Cmd for Mac | |
switch(e.key.toLowerCase()) { | |
case 'enter': | |
e.preventDefault(); | |
const playBtn = Array.from(document.querySelectorAll('button')).find( | |
button => button.textContent.includes('PLAY') || button.querySelector('span')?.textContent.includes('PLAY') | |
); | |
if (playBtn) playBtn.click(); | |
break; | |
case ' ': | |
e.preventDefault(); | |
const stopBtn = Array.from(document.querySelectorAll('button')).find( | |
button => button.textContent.includes('STOP') || button.querySelector('span')?.textContent.includes('STOP') | |
); | |
if (stopBtn) stopBtn.click(); | |
break; | |
case 'm': | |
e.preventDefault(); | |
window.startVoiceRecognition(); | |
break; | |
} | |
} | |
}); | |
console.log('๐ต Voice-Controlled Strudel Generator Ready!'); | |
console.log('๐ผ Shortcuts: Ctrl+Enter (Play), Ctrl+Space (Stop), Ctrl+M (Voice)'); | |
return "๐ต Real Voice Recognition and Strudel.js Initialized!"; | |
} | |
""" | |
# --- Gradio Interface Layout --- | |
with gr.Blocks(css=custom_css, js=voice_strudel_js, title="๐ต Voice Strudel Synth") as interface: | |
# Header Section | |
gr.HTML(""" | |
<div style="text-align: center; padding: 20px;"> | |
<h1>๐ต VOICE STRUDEL SYNTH</h1> | |
<p>Speak Your Beats Into Existence โ Code, See, Hear!</p> | |
</div> | |
""") | |
with gr.Column(elem_classes="main-controls"): | |
# Voice Input and Status | |
voice_status = gr.Textbox( | |
value="๐ค Ready - Click 'Start Voice' or Ctrl+M!", | |
label="VOICE STATUS", | |
elem_id="voice-status", | |
interactive=False, | |
max_lines=2 | |
) | |
with gr.Row(elem_classes="control-buttons"): | |
start_voice_btn = gr.Button("๐ค START VOICE", variant="primary") | |
stop_voice_btn = gr.Button("โน๏ธ STOP VOICE", variant="secondary") | |
voice_input = gr.Textbox( | |
label="๐ฃ๏ธ CAPTURED VOICE COMMAND", | |
placeholder="Voice commands appear here automatically...", | |
lines=1, # Simplified to 1 line for minimal display | |
interactive=False | |
) | |
process_voice_btn = gr.Button("๐ฏ PROCESS VOICE COMMAND", variant="primary") | |
gr.Markdown("---") # Separator | |
# Audio Status and Controls | |
audio_status = gr.Textbox( | |
value="โน๏ธ Ready to play", | |
label="๐ AUDIO STATUS", | |
interactive=False, | |
max_lines=2 | |
) | |
with gr.Row(elem_classes="control-buttons"): | |
play_btn = gr.Button("โถ๏ธ PLAY MUSIC", variant="primary") | |
stop_btn = gr.Button("โน๏ธ STOP MUSIC", variant="secondary") | |
# Strudel Code Editor | |
music_code = gr.Code( | |
value='''// ๐ต Voice Strudel Synth | |
// Speak a command to generate music! | |
// Try: "Make a beat" | |
// Then: "Play music" (or Ctrl+Enter) | |
stack( | |
// Your generated code will appear here... | |
).cpm(120) // Master tempo | |
''', | |
label="๐ผ STRUDEL CODE EDITOR", | |
language="javascript", # Strudel is JavaScript-like | |
lines=15, # Reduced lines for more compact view | |
interactive=True | |
) | |
# Instructions Section (Simplified) | |
gr.HTML(""" | |
<div class="instructions"> | |
<h3>HOW TO USE</h3> | |
<ul> | |
<li>Click <strong>"START VOICE"</strong> or press <strong>Ctrl+M</strong>.</li> | |
<li>Say: <strong>"Make a beat"</strong></li> | |
<li>Then: <strong>"Play music"</strong> (or click <strong>"PLAY MUSIC"</strong> / <strong>Ctrl+Enter</strong>)</li> | |
<li>To stop: <strong>"Stop music"</strong> (or click <strong>"STOP MUSIC"</strong> / <strong>Ctrl+Space</strong>)</li> | |
<li>To clear: <strong>"Clear code"</strong></li> | |
</ul> | |
<p style="text-align: center;">๐ค IMPORTANT: Allow microphone access when prompted!</p> | |
</div> | |
""") | |
# --- Event Handlers (Python functions triggered by Gradio events) --- | |
start_voice_btn.click( | |
fn=lambda: "๐ค Voice recognition started - speak now!", | |
outputs=voice_status, | |
js="() => window.startVoiceRecognition()" | |
) | |
stop_voice_btn.click( | |
fn=lambda: "โน๏ธ Voice recognition stopped", | |
outputs=voice_status, | |
js="() => window.stopVoiceRecognition()" | |
) | |
process_voice_btn.click( | |
fn=process_voice_command, | |
inputs=[voice_input, music_code], | |
outputs=[voice_status, music_code] | |
) | |
# This handles the play button click, which also triggers JS play function | |
def handle_play_button_click_status(js_status_message): # Only receive the status message from JS | |
return js_status_message | |
play_btn.click( | |
fn=handle_play_button_click_status, | |
inputs=[music_code], # music_code is passed to the JS function, not the Python fn directly | |
outputs=audio_status, | |
js="(code) => window.playStrudelCode(code)" # Pass code to JS, JS handles audio and returns status | |
) | |
stop_btn.click( | |
fn=lambda: "โน๏ธ Music stopped", | |
outputs=audio_status, | |
js="() => window.stopStrudelCode()" | |
) | |
clear_btn = gr.Button("๐๏ธ Clear Code", variant="secondary") | |
clear_btn.click(fn=clear_code, outputs=[voice_status, music_code]) | |
return interface | |
# --- Main execution block for Gradio --- | |
if __name__ == "__main__": | |
create_interface().launch() |