Image-to-Sound & Creative Intelligence
Upload any image. LYGO RESONANCE analyzes its geometry, color, and texture to generate rich stereo soundscapes or structured creative profiles for music and storytelling.
Any image works โ manuscripts, drawings, photos, abstract art, or quick sketches.
Computer vision reads edges, contours, brightness, color, and structural density to extract resonant musical data.
Resonance Engine โ High-quality stereo WAV audio
Profile Generator โ Structured JSON + Creative Brief for AI music tools
Generate unique cinematic soundscapes or ready-to-use prompts for Suno, Udio, or your DAW.
Each image generates a unique sound or JSON profile
LYGO RESONANCE is a two-part creative tool that uses computer vision to turn any image into musical or creative data. It's designed for artists, musicians, and creators who want to find hidden sonic meaning in visual art.
This script generates a rich, stereo WAV soundscape based on an image's geometry, color, and texture. It creates four sonic layers:
Reproducibility: Without --seed, each run produces a slightly different result (like a fresh improvisation). With --seed, the output is exactly the same every time.
python resonance_engine.py image.jpg --style cinematic --seed 42
Use --seed when you want to lock in a specific output (e.g., for an album track or collaboration). Skip the seed for endless fresh variations.
Presets: cinematic, ambient, glitch, ethereal, raw
This script extracts fixed, deterministic mathematical data from an image and translates it into a structured creative profile. It includes:
Deterministic: The same image always produces the exact same JSON and Creative Brief. This is ideal for batch processing, study, or building a consistent library of creative assets.
python lygo_profile.py image.jpg --brief
The --brief flag generates a human-readable .brief.txt file in addition to the JSON.
Pro Tip: Run both engines on the same image. Listen to the audio, then read the creative brief. Let the two outputs inform each other โ the audio will feel like the "soul" and the JSON will feel like the "blueprint."
Run the visual-to-audio or LYGO profile engine on your own machine. Choose between the Resonance Engine (Stereo Audio) or Profile Generator (JSON + Creative Brief).
A friendly donation is not required, but deeply appreciated to help keep the servers running and the coffee flowing!
๐ Donate via PayPalClicking the button will instantly unlock both code modules below.
eidolon or lygo.resonance_engine.py (for audio) or lygo_profile.py (for JSON).image.jpg.cd Desktop/eidoloncd Desktop\eidoloncmd in the address bar and hit Enter to instantly open a command prompt inside your folder. On macOS, right-click the folder and select "Open in Terminal".
pip install opencv-python numpy soundfile mido gradio requestspython resonance_engine.py image.jpg --style cinematicpython lygo_profile.py image.jpg --brief#!/usr/bin/env python3
"""
LYGO Resonance Engine v0.3
Image โ Living Stereo Soundscape
A spectral translator that gives voice to the hidden geometry, texture, and color of any image.
"""
import cv2
import numpy as np
import soundfile as sf
import math
import argparse
import sys
from pathlib import Path
from typing import Optional, Dict, Any
import mido
from mido import MidiFile, MidiTrack, Message
__version__ = "0.3.0"
# Artistic Presets
PRESETS = {
"raw": {},
"ambient": {
"noise_vol": 0.055,
"drone_vol": 0.095,
"note_vol": 0.11,
"glitch_vol": 0.012,
"drone_attack": 5.5,
"drone_decay": 5.5,
"note_attack": 0.04,
"note_decay": 0.35,
"max_glitches": 10,
"noise_lowpass_hz": 650,
},
"glitch": {
"noise_vol": 0.16,
"drone_vol": 0.06,
"note_vol": 0.09,
"glitch_vol": 0.07,
"max_notes": 8,
"max_glitches": 50,
"note_decay": 0.10,
"glitch_decay": 0.008,
"noise_lowpass_hz": 2800,
},
"ethereal": {
"noise_vol": 0.04,
"drone_vol": 0.08,
"note_vol": 0.14,
"glitch_vol": 0.02,
"root_freq_range": (35, 95),
"theta_lock_range": (6, 14),
"note_attack": 0.06,
"note_decay": 0.45,
"noise_lowpass_hz": 450,
},
"cinematic": {
"noise_vol": 0.07,
"drone_vol": 0.11,
"note_vol": 0.13,
"glitch_vol": 0.025,
"drone_attack": 4.0,
"drone_decay": 4.5,
"max_drones": 5,
"noise_lowpass_hz": 900,
},
}
class ResonanceEngine:
def __init__(self, config: Optional[Dict[str, Any]] = None):
self.config = {
"sr": 44100,
"duration": 15.0,
"global_fade": 0.7,
"soft_clip": True,
"soft_clip_amount": 1.7,
"max_drones": 6,
"max_notes": 12,
"max_glitches": 30,
"noise_vol": 0.095,
"drone_vol": 0.075,
"note_vol": 0.15,
"glitch_vol": 0.032,
"root_freq_range": (28, 72),
"theta_lock_range": (4.5, 11),
"drone_attack": 3.2,
"drone_decay": 3.2,
"note_attack": 0.022,
"note_decay": 0.20,
"glitch_attack": 0.003,
"glitch_decay": 0.011,
"noise_lowpass_hz": 0,
"random_seed": None,
"verbose": True,
"export_stems": False,
"export_midi": False,
}
if config:
self.config.update(config)
def _log(self, msg: str):
if self.config.get("verbose", True):
print(msg)
def analyze_image(self, image_path: str) -> Dict[str, Any]:
img = cv2.imread(str(image_path))
if img is None:
raise FileNotFoundError(f"Could not load image: {image_path}")
if len(img.shape) == 2:
img = cv2.cvtColor(img, cv2.COLOR_GRAY2BGR)
gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
h, w = gray.shape
avg_blue, avg_green, avg_red, _ = cv2.mean(img)
edges = cv2.Canny(gray, 50, 150)
edge_density = np.sum(edges > 0) / (h * w)
contours, _ = cv2.findContours(edges, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)
lines = cv2.HoughLinesP(edges, 1, np.pi / 180, 50, minLineLength=28, maxLineGap=12)
fast = cv2.FastFeatureDetector_create(threshold=38)
keypoints = fast.detect(gray, None)
features = {
"width": w, "height": h,
"avg_red": avg_red, "avg_green": avg_green, "avg_blue": avg_blue,
"edge_density": edge_density,
"contours": contours,
"lines": lines if lines is not None else [],
"keypoints": keypoints,
}
return features
def _generate_tone(self, freq: float, duration: float, wave_type: str = "sine") -> np.ndarray:
sr = self.config["sr"]
t = np.linspace(0, duration, int(sr * duration), False)
if wave_type == "sine":
return np.sin(freq * t * 2 * np.pi).astype(np.float32)
elif wave_type == "sawtooth":
return (2 * (t * freq - np.floor(0.5 + t * freq))).astype(np.float32)
elif wave_type == "noise":
return np.random.uniform(-1.0, 1.0, len(t)).astype(np.float32)
return np.zeros(len(t), dtype=np.float32)
def _apply_envelope(self, audio: np.ndarray, attack: float, decay: float) -> np.ndarray:
sr = self.config["sr"]
a = max(1, int(attack * sr))
d = max(1, int(decay * sr))
env = np.ones_like(audio, dtype=np.float32)
if len(audio) > a + d:
env[:a] = np.linspace(0, 1, a)
env[-d:] = np.linspace(1, 0, d)
return audio * env
def _stereo_pan(self, mono: np.ndarray, pan: float) -> np.ndarray:
pan = max(-1.0, min(1.0, pan))
left = math.cos((pan + 1) * math.pi / 4)
right = math.sin((pan + 1) * math.pi / 4)
return np.column_stack((mono * left, mono * right)).astype(np.float32)
def _fft_lowpass(self, audio: np.ndarray, cutoff_hz: float) -> np.ndarray:
if cutoff_hz <= 0 or len(audio) < 32:
return audio
sr = self.config["sr"]
n = len(audio)
fft = np.fft.rfft(audio)
freqs = np.fft.rfftfreq(n, 1.0 / sr)
fft[freqs > cutoff_hz] = 0
return np.fft.irfft(fft, n=n).real.astype(np.float32)
def _soft_limit(self, audio: np.ndarray) -> np.ndarray:
if self.config["soft_clip"]:
amt = self.config["soft_clip_amount"]
return np.tanh(audio * amt) / np.tanh(amt)
return audio
def _freq_to_midi(self, freq: float) -> int:
if freq <= 0:
return 0
return max(0, min(127, int(12 * math.log2(freq / 440) + 69)))
def synthesize(self, features: Dict[str, Any], output_path: str):
cfg = self.config
if cfg["random_seed"] is not None:
np.random.seed(cfg["random_seed"])
sr = cfg["sr"]
duration = cfg["duration"]
audio = np.zeros((int(sr * duration), 2), dtype=np.float32)
root = np.interp(features["avg_red"], [0, 255], cfg["root_freq_range"])
theta = np.interp(features["avg_green"], [0, 255], cfg["theta_lock_range"])
w, h = features["width"], features["height"]
# Initialize stem collections
audio_noise = np.zeros((int(sr * duration), 2), dtype=np.float32)
audio_drone = np.zeros((int(sr * duration), 2), dtype=np.float32)
audio_melody = np.zeros((int(sr * duration), 2), dtype=np.float32)
audio_glitch = np.zeros((int(sr * duration), 2), dtype=np.float32)
melody_events = []
# Layer 1: Texture Floor
if features["edge_density"] > 0.007:
noise = self._generate_tone(0, duration, "noise")
if cfg["noise_lowpass_hz"] > 0:
noise = self._fft_lowpass(noise, cfg["noise_lowpass_hz"])
noise = self._apply_envelope(noise, cfg["drone_attack"], cfg["drone_decay"])
vol = min(features["edge_density"] * 1.6, cfg["noise_vol"])
stereo_noise = self._stereo_pan(noise, 0.0) * vol
audio += stereo_noise
audio_noise += stereo_noise
# Layer 2: Drones
for i, line in enumerate(features["lines"][:cfg["max_drones"]]):
x1, _, x2, _ = line[0]
length = math.hypot(x2 - x1, 0)
detune = (i * 0.7) if cfg["random_seed"] is not None else 0
freq = root + (max(1, int(length / 48)) * theta * 0.55) + detune
tone = self._generate_tone(freq, duration, "sawtooth")
tone = self._apply_envelope(tone, cfg["drone_attack"], cfg["drone_decay"])
pan = (x1 / w) * 2 - 1
stereo_drone = self._stereo_pan(tone, pan) * cfg["drone_vol"]
audio += stereo_drone
audio_drone += stereo_drone
# Layer 3: Contours โ Melody
valid = [c for c in features["contours"] if 90 < cv2.contourArea(c) < (w * h * 0.6)]
valid.sort(key=lambda c: cv2.boundingRect(c)[0])
for i, cnt in enumerate(valid[:cfg["max_notes"]]):
area = cv2.contourArea(cnt)
verts = len(cv2.approxPolyDP(cnt, 0.04 * cv2.arcLength(cnt, True), True))
freq = (root * 3.7) + (verts * theta * 1.6)
dur = min(2.6, 0.22 + (area / 13500))
tone = self._generate_tone(freq, dur, "sine")
tone = self._apply_envelope(tone, cfg["note_attack"], cfg["note_decay"])
M = cv2.moments(cnt)
cx = int(M["m10"] / M["m00"]) if M["m00"] != 0 else cv2.boundingRect(cnt)[0]
start = (cx / w) * (duration - dur)
idx = int(start * sr)
end = min(idx + len(tone), len(audio))
pan = (cx / w) * 2 - 1
stereo_note = self._stereo_pan(tone[:end-idx], pan) * cfg["note_vol"]
audio[idx:end] += stereo_note
audio_melody[idx:end] += stereo_note
melody_events.append((freq, dur, start))
# Layer 4: Glitch / Micro events
for i, kp in enumerate(features["keypoints"][:cfg["max_glitches"]]):
x, y = kp.pt
freq = root * 13.5 + (y % 85) * 1.4
tone = self._generate_tone(freq, 0.042, "sine")
tone = self._apply_envelope(tone, cfg["glitch_attack"], cfg["glitch_decay"])
start = (y / h) * (duration - 0.05)
idx = int(start * sr)
end = min(idx + len(tone), len(audio))
pan = (x / w) * 2 - 1
stereo_glitch = self._stereo_pan(tone[:end-idx], pan) * cfg["glitch_vol"]
audio[idx:end] += stereo_glitch
audio_glitch[idx:end] += stereo_glitch
# Final polish
audio = self._soft_limit(audio)
fade = int(cfg["global_fade"] * sr)
if fade > 0 and len(audio) > fade * 2:
audio[:fade] *= np.linspace(0, 1, fade)[:, None]
audio[-fade:] *= np.linspace(1, 0, fade)[:, None]
peak = np.max(np.abs(audio))
if peak > 0:
audio = audio / peak * 0.97
sf.write(output_path, audio, sr)
self._log(f"โ Saved: {output_path} | Peak: {peak:.3f}")
# Export Stems
if cfg.get("export_stems"):
base = output_path.replace(".wav", "")
for stem, name in [(audio_noise, "noise"), (audio_drone, "drone"),
(audio_melody, "melody"), (audio_glitch, "glitch")]:
max_val = np.max(np.abs(stem))
if max_val > 0:
stem = stem / max_val * 0.97
sf.write(f"{base}_{name}.wav", stem, sr)
self._log(f"โ Stem saved: {base}_{name}.wav")
# Export MIDI
if cfg.get("export_midi") and melody_events:
mid = MidiFile()
track = MidiTrack()
mid.tracks.append(track)
ticks_per_beat = 480
tempo = 120
tick_offset = 0
for freq, dur, start in melody_events:
midi_note = self._freq_to_midi(freq)
duration_ticks = int(dur * ticks_per_beat * (tempo / 60))
start_ticks = int(start * ticks_per_beat * (tempo / 60))
track.append(Message('note_on', note=midi_note, velocity=64, time=start_ticks - tick_offset))
track.append(Message('note_off', note=midi_note, velocity=64, time=duration_ticks))
tick_offset = start_ticks + duration_ticks
mid_path = output_path.replace(".wav", ".mid")
mid.save(mid_path)
self._log(f"โ MIDI saved: {mid_path}")
def process(self, image_path: str, output_path: str):
self._log(f"\nโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ")
self._log(f"โ LYGO Resonance Engine v{__version__} โ")
self._log(f"โ Image โ Living Stereo Soundscape โ")
self._log(f"โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ\n")
self._log(f"Analyzing: {image_path}")
features = self.analyze_image(image_path)
self.synthesize(features, output_path)
def main():
parser = argparse.ArgumentParser(
description="LYGO Resonance Engine โ Turn any image into a rich stereo soundscape"
)
parser.add_argument("image", help="Input image path")
parser.add_argument("-o", "--output", default=None, help="Output .wav path")
parser.add_argument("--duration", type=float, default=15.0)
parser.add_argument("--style", choices=list(PRESETS.keys()), default="cinematic",
help="Artistic preset")
parser.add_argument("--seed", type=int, default=None, help="Random seed for reproducibility")
parser.add_argument("--noise-filter", type=float, default=None,
help="Lowpass cutoff Hz for noise layer (0 = off)")
parser.add_argument("--stems", action="store_true", help="Export individual stems (noise, drone, melody, glitch)")
parser.add_argument("--midi", action="store_true", help="Export MIDI file from melody events")
parser.add_argument("--batch", action="store_true", help="Process all images in a folder")
parser.add_argument("--quiet", action="store_true")
args = parser.parse_args()
config = {
"duration": args.duration,
"random_seed": args.seed,
"verbose": not args.quiet,
"export_stems": args.stems,
"export_midi": args.midi,
}
if args.noise_filter is not None:
config["noise_lowpass_hz"] = args.noise_filter
preset = PRESETS.get(args.style, {})
config.update(preset)
if args.batch:
folder = Path(args.image)
if not folder.is_dir():
print("Error: --batch requires a folder path")
return
images = sorted(folder.glob("*.jpg")) + sorted(folder.glob("*.png")) + sorted(folder.glob("*.jpeg"))
if not images:
print("No images found in folder")
return
for img in images:
print(f"\nProcessing: {img.name}")
out_path = f"resonance_{img.stem}.wav"
engine = ResonanceEngine(config)
engine.process(str(img), out_path)
return
out_path = args.output or f"resonance_{Path(args.image).stem}.wav"
engine = ResonanceEngine(config)
engine.process(args.image, out_path)
if __name__ == "__main__":
main()
#!/usr/bin/env python3
"""
LYGO Profile Generator v0.3
Image โ Musical DNA + Lyrical Framework
Extracts visual mathematics from an image and translates it into
structured creative direction for music production and AI-assisted lyric writing.
"""
import cv2
import numpy as np
import json
import math
import argparse
from pathlib import Path
from datetime import datetime
from typing import Dict, Any, Optional
__version__ = "0.3.0"
class LYGOProfileGenerator:
def __init__(self, verbose: bool = True):
self.verbose = verbose
def _log(self, msg: str):
if self.verbose:
print(msg)
def analyze_image(self, image_path: str) -> Dict[str, Any]:
"""Extract rich mathematical features from the image."""
img = cv2.imread(str(image_path))
if img is None:
raise FileNotFoundError(f"Image not found: {image_path}")
if len(img.shape) == 2:
img = cv2.cvtColor(img, cv2.COLOR_GRAY2BGR)
h, w, _ = img.shape
total_pixels = h * w
# === Color Analysis (HSV) ===
hsv = cv2.cvtColor(img, cv2.COLOR_BGR2HSV)
avg_hue = float(np.mean(hsv[:, :, 0]) * 2) # 0-360
avg_sat = float(np.mean(hsv[:, :, 1]) / 255.0)
avg_val = float(np.mean(hsv[:, :, 2]) / 255.0)
sat_std = float(np.std(hsv[:, :, 1]) / 255.0) # colorfulness
# === Luminance & Contrast ===
gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
brightness = float(np.mean(gray) / 255.0)
contrast = float(np.std(gray) / 255.0)
# === Structural Analysis ===
edges = cv2.Canny(gray, 50, 150)
edge_density = float(np.count_nonzero(edges) / total_pixels)
# Contours for structural complexity
contours, _ = cv2.findContours(edges, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)
structure_index = min(len(contours) / 80.0, 1.0)
# Micro-chaos (FAST corners)
fast = cv2.FastFeatureDetector_create(threshold=38)
keypoints = fast.detect(gray, None)
chaos_index = len(keypoints)
features = {
"source_image": str(Path(image_path).name),
"dimensions": {"width": w, "height": h},
"color": {
"average_hue": round(avg_hue, 2),
"average_saturation": round(avg_sat, 4),
"average_brightness": round(brightness, 4),
"colorfulness": round(sat_std, 4),
},
"structure": {
"edge_density": round(edge_density, 4),
"contrast": round(contrast, 4),
"structure_index": round(structure_index, 4),
"chaos_keypoints": chaos_index,
},
}
return features
def _get_musical_key(self, hue: float, brightness: float) -> str:
keys = ["C", "G", "D", "A", "E", "B", "F#", "Db", "Ab", "Eb", "Bb", "F"]
key_index = int(hue / 30) % 12
mode = "Minor" if brightness < 0.48 else "Major"
return f"{keys[key_index]} {mode}"
def _calculate_bpm(self, edge_density: float, chaos: int, brightness: float) -> int:
base = 82 + (edge_density * 920)
chaos_mod = min(chaos / 1800, 0.6)
brightness_mod = (brightness - 0.5) * 12
bpm = int(base + (chaos_mod * 25) + brightness_mod)
return max(78, min(178, bpm))
def _generate_genre_texture(self, features: Dict) -> Dict[str, str]:
e = features["structure"]["edge_density"]
c = features["structure"]["chaos_keypoints"]
b = features["color"]["average_brightness"]
contrast = features["structure"]["contrast"]
if c > 650 and b < 0.38:
genre = "Industrial Dubstep / Dark Phonk"
texture = "Heavy distortion, aggressive stutters, deep sub-bass, metallic textures"
energy = "High-aggression"
elif e > 0.065 and contrast > 0.18:
genre = "Emo Rap / Modern Trap"
texture = "Crisp hi-hats, melancholic melodies, heavy 808s, emotional vocal layers"
energy = "Mid-High emotional"
elif c > 420 and b > 0.55:
genre = "Experimental / Glitch Hop"
texture = "Glitchy percussion, chopped vocals, atmospheric synths, rhythmic complexity"
energy = "High chaotic"
elif e < 0.035 and b > 0.6:
genre = "West Coast G-Funk / Smooth Instrumental"
texture = "Laid-back grooves, warm analog bass, melodic leads, nostalgic atmosphere"
energy = "Mid relaxed"
else:
genre = "Dark Alternative / Cinematic Rap"
texture = "Atmospheric pads, punchy drums, moody synths, introspective energy"
energy = "Mid cinematic"
return {"genre": genre, "texture": texture, "energy": energy}
def translate_to_lygo(self, features: Dict[str, Any]) -> Dict[str, Any]:
"""Convert visual features into musical and lyrical creative direction."""
hue = features["color"]["average_hue"]
brightness = features["color"]["average_brightness"]
edge_density = features["structure"]["edge_density"]
chaos = features["structure"]["chaos_keypoints"]
contrast = features["structure"]["contrast"]
musical_key = self._get_musical_key(hue, brightness)
bpm = self._calculate_bpm(edge_density, chaos, brightness)
genre_data = self._generate_genre_texture(features)
# === Lyrical Theme Engine ===
if brightness < 0.42 and edge_density > 0.055:
core_theme = "Survival, betrayal, lone wolf resilience, moving in silence"
lyric_prompt = (
"Write raw, introspective lyrics about being the last one standing after betrayal. "
"Focus on trust issues, a very small circle of ride-or-die people, and the cold satisfaction of outlasting everyone who counted you out."
)
vocal_style = "Raspy melodic rap or gritty sung-rap hybrid"
elif chaos > 550:
core_theme = "Breaking chains, system resistance, unchained personal power"
lyric_prompt = (
"Write aggressive yet intelligent lyrics about breaking free from systems that tried to define you. "
"Emphasize resilience, moving in silence, and turning pain into unstoppable momentum."
)
vocal_style = "Assertive rap with melodic moments or distorted vocal processing"
else:
core_theme = "Observation, loyalty, navigating a cold modern world with quiet edge"
lyric_prompt = (
"Write clever, slightly dark observational lyrics with dry humor about modern life, loyalty, "
"and staying true to your own code while everything around you feels artificial."
)
vocal_style = "Deadpan to melodic rap delivery, slightly introspective"
# === Final Structured Output ===
lygo_profile = {
"LYGO_PROFILE": {
"version": __version__,
"generated_at": datetime.now().isoformat(),
"source": features["source_image"],
"mathematics": features,
"musical_dna": {
"root_key": musical_key,
"bpm": bpm,
"energy_level": genre_data["energy"],
"suggested_genre": genre_data["genre"],
"texture_description": genre_data["texture"],
"vocal_style": vocal_style,
},
"lyrical_framework": {
"core_theme": core_theme,
"ai_lyric_prompt": lyric_prompt,
},
"ai_music_prompt": (
f"Create a {genre_data['genre']} track at {bpm} BPM in the key of {musical_key}. "
f"The overall energy should feel {genre_data['energy'].lower()}. "
f"Sound design and texture: {genre_data['texture']}. "
f"Lyrical themes should center around {core_theme}."
),
"production_notes": (
f"High contrast and structural complexity suggest strong dynamic range. "
f"Consider heavy low-end support and atmospheric layers to match the visual weight."
),
}
}
return lygo_profile
def generate(self, image_path: str, output_json: str = "lygo_profile.json", create_brief: bool = False):
self._log(f"\nโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ")
self._log(f"โ LYGO Profile Generator v{__version__} โ")
self._log(f"โ Image โ Musical DNA + Lyrical Frameworkโ")
self._log(f"โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ\n")
features = self.analyze_image(image_path)
profile = self.translate_to_lygo(features)
# Save JSON
with open(output_json, "w") as f:
json.dump(profile, f, indent=2)
self._log(json.dumps(profile, indent=2))
self._log(f"\n[+] LYGO Profile saved โ {output_json}")
if create_brief:
brief_path = Path(output_json).with_suffix(".brief.txt")
self._create_creative_brief(profile, brief_path)
self._log(f"[+] Creative Brief saved โ {brief_path}")
def _create_creative_brief(self, profile: Dict, path: Path):
data = profile["LYGO_PROFILE"]
brief = f"""LYGO CREATIVE BRIEF
Generated: {data['generated_at']}
Source Image: {data['source']}
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
MUSICAL DNA
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
Key: {data['musical_dna']['root_key']}
BPM: {data['musical_dna']['bpm']}
Energy: {data['musical_dna']['energy_level']}
Genre Direction: {data['musical_dna']['suggested_genre']}
Texture & Vibe:
{data['musical_dna']['texture_description']}
Vocal Approach: {data['musical_dna']['vocal_style']}
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
LYRICAL DIRECTION
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
Core Theme: {data['lyrical_framework']['core_theme']}
AI Prompt:
{data['lyrical_framework']['ai_lyric_prompt']}
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
FULL AI MUSIC PROMPT (Copy-Paste Ready)
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
{data['ai_music_prompt']}
Production Notes:
{data['production_notes']}
"""
path.write_text(brief, encoding='utf-8')
def main():
parser = argparse.ArgumentParser(
description="LYGO Profile Generator โ Turn any image into structured musical + lyrical creative direction"
)
parser.add_argument("image", help="Path to input image")
parser.add_argument("-o", "--output", default="lygo_profile.json", help="Output JSON file")
parser.add_argument("--brief", action="store_true", help="Also generate a human-readable .brief.txt file")
parser.add_argument("--batch", action="store_true", help="Process all images in a folder")
parser.add_argument("--quiet", action="store_true", help="Suppress console output")
args = parser.parse_args()
generator = LYGOProfileGenerator(verbose=not args.quiet)
if args.batch:
folder = Path(args.image)
if not folder.is_dir():
print("Error: --batch requires a folder path")
return
images = sorted(folder.glob("*.jpg")) + sorted(folder.glob("*.png")) + sorted(folder.glob("*.jpeg"))
if not images:
print("No images found in folder")
return
for img in images:
print(f"\nProcessing: {img.name}")
out_json = f"lygo_profile_{img.stem}.json"
generator.generate(str(img), out_json, create_brief=args.brief)
return
generator.generate(args.image, args.output, create_brief=args.brief)
if __name__ == "__main__":
main()
#!/usr/bin/env python3
"""
LYGO RESONANCE - Gradio GUI
Web interface for the Resonance Engine and Profile Generator.
"""
import gradio as gr
import os
import json
from pathlib import Path
from resonance_engine import ResonanceEngine, PRESETS
from lygo_profile import LYGOProfileGenerator
def process_image(image_path, engine_type, style, seed, duration, noise_filter,
export_stems, export_midi, export_brief, use_batch, batch_folder):
# 1. Validation guardrails
if not image_path and not use_batch:
return "โ ๏ธ Error: Please upload an image or enable batch processing mode.", None, None
# 2. Setup output collections for file download components
downloadable_files = []
playback_audio = None
try:
# --- BATCH PROCESSING MODE ---
if use_batch and batch_folder:
folder = Path(batch_folder)
if not folder.is_dir():
return f"โ Error: Batch folder path '{batch_folder}' does not exist or is invalid.", None, None
images = sorted(folder.glob("*.jpg")) + sorted(folder.glob("*.png")) + sorted(folder.glob("*.jpeg"))
if not images:
return f"โน๏ธ Notice: No compatible images (.jpg, .jpeg, .png) found in '{batch_folder}'.", None, None
results = []
for img in images:
try:
if engine_type == "Resonance Engine (Audio)":
out_path = f"resonance_{img.stem}.wav"
config = {
"duration": duration,
"random_seed": int(seed) if seed != 0 else None,
"verbose": False,
"export_stems": export_stems,
"export_midi": export_midi,
}
if noise_filter > 0:
config["noise_lowpass_hz"] = noise_filter
preset = PRESETS.get(style, {})
config.update(preset)
engine = ResonanceEngine(config)
engine.process(str(img), out_path)
results.append(f"โ {img.name} โ {out_path}")
downloadable_files.append(out_path)
else:
out_json = f"lygo_profile_{img.stem}.json"
generator = LYGOProfileGenerator(verbose=False)
generator.generate(str(img), out_json, create_brief=export_brief)
results.append(f"โ {img.name} โ {out_json}")
downloadable_files.append(out_json)
if export_brief:
downloadable_files.append(out_json.replace(".json", ".brief.txt"))
except Exception as batch_err:
results.append(f"โ {img.name} โ Error: {str(batch_err)}")
return "๐ฆ Batch Processing Logs:\n" + "\n".join(results), None, downloadable_files
# --- SINGLE IMAGE MODE ---
img_p = Path(image_path)
if engine_type == "Resonance Engine (Audio)":
out_path = f"resonance_{img_p.stem}.wav"
config = {
"duration": duration,
"random_seed": int(seed) if seed != 0 else None,
"verbose": False,
"export_stems": export_stems,
"export_midi": export_midi,
}
if noise_filter > 0:
config["noise_lowpass_hz"] = noise_filter
preset = PRESETS.get(style, {})
config.update(preset)
engine = ResonanceEngine(config)
engine.process(image_path, out_path)
downloadable_files.append(out_path)
playback_audio = out_path # Feed directly to audio player
# Catch accompanying files if checked
if export_midi:
mid_file = out_path.replace(".wav", ".mid")
if os.path.exists(mid_file):
downloadable_files.append(mid_file)
if export_stems:
for stem in ["noise", "drone", "melody", "glitch"]:
stem_file = out_path.replace(".wav", f"_{stem}.wav")
if os.path.exists(stem_file):
downloadable_files.append(stem_file)
log_msg = f"โ
Resonance Engine Matrix Complete.\nGenerated Stereo Mixdown: {out_path}"
return log_msg, playback_audio, downloadable_files
else:
# LYGO Profile Mode
out_json = f"lygo_profile_{img_p.stem}.json"
generator = LYGOProfileGenerator(verbose=False)
generator.generate(image_path, out_json, create_brief=export_brief)
downloadable_files.append(out_json)
# Read profile payload back to show the user the prompt data directly
with open(out_json, "r", encoding="utf-8") as f:
payload = json.load(f)
ai_prompt = payload.get("LYGO_PROFILE", {}).get("ai_music_prompt", "Profile created.")
log_msg = f"โ
LYGO DNA Profile Compiled Successfully!\nSaved Destination: {out_json}\n\n๐ AI Music Prompt Copy-Ready:\n\"{ai_prompt}\""
if export_brief:
brief_file = out_json.replace(".json", ".brief.txt")
if os.path.exists(brief_file):
downloadable_files.append(brief_file)
return log_msg, None, downloadable_files
except Exception as global_err:
return f"โ System Error executing core logic: {str(global_err)}", None, None
# --- DESIGN & LAYOUT THE INTERFACE ---
with gr.Blocks(theme=gr.themes.Box()) as demo:
gr.Markdown("# ๐ LYGO RESONANCE")
gr.Markdown("### Core SDK Deployment โ Visual-to-Audio Translation & Structural DNA Engine")
with gr.Row():
with gr.Column(scale=1):
# Input block
img_input = gr.Image(type="filepath", label="๐ธ Upload Source Image (Single File)")
engine_choice = gr.Radio(
["Resonance Engine (Audio)", "LYGO Profile Generator"],
value="Resonance Engine (Audio)",
label="โ๏ธ Active Core Engine"
)
with gr.Accordion("๐จ Audio Synth Parameters (Resonance Engine)", open=True):
preset_style = gr.Dropdown(
["cinematic", "ambient", "glitch", "ethereal", "raw"],
value="cinematic",
label="Artistic Preset Blueprint"
)
duration_slider = gr.Slider(5, 60, value=15, step=1, label="Track Duration Length (Seconds)")
seed_num = gr.Number(value=0, label="Mathematical Seed Lock (0 = Generative Continuous)")
filter_hz = gr.Number(value=0, label="Noise Layer Lowpass Filter (Hz, 0 = Off)")
stem_check = gr.Checkbox(label="Export Separated Audio Stems (.wav split)")
midi_check = gr.Checkbox(label="Export Extracted Melodic MIDI Sequence")
with gr.Accordion("๐ Analytical Parameters (Profile Engine)", open=False):
brief_check = gr.Checkbox(value=True, label="Generate Human-Readable Brief (.brief.txt)")
with gr.Accordion("๐ Automated Batch Processing Cluster", open=False):
batch_check = gr.Checkbox(label="Activate Mass Batch Folder Mode")
batch_dir = gr.Textbox(
label="Local Server Input Folder Directory",
placeholder="e.g., ./input_folder"
)
submit_btn = gr.Button("๐ฎ Execute Spectral Scan", variant="primary")
with gr.Column(scale=1):
# Output block
text_output = gr.Textbox(label="๐ฅ๏ธ Core Diagnostics Log & Text Prompts", lines=10, interactive=False)
audio_player = gr.Audio(label="๐ง Real-Time Stereo Mix Down Preview", interactive=False)
file_download = gr.Files(label="๐ฆ Download Output Manifest (WAV, JSON, MID, TXT)", interactive=False)
# Attach event processing hook
submit_btn.click(
fn=process_image,
inputs=[
img_input, engine_choice, preset_style, seed_num, duration_slider, filter_hz,
stem_check, midi_check, brief_check, batch_check, batch_dir
],
outputs=[text_output, audio_player, file_download]
)
if __name__ == "__main__":
demo.launch()
#!/usr/bin/env python3
"""
LYGO Video Resonance Engine
Extracts audio from video by analyzing motion and frame geometry over time.
"""
import cv2
import numpy as np
import soundfile as sf
import math
import argparse
from pathlib import Path
from resonance_engine import ResonanceEngine, PRESETS
class VideoResonanceEngine:
def __init__(self, config=None):
self.engine = ResonanceEngine(config)
def process_video(self, video_path, output_path="video_soundscape.wav",
fps=10, style="cinematic", duration_factor=1.0):
cap = cv2.VideoCapture(video_path)
if not cap.isOpened():
raise ValueError(f"Cannot open video: {video_path}")
frames = []
while True:
ret, frame = cap.read()
if not ret:
break
frames.append(frame)
cap.release()
if not frames:
raise ValueError("No frames extracted from video.")
src_fps = cap.get(cv2.CAP_PROP_FPS)
if src_fps <= 0:
src_fps = 30
step = max(1, int(src_fps / fps))
frames = frames[::step]
actual_fps = src_fps / step
print(f"Extracted {len(frames)} frames at ~{actual_fps:.1f} FPS")
# Use last frame for feature extraction
temp_img_path = "_temp_video_frame.jpg"
cv2.imwrite(temp_img_path, frames[-1])
# Get baseline config from preset
config = dict(PRESETS.get(style, {}))
config["duration"] = len(frames) / actual_fps * duration_factor
config["verbose"] = True
engine = ResonanceEngine(config)
features = engine.analyze_image(temp_img_path)
# Generate audio segment for each frame with interpolation
sr = engine.config["sr"]
duration = config["duration"]
audio = np.zeros((int(sr * duration), 2), dtype=np.float32)
# Motion detection (optical flow between frames)
prev_gray = cv2.cvtColor(frames[0], cv2.COLOR_BGR2GRAY)
motion_scores = []
for i, frame in enumerate(frames):
gray = cv2.cvtColor(frame, cv2.COLOR_BGR2GRAY)
flow = cv2.calcOpticalFlowFarneback(prev_gray, gray, None, 0.5, 3, 15, 3, 5, 1.2, 0)
mag, _ = cv2.cartToPolar(flow[..., 0], flow[..., 1])
motion_score = np.mean(mag)
motion_scores.append(motion_score)
prev_gray = gray
# Normalize motion scores
max_motion = max(motion_scores) if motion_scores else 1
motion_scores = [m / max_motion for m in motion_scores]
# Generate audio
for i, frame in enumerate(frames):
cv2.imwrite(temp_img_path, frame)
seg_features = engine.analyze_image(temp_img_path)
# Adjust parameters based on motion
motion = motion_scores[i] if i < len(motion_scores) else 0
config["glitch_vol"] = 0.032 + (motion * 0.08)
config["noise_vol"] = 0.095 + (motion * 0.06)
config["drone_vol"] = 0.075 - (motion * 0.03)
engine.config.update(config)
# Generate short segment
seg_duration = 1.0 / actual_fps
engine.config["duration"] = seg_duration
seg_audio = np.zeros((int(sr * seg_duration), 2), dtype=np.float32)
# Simplified segment synthesis (reuse analyze/synthesize)
# For efficiency, we use the full synthesis but only for a short duration
engine.synthesize(seg_features, "_temp_seg.wav")
seg, _ = sf.read("_temp_seg.wav")
# Place in main audio
start_idx = int(i * sr / actual_fps)
end_idx = min(start_idx + len(seg), len(audio))
seg_len = end_idx - start_idx
audio[start_idx:end_idx] += seg[:seg_len]
# Cleanup
if Path("_temp_video_frame.jpg").exists():
Path("_temp_video_frame.jpg").unlink()
if Path("_temp_seg.wav").exists():
Path("_temp_seg.wav").unlink()
# Normalize and save
peak = np.max(np.abs(audio))
if peak > 0:
audio = audio / peak * 0.97
sf.write(output_path, audio, sr)
print(f"โ Video soundscape saved: {output_path}")
return output_path
def main():
parser = argparse.ArgumentParser(
description="LYGO Video Resonance โ Turn a video into a motion-driven soundscape"
)
parser.add_argument("video", help="Input video path")
parser.add_argument("-o", "--output", default="video_soundscape.wav", help="Output .wav path")
parser.add_argument("--fps", type=float, default=10, help="Frames per second to extract")
parser.add_argument("--style", choices=list(PRESETS.keys()), default="cinematic",
help="Artistic preset")
parser.add_argument("--duration-factor", type=float, default=1.0,
help="Multiply final duration (e.g., 0.5 for half speed, 2.0 for double)")
args = parser.parse_args()
engine = VideoResonanceEngine()
engine.process_video(
args.video,
output_path=args.output,
fps=args.fps,
style=args.style,
duration_factor=args.duration_factor
)
if __name__ == "__main__":
main()
#!/usr/bin/env python3
"""
LYGO LLM Integration
Expands LYGO creative briefs into full song lyrics using local LLMs (Ollama, llama.cpp, etc.)
"""
import json
import requests
from pathlib import Path
from typing import Optional, Dict, Any
class LYGOLLMExpander:
def __init__(self, llm_url: str = "http://localhost:11434/api/generate",
model: str = "llama3.2", verbose: bool = True):
self.llm_url = llm_url
self.model = model
self.verbose = verbose
def _log(self, msg: str):
if self.verbose:
print(msg)
def expand_brief_to_lyrics(self, brief_path: Path, output_path: Optional[Path] = None) -> str:
"""Read a LYGO creative brief and generate full song lyrics using a local LLM."""
with open(brief_path, 'r', encoding='utf-8') as f:
brief = f.read()
self._log(f"Reading brief: {brief_path}")
prompt = f"""You are a professional songwriter. Based on the following creative brief, write a complete song with a title, verses, a chorus, a bridge, and an outro. Use vivid imagery and emotional depth.
Creative Brief:
{brief}
Now write the song. Include a title at the top."""
self._log(f"Contacting LLM at {self.llm_url} with model {self.model}...")
try:
response = requests.post(
self.llm_url,
json={
"model": self.model,
"prompt": prompt,
"stream": False,
"temperature": 0.8,
},
timeout=60
)
response.raise_for_status()
result = response.json()
lyrics = result.get("response", "No response from LLM.")
if output_path is None:
output_path = brief_path.with_suffix(".lyrics.txt")
with open(output_path, 'w', encoding='utf-8') as f:
f.write(lyrics)
self._log(f"โ Lyrics saved: {output_path}")
return lyrics
except requests.exceptions.ConnectionError:
error_msg = "Error: Could not connect to LLM. Make sure Ollama or llama.cpp is running."
self._log(error_msg)
return error_msg
except Exception as e:
self._log(f"Error: {str(e)}")
return str(e)
def batch_process_folder(self, folder_path: Path, output_folder: Optional[Path] = None):
"""Process all .brief.txt files in a folder."""
briefs = list(folder_path.glob("*.brief.txt"))
if not briefs:
self._log("No .brief.txt files found in folder.")
return
if output_folder is None:
output_folder = folder_path
output_folder.mkdir(parents=True, exist_ok=True)
for brief in briefs:
out_path = output_folder / brief.with_suffix(".lyrics.txt").name
self._log(f"Processing: {brief.name}")
self.expand_brief_to_lyrics(brief, out_path)
def main():
import argparse
parser = argparse.ArgumentParser(
description="LYGO LLM Expander โ Turn creative briefs into full lyrics using local LLMs"
)
parser.add_argument("input", help="Path to .brief.txt file or folder (with --batch)")
parser.add_argument("-o", "--output", default=None, help="Output path or folder")
parser.add_argument("--llm-url", default="http://localhost:11434/api/generate",
help="Ollama/llama.cpp API URL")
parser.add_argument("--model", default="llama3.2", help="LLM model name")
parser.add_argument("--batch", action="store_true", help="Process all .brief.txt files in folder")
args = parser.parse_args()
expander = LYGOLLMExpander(llm_url=args.llm_url, model=args.model)
if args.batch:
folder = Path(args.input)
if not folder.is_dir():
print("Error: --batch requires a folder path")
return
expander.batch_process_folder(folder, Path(args.output) if args.output else None)
else:
brief_path = Path(args.input)
if not brief_path.exists():
print("Error: File not found")
return
output_path = Path(args.output) if args.output else None
expander.expand_brief_to_lyrics(brief_path, output_path)
if __name__ == "__main__":
main()
Listen to LYGO RESONANCE running live โ a continuous evolving audio environment.
Help spread the LYGO Resonance!