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.
Use the hosted Gradio space for one-click unique DNA profiles + working LYGO Protocol audio (LDQ crash fixed). Every photo now gets a unique fingerprint, genre blend, BPM, key, and Suno style string.
Open LYGO Resonance Engine on Hugging Face → Lattice gateway
Profile Generator v0.4.1 · Resonance Engine v0.5.3 · Unique image seeds · LDQ scipy EQ fix
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 (v0.4.1 fingerprint DNA)
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.4.1
Image → Musical DNA + Lyrical Framework
Every image produces a unique fingerprint-driven style (genre blend, BPM, key,
Suno/Udio tags, production notes). No coarse 5-bucket collapse.
"""
from __future__ import annotations
import argparse
import hashlib
import json
import math
from datetime import datetime, timezone
from pathlib import Path
from typing import Any, Dict, List, Optional, Tuple
import cv2
import numpy as np
__version__ = "0.4.1"
# Large genre palette — blended by continuous visual scores (not hard if/else only)
GENRE_AXES: List[Tuple[str, str, str]] = [
("Industrial Dubstep", "distorted reese bass, metallic hits, half-time drops", "high-aggression"),
("Dark Phonk", "cowbells, deep 808s, Memphis grit, drift cars atmosphere", "mid-high dark"),
("Emo Trap", "crisp hi-hats, melancholic melodies, heavy 808s, emotional space", "mid-high emotional"),
("Glitch Hop", "chopped vocals, stutter edits, broken rhythms, vinyl dirt", "high chaotic"),
("G-Funk Instrumental", "warm analog bass, melodic leads, laid-back swing", "mid relaxed"),
("Cinematic Ambient", "wide pads, slow risers, sparse percussion, film score air", "low-mid cinematic"),
("Synthwave", "neon arps, gated reverb drums, retro analog leads", "mid nostalgic"),
("Drill / UK Drill", "sliding 808s, sparse hats, cold melodic loops", "mid tense"),
("Lo-fi Hip Hop", "dusty drums, soft keys, tape hiss, chill pocket", "low-mid chill"),
("Techno / Warehouse", "four-on-floor, hypnotic bass, industrial room tone", "high driving"),
("Orchestral Hybrid", "strings + brass hits, hybrid trailer drums, epic risers", "high cinematic"),
("R&B Alt", "silky chords, half-time groove, intimate vocal pocket", "mid sensual"),
("Breakbeat / Jungle", "amen cuts, rolling bass, rapid edits", "high kinetic"),
("Shoegaze Electronica", "washed guitars, reverb walls, dreamy tempo", "mid dreamy"),
("Afro-Electronic", "polyrhythms, warm percussion, bright melodic motifs", "mid-high vibrant"),
("Doom Electronica", "sub pressure, slow drones, crushed mids, ritual pace", "low heavy"),
("Hyperpop", "glassy synths, pitch-shifted vox textures, maximal color", "high bright"),
("Jazz-Electronica", "complex chords, brushed drums, nocturnal harmony", "mid sophisticated"),
("Folktronica", "acoustic plucks, soft pads, organic grain", "mid earthy"),
("Cyberpunk IDM", "angular synths, irregular meters, neon noir", "high cerebral"),
]
class LYGOProfileGenerator:
def __init__(self, verbose: bool = True):
self.verbose = verbose
def _log(self, msg: str) -> None:
if self.verbose:
print(msg)
@staticmethod
def _stable_u32(*parts: Any) -> int:
h = hashlib.sha256()
for p in parts:
h.update(str(p).encode("utf-8", errors="replace"))
h.update(b"|")
return int.from_bytes(h.digest()[:4], "big")
def analyze_image(self, image_path: str) -> Dict[str, Any]:
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 = max(1, h * w)
# Downsample for stable histograms / speed
small = cv2.resize(img, (min(320, w), min(240, h)), interpolation=cv2.INTER_AREA)
hsv = cv2.cvtColor(small, cv2.COLOR_BGR2HSV)
gray = cv2.cvtColor(small, cv2.COLOR_BGR2GRAY)
sh, sw = gray.shape
stotal = max(1, sh * sw)
avg_hue = float(np.mean(hsv[:, :, 0]) * 2.0) # 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)
hue_std = float(np.std(hsv[:, :, 0].astype(np.float32)) * 2.0)
brightness = float(np.mean(gray) / 255.0)
contrast = float(np.std(gray) / 255.0)
# Regional brightness (3x3 grid) — unique spatial signature
gy, gx = sh // 3, sw // 3
regions = []
for ri in range(3):
for ci in range(3):
tile = gray[ri * gy : (ri + 1) * gy or sh, ci * gx : (ci + 1) * gx or sw]
regions.append(round(float(np.mean(tile) / 255.0), 4) if tile.size else 0.0)
edges = cv2.Canny(gray, 50, 150)
edge_density = float(np.count_nonzero(edges) / stotal)
# Directionality via Sobel
sx = cv2.Sobel(gray, cv2.CV_32F, 1, 0, ksize=3)
sy = cv2.Sobel(gray, cv2.CV_32F, 0, 1, ksize=3)
mag = np.sqrt(sx * sx + sy * sy) + 1e-6
# Dominant angle 0-180
ang = (np.arctan2(sy, sx) * 180.0 / math.pi) % 180.0
hist_ang, _ = np.histogram(ang, bins=8, range=(0, 180), weights=mag)
hist_ang = hist_ang / (hist_ang.sum() + 1e-9)
directionality = float(hist_ang.max()) # high = strong lines
contours, _ = cv2.findContours(edges, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)
structure_index = min(len(contours) / 80.0, 1.0)
fast = cv2.FastFeatureDetector_create(threshold=38)
keypoints = fast.detect(gray, None)
chaos_index = int(len(keypoints))
# Color histogram fingerprint (8 bins per channel)
hist_bits = []
for ch in range(3):
hist = cv2.calcHist([small], [ch], None, [8], [0, 256]).flatten()
hist = hist / (hist.sum() + 1e-9)
hist_bits.extend([round(float(x), 5) for x in hist])
# Content hash of resized pixels (unique per photo content)
pixel_hash = hashlib.sha256(small.tobytes()).hexdigest()[:16]
# Warmth: red vs blue mean
b_m, g_m, r_m = [float(np.mean(small[:, :, i])) for i in range(3)]
warmth = (r_m - b_m) / 255.0 # -1 cold → +1 warm
features = {
"source_image": str(Path(image_path).name),
"dimensions": {"width": w, "height": h},
"aspect_ratio": round(w / max(1, h), 4),
"fingerprint": {
"pixel_hash": pixel_hash,
"region_brightness": regions,
"bgr_hist": hist_bits,
"angle_hist": [round(float(x), 4) for x in hist_ang.tolist()],
},
"color": {
"average_hue": round(avg_hue, 2),
"hue_variance": round(hue_std, 2),
"average_saturation": round(avg_sat, 4),
"average_value": round(avg_val, 4),
"average_brightness": round(brightness, 4),
"colorfulness": round(sat_std, 4),
"warmth": round(warmth, 4),
},
"structure": {
"edge_density": round(edge_density, 4),
"contrast": round(contrast, 4),
"structure_index": round(structure_index, 4),
"chaos_keypoints": chaos_index,
"directionality": round(directionality, 4),
},
}
return features
def _get_musical_key(self, hue: float, brightness: float, sat: float, seed: int) -> str:
keys = ["C", "C#", "D", "Eb", "E", "F", "F#", "G", "Ab", "A", "Bb", "B"]
# Fine hue mapping + seed nudge so near-hues still diverge across photos
key_index = (int(hue / 15.0) + (seed % 3)) % 12
# Mode from brightness + saturation
mode_score = brightness * 0.7 + sat * 0.3
if mode_score < 0.38:
mode = "Phrygian" if sat > 0.45 else "Minor"
elif mode_score < 0.55:
mode = "Dorian" if (seed % 2) else "Minor"
elif mode_score < 0.72:
mode = "Mixolydian" if sat > 0.4 else "Major"
else:
mode = "Lydian" if (seed % 3 == 0) else "Major"
return f"{keys[key_index]} {mode}"
def _calculate_bpm(self, edge: float, chaos: int, brightness: float, contrast: float, seed: int) -> int:
base = 70 + edge * 1100 + contrast * 40 + min(chaos, 2000) / 40.0
base += (brightness - 0.5) * 18
base += ((seed % 17) - 8) * 1.4 # ±~11 BPM unique nudge
bpm = int(round(base))
return max(60, min(190, bpm))
def _genre_scores(self, f: Dict[str, Any]) -> List[Tuple[float, int]]:
"""Score each genre axis from continuous features."""
c = f["color"]
s = f["structure"]
e, ch, b = s["edge_density"], s["chaos_keypoints"], c["average_brightness"]
contrast, sat = s["contrast"], c["average_saturation"]
warmth, direct = c["warmth"], s["directionality"]
hue = c["average_hue"]
chaos_n = min(ch / 1200.0, 1.5)
# Feature vector for scoring
scores = []
for i, (name, _tex, _en) in enumerate(GENRE_AXES):
sc = 0.0
# Industrial Dubstep
if i == 0:
sc = chaos_n * 1.2 + (1 - b) * 0.9 + e * 8
elif i == 1: # Dark Phonk
sc = (1 - b) * 1.1 + contrast * 1.4 + (1 if 200 < hue < 300 else 0.2)
elif i == 2: # Emo Trap
sc = e * 10 + contrast * 1.2 + (1 - b) * 0.4
elif i == 3: # Glitch Hop
sc = chaos_n * 1.5 + e * 6 + contrast * 0.8
elif i == 4: # G-Funk
sc = b * 0.9 + (1 - e * 5) + max(0, warmth) * 0.8
elif i == 5: # Cinematic Ambient
sc = (1 - e * 8) + b * 0.5 + (1 - chaos_n) * 0.7
elif i == 6: # Synthwave
sc = sat * 1.3 + b * 0.6 + (1 if 280 < hue or hue < 40 else 0.3)
elif i == 7: # Drill
sc = (1 - b) * 0.8 + e * 7 + contrast * 0.9
elif i == 8: # Lo-fi
sc = (1 - contrast) * 1.2 + (1 - e * 6) + (1 - sat) * 0.5
elif i == 9: # Techno
sc = direct * 1.5 + e * 5 + chaos_n * 0.3
elif i == 10: # Orchestral Hybrid
sc = contrast * 1.3 + b * 0.4 + s["structure_index"] * 0.8
elif i == 11: # R&B
sc = sat * 0.9 + b * 0.7 + (1 - chaos_n) * 0.5
elif i == 12: # Breakbeat
sc = chaos_n * 1.1 + e * 9 + contrast * 0.6
elif i == 13: # Shoegaze
sc = sat * 0.7 + (1 - contrast * 0.5) + b * 0.5
elif i == 14: # Afro-Electronic
sc = b * 0.8 + sat * 1.0 + max(0, warmth) * 0.6 + e * 3
elif i == 15: # Doom
sc = (1 - b) * 1.4 + (1 - sat) * 0.5 + contrast * 0.4
elif i == 16: # Hyperpop
sc = sat * 1.5 + b * 0.9 + chaos_n * 0.4
elif i == 17: # Jazz-Electronica
sc = s["structure_index"] * 1.2 + (1 - e * 4) + sat * 0.4
elif i == 18: # Folktronica
sc = max(0, warmth) * 1.1 + (1 - chaos_n) * 0.6 + b * 0.5
else: # Cyberpunk IDM
sc = chaos_n * 0.9 + direct * 0.8 + (1 if 160 < hue < 240 else 0.2) + e * 4
# Regional uniqueness
regs = f["fingerprint"]["region_brightness"]
sc += abs(regs[i % 9] - b) * 0.35
scores.append((sc, i))
scores.sort(reverse=True)
return scores
def _generate_genre_texture(self, features: Dict[str, Any], seed: int) -> Dict[str, Any]:
ranked = self._genre_scores(features)
top = ranked[0][1]
second = ranked[1][1]
# Seed can flip primary/secondary when scores close
if ranked[0][0] - ranked[1][0] < 0.25 and (seed % 2):
top, second = second, top
g1, t1, e1 = GENRE_AXES[top]
g2, t2, e2 = GENRE_AXES[second]
blend = 0.55 + (seed % 20) / 100.0
genre = f"{g1} × {g2}" if top != second else g1
texture = f"{t1}; secondary: {t2}"
energy = e1 if blend >= 0.55 else e2
# Unique style tags from feature thresholds
tags: List[str] = []
c, s = features["color"], features["structure"]
if c["average_brightness"] < 0.35:
tags.append("nocturnal")
elif c["average_brightness"] > 0.7:
tags.append("daylight-bright")
if c["colorfulness"] > 0.25:
tags.append("high-chroma")
if c["warmth"] > 0.15:
tags.append("warm-palette")
elif c["warmth"] < -0.15:
tags.append("cold-palette")
if s["edge_density"] > 0.08:
tags.append("dense-detail")
if s["edge_density"] < 0.02:
tags.append("soft-focus")
if s["chaos_keypoints"] > 800:
tags.append("micro-chaos")
if s["directionality"] > 0.28:
tags.append("linear-geometry")
if s["contrast"] > 0.22:
tags.append("high-contrast")
tags.append(f"hue-{int(c['average_hue']) // 30 * 30}")
tags.append(f"fp-{features['fingerprint']['pixel_hash'][:6]}")
return {
"genre": genre,
"primary_genre": g1,
"secondary_genre": g2,
"texture": texture,
"energy": energy,
"style_tags": tags,
"genre_scores_top3": [
{"genre": GENRE_AXES[i][0], "score": round(sc, 3)} for sc, i in ranked[:3]
],
}
def _lyric_framework(self, features: Dict, genre_data: Dict, seed: int) -> Dict[str, str]:
b = features["color"]["average_brightness"]
e = features["structure"]["edge_density"]
ch = features["structure"]["chaos_keypoints"]
warmth = features["color"]["warmth"]
hue = features["color"]["average_hue"]
themes = [
("Survival after silence", "Write about enduring when everyone left — quiet power, small circle, long game."),
("Breaking engineered cages", "Write about systems that labeled you wrong and the moment you stopped complying."),
("Neon loyalty", "Write about keeping a code of loyalty in a synthetic, online, cold city."),
("Memory as weapon", "Write about archived memories that refuse deletion and become fuel."),
("Gravity of truth", "Write about refusing gaslight — measuring what is real under pressure."),
("Threshold walker", "Write about standing at a door between old self and becoming."),
("Warm ruin", "Write about beauty found inside collapse — soft light on hard edges."),
("Cold machinery heart", "Write about human feeling inside industrial noise and algorithm speed."),
("Spiral return", "Write about cycles that look like loops until they become ascent."),
("Quiet fire", "Write about controlled intensity — no spectacle, just irreversible intent."),
]
# Index from multi-feature hash so different photos diverge
idx = (
int(b * 40)
+ int(e * 200)
+ int(ch / 80)
+ int((warmth + 1) * 10)
+ int(hue / 36)
+ (seed % 97)
) % len(themes)
core, prompt_base = themes[idx]
vocal_pool = [
"raspy melodic rap",
"deadpan spoken-sung hybrid",
"assertive trap vocal with airy doubles",
"breathy alt-pop lead",
"distorted chant + clean hook",
"choir-stacked ambient voice",
"grit-to-silk dynamic rap",
"half-whisper intimate R&B",
]
vocal = vocal_pool[(seed + idx) % len(vocal_pool)]
return {
"core_theme": f"{core} · {genre_data['primary_genre']}",
"ai_lyric_prompt": (
f"{prompt_base} Frame it through {genre_data['primary_genre']} energy "
f"({genre_data['energy']}). Keep imagery tied to the visual: "
f"hue≈{features['color']['average_hue']:.0f}°, "
f"brightness={features['color']['average_brightness']:.2f}, "
f"edge density={features['structure']['edge_density']:.3f}."
),
"vocal_style": vocal,
}
def translate_to_lygo(self, features: Dict[str, Any]) -> Dict[str, Any]:
fp = features["fingerprint"]["pixel_hash"]
seed = self._stable_u32(fp, features["source_image"], features["color"]["average_hue"])
hue = features["color"]["average_hue"]
brightness = features["color"]["average_brightness"]
sat = features["color"]["average_saturation"]
edge = features["structure"]["edge_density"]
chaos = features["structure"]["chaos_keypoints"]
contrast = features["structure"]["contrast"]
musical_key = self._get_musical_key(hue, brightness, sat, seed)
bpm = self._calculate_bpm(edge, chaos, brightness, contrast, seed)
genre_data = self._generate_genre_texture(features, seed)
lyrics = self._lyric_framework(features, genre_data, seed)
# Time signature / scale extras from structure
if features["structure"]["directionality"] > 0.3 and chaos > 600:
meter = "7/8"
elif edge > 0.07:
meter = "4/4 double-time feel"
else:
meter = "4/4"
# Unique Suno/Udio style line
style_line = (
f"{genre_data['genre']}, {bpm} BPM, {musical_key}, {genre_data['energy']}, "
+ ", ".join(genre_data["style_tags"][:6])
+ f", {genre_data['texture']}"
)
suno_style = style_line[:200] # keep paste-ready length
udio_prompt = (
f"{genre_data['primary_genre']} with {genre_data['secondary_genre']} influence, "
f"{bpm}bpm {musical_key}, {lyrics['vocal_style']}, themes of {lyrics['core_theme']}"
)
ai_music_prompt = (
f"Create a {genre_data['genre']} track at {bpm} BPM in {musical_key} ({meter}). "
f"Energy: {genre_data['energy'].lower()}. Texture: {genre_data['texture']}. "
f"Vocal approach: {lyrics['vocal_style']}. "
f"Lyrical center: {lyrics['core_theme']}. "
f"Visual DNA tags: {', '.join(genre_data['style_tags'])}. "
f"Image fingerprint: {fp}."
)
production_notes = (
f"Brightness {brightness:.2f} → {'bright top end / open air' if brightness > 0.55 else 'darker low-mid focus'}. "
f"Edge density {edge:.3f} → {'dense rhythmic detail, tighter edits' if edge > 0.05 else 'space and longer tails'}. "
f"Contrast {contrast:.2f}; chaos keypoints {chaos}; warmth {features['color']['warmth']:+.2f}. "
f"Directionality {features['structure']['directionality']:.2f} → "
f"{'strong linear motifs / ostinato' if features['structure']['directionality'] > 0.25 else ' freer organic phrasing'}. "
f"Unique seed {seed & 0xFFFF:04X}."
)
return {
"LYGO_PROFILE": {
"version": __version__,
"generated_at": datetime.now(timezone.utc).isoformat(),
"source": features["source_image"],
"unique_id": f"LYGO-{fp}-{seed & 0xFFFFFF:06X}",
"image_seed": seed,
"mathematics": features,
"musical_dna": {
"root_key": musical_key,
"bpm": bpm,
"meter": meter,
"energy_level": genre_data["energy"],
"suggested_genre": genre_data["genre"],
"primary_genre": genre_data["primary_genre"],
"secondary_genre": genre_data["secondary_genre"],
"texture_description": genre_data["texture"],
"vocal_style": lyrics["vocal_style"],
"style_tags": genre_data["style_tags"],
"genre_scores_top3": genre_data["genre_scores_top3"],
},
"lyrical_framework": {
"core_theme": lyrics["core_theme"],
"ai_lyric_prompt": lyrics["ai_lyric_prompt"],
},
"ai_music_prompt": ai_music_prompt,
"suno_style": suno_style,
"udio_prompt": udio_prompt,
"production_notes": production_notes,
}
}
def generate(
self,
image_path: str,
output_json: str = "lygo_profile.json",
create_brief: bool = False,
) -> Dict[str, Any]:
self._log(f"\n╔════════════════════════════════════════════╗")
self._log(f"║ LYGO Profile Generator v{__version__} ║")
self._log(f"║ Image → Unique Musical DNA + Lyrics ║")
self._log(f"╚════════════════════════════════════════════╝\n")
features = self.analyze_image(image_path)
profile = self.translate_to_lygo(features)
with open(output_json, "w", encoding="utf-8") as f:
json.dump(profile, f, indent=2)
self._log(json.dumps(profile, indent=2))
self._log(f"\n[+] LYGO Profile saved → {output_json}")
self._log(f"[+] unique_id={profile['LYGO_PROFILE']['unique_id']}")
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}")
return profile
def _create_creative_brief(self, profile: Dict, path: Path) -> None:
data = profile["LYGO_PROFILE"]
dna = data["musical_dna"]
brief = f"""LYGO CREATIVE BRIEF
Generated: {data['generated_at']}
Source Image: {data['source']}
Unique ID: {data.get('unique_id', 'n/a')}
══════════════════════════════════════════════
MUSICAL DNA
══════════════════════════════════════════════
Key: {dna['root_key']}
BPM: {dna['bpm']}
Meter: {dna.get('meter', '4/4')}
Energy: {dna['energy_level']}
Genre: {dna['suggested_genre']}
Primary: {dna.get('primary_genre', '')}
Secondary: {dna.get('secondary_genre', '')}
Tags: {', '.join(dna.get('style_tags') or [])}
Texture & Vibe:
{dna['texture_description']}
Vocal Approach: {dna['vocal_style']}
══════════════════════════════════════════════
LYRICAL DIRECTION
══════════════════════════════════════════════
Core Theme: {data['lyrical_framework']['core_theme']}
AI Lyric Prompt:
{data['lyrical_framework']['ai_lyric_prompt']}
══════════════════════════════════════════════
SUNO STYLE (paste into Style field)
══════════════════════════════════════════════
{data.get('suno_style', '')}
══════════════════════════════════════════════
UDIO / FULL AI MUSIC PROMPT
══════════════════════════════════════════════
{data.get('udio_prompt', data['ai_music_prompt'])}
Full prompt:
{data['ai_music_prompt']}
Production Notes:
{data['production_notes']}
"""
path.write_text(brief, encoding="utf-8")
def main() -> None:
parser = argparse.ArgumentParser(
description="LYGO Profile Generator — unique musical + lyrical DNA per image"
)
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 .brief.txt")
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"))
+ sorted(folder.glob("*.webp"))
)
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!