LYGO RESONANCE

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.

Live Engine (fixed)

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

How It Works

01

Upload Image

Any image works — manuscripts, drawings, photos, abstract art, or quick sketches.

02

Deep Analysis

Computer vision reads edges, contours, brightness, color, and structural density to extract resonant musical data.

03

Choose Output

Resonance Engine → High-quality stereo WAV audio
Profile Generator → Structured JSON + Creative Brief for AI music tools

04

Create & Export

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)

Help & Explanation

What is LYGO RESONANCE?

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.

🔊 The Resonance Engine (Audio)

This script generates a rich, stereo WAV soundscape based on an image's geometry, color, and texture. It creates four sonic layers:

  • Texture Floor — Filtered noise from edge density
  • Drones — Sustained tones from structural lines
  • Melodies — Musical notes from contour shapes
  • Glitch Micro-Events — Short percussive blips from corner detection

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

🧠 LYGO Profile Generator (JSON + Brief)

This script extracts fixed, deterministic mathematical data from an image and translates it into a structured creative profile. It includes:

  • Color analysis (hue, saturation, brightness, colorfulness)
  • Structural metrics (edge density, contrast, chaos keypoints)
  • Musical parameters (key, BPM, genre, texture, energy level)
  • Lyrical framework (core theme, AI lyric prompt, vocal style)
  • A full AI music prompt ready to copy into Suno, Udio, or ChatGPT

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.

📌 When to use which?

  • Resonance Engine → You want to hear the image. Use for ambient music, sound design, generative art projects, or simply exploring the hidden "song" of a drawing.
  • Profile Generator → You want data and direction. Use for writing lyrics, producing a track with AI, building a creative brief for a collaborator, or analyzing visual art through a musical lens.

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."

Developer Open Source Access

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).

🔒 Unlock Developer Resources

A friendly donation is not required, but deeply appreciated to help keep the servers running and the coffee flowing!

💖 Donate via PayPal

Clicking the button will instantly unlock both code modules below.

How to Run the Engines Locally

  1. Install Python: Make sure you have Python installed on your computer.
  2. Create your folder: Create a new folder on your Desktop (or anywhere you like) and name it something simple, like eidolon or lygo.
  3. Save your script: Inside that folder, save the desired code block below as resonance_engine.py (for audio) or lygo_profile.py (for JSON).
  4. Add your image: Place the image you want to translate into the same folder and name it image.jpg.
  5. Navigate to your folder (CRITICAL STEP): Open your terminal/command prompt and change to the directory where you saved your script.

    Example for macOS/Linux: cd Desktop/eidolon
    Example for Windows: cd Desktop\eidolon

    💡 Tip: In Windows Explorer, you can type cmd 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".
  6. Install dependencies: Type the following and hit Enter: pip install opencv-python numpy soundfile mido gradio requests
  7. Generate Output: Run the script by typing:

    python resonance_engine.py image.jpg --style cinematic
    or
    python lygo_profile.py image.jpg --brief

    A new WAV file or JSON file will appear in your folder!
Resonance Engine (Audio)
Profile Generator (JSON)
Gradio GUI (Web Interface)
Video Engine (Motion Audio)
LLM Integration (Lyrics)
#!/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()

Want to host this UI locally? Download app.py:

#!/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()

Live LYGO RESONANCE Stream

🎧 24/7 Generative Soundscape

Listen to LYGO RESONANCE running live — a continuous evolving audio environment.

Watch & Support on Rumble