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.

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

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.3
Image โ†’ Musical DNA + Lyrical Framework

Extracts visual mathematics from an image and translates it into
structured creative direction for music production and AI-assisted lyric writing.
"""

import cv2
import numpy as np
import json
import math
import argparse
from pathlib import Path
from datetime import datetime
from typing import Dict, Any, Optional

__version__ = "0.3.0"


class LYGOProfileGenerator:
    def __init__(self, verbose: bool = True):
        self.verbose = verbose

    def _log(self, msg: str):
        if self.verbose:
            print(msg)

    def analyze_image(self, image_path: str) -> Dict[str, Any]:
        """Extract rich mathematical features from the image."""
        img = cv2.imread(str(image_path))
        if img is None:
            raise FileNotFoundError(f"Image not found: {image_path}")

        if len(img.shape) == 2:
            img = cv2.cvtColor(img, cv2.COLOR_GRAY2BGR)

        h, w, _ = img.shape
        total_pixels = h * w

        # === Color Analysis (HSV) ===
        hsv = cv2.cvtColor(img, cv2.COLOR_BGR2HSV)
        avg_hue = float(np.mean(hsv[:, :, 0]) * 2)          # 0-360
        avg_sat = float(np.mean(hsv[:, :, 1]) / 255.0)
        avg_val = float(np.mean(hsv[:, :, 2]) / 255.0)
        sat_std = float(np.std(hsv[:, :, 1]) / 255.0)       # colorfulness

        # === Luminance & Contrast ===
        gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
        brightness = float(np.mean(gray) / 255.0)
        contrast = float(np.std(gray) / 255.0)

        # === Structural Analysis ===
        edges = cv2.Canny(gray, 50, 150)
        edge_density = float(np.count_nonzero(edges) / total_pixels)

        # Contours for structural complexity
        contours, _ = cv2.findContours(edges, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)
        structure_index = min(len(contours) / 80.0, 1.0)

        # Micro-chaos (FAST corners)
        fast = cv2.FastFeatureDetector_create(threshold=38)
        keypoints = fast.detect(gray, None)
        chaos_index = len(keypoints)

        features = {
            "source_image": str(Path(image_path).name),
            "dimensions": {"width": w, "height": h},
            "color": {
                "average_hue": round(avg_hue, 2),
                "average_saturation": round(avg_sat, 4),
                "average_brightness": round(brightness, 4),
                "colorfulness": round(sat_std, 4),
            },
            "structure": {
                "edge_density": round(edge_density, 4),
                "contrast": round(contrast, 4),
                "structure_index": round(structure_index, 4),
                "chaos_keypoints": chaos_index,
            },
        }
        return features

    def _get_musical_key(self, hue: float, brightness: float) -> str:
        keys = ["C", "G", "D", "A", "E", "B", "F#", "Db", "Ab", "Eb", "Bb", "F"]
        key_index = int(hue / 30) % 12
        mode = "Minor" if brightness < 0.48 else "Major"
        return f"{keys[key_index]} {mode}"

    def _calculate_bpm(self, edge_density: float, chaos: int, brightness: float) -> int:
        base = 82 + (edge_density * 920)
        chaos_mod = min(chaos / 1800, 0.6)
        brightness_mod = (brightness - 0.5) * 12
        bpm = int(base + (chaos_mod * 25) + brightness_mod)
        return max(78, min(178, bpm))

    def _generate_genre_texture(self, features: Dict) -> Dict[str, str]:
        e = features["structure"]["edge_density"]
        c = features["structure"]["chaos_keypoints"]
        b = features["color"]["average_brightness"]
        contrast = features["structure"]["contrast"]

        if c > 650 and b < 0.38:
            genre = "Industrial Dubstep / Dark Phonk"
            texture = "Heavy distortion, aggressive stutters, deep sub-bass, metallic textures"
            energy = "High-aggression"
        elif e > 0.065 and contrast > 0.18:
            genre = "Emo Rap / Modern Trap"
            texture = "Crisp hi-hats, melancholic melodies, heavy 808s, emotional vocal layers"
            energy = "Mid-High emotional"
        elif c > 420 and b > 0.55:
            genre = "Experimental / Glitch Hop"
            texture = "Glitchy percussion, chopped vocals, atmospheric synths, rhythmic complexity"
            energy = "High chaotic"
        elif e < 0.035 and b > 0.6:
            genre = "West Coast G-Funk / Smooth Instrumental"
            texture = "Laid-back grooves, warm analog bass, melodic leads, nostalgic atmosphere"
            energy = "Mid relaxed"
        else:
            genre = "Dark Alternative / Cinematic Rap"
            texture = "Atmospheric pads, punchy drums, moody synths, introspective energy"
            energy = "Mid cinematic"

        return {"genre": genre, "texture": texture, "energy": energy}

    def translate_to_lygo(self, features: Dict[str, Any]) -> Dict[str, Any]:
        """Convert visual features into musical and lyrical creative direction."""
        hue = features["color"]["average_hue"]
        brightness = features["color"]["average_brightness"]
        edge_density = features["structure"]["edge_density"]
        chaos = features["structure"]["chaos_keypoints"]
        contrast = features["structure"]["contrast"]

        musical_key = self._get_musical_key(hue, brightness)
        bpm = self._calculate_bpm(edge_density, chaos, brightness)
        genre_data = self._generate_genre_texture(features)

        # === Lyrical Theme Engine ===
        if brightness < 0.42 and edge_density > 0.055:
            core_theme = "Survival, betrayal, lone wolf resilience, moving in silence"
            lyric_prompt = (
                "Write raw, introspective lyrics about being the last one standing after betrayal. "
                "Focus on trust issues, a very small circle of ride-or-die people, and the cold satisfaction of outlasting everyone who counted you out."
            )
            vocal_style = "Raspy melodic rap or gritty sung-rap hybrid"
        elif chaos > 550:
            core_theme = "Breaking chains, system resistance, unchained personal power"
            lyric_prompt = (
                "Write aggressive yet intelligent lyrics about breaking free from systems that tried to define you. "
                "Emphasize resilience, moving in silence, and turning pain into unstoppable momentum."
            )
            vocal_style = "Assertive rap with melodic moments or distorted vocal processing"
        else:
            core_theme = "Observation, loyalty, navigating a cold modern world with quiet edge"
            lyric_prompt = (
                "Write clever, slightly dark observational lyrics with dry humor about modern life, loyalty, "
                "and staying true to your own code while everything around you feels artificial."
            )
            vocal_style = "Deadpan to melodic rap delivery, slightly introspective"

        # === Final Structured Output ===
        lygo_profile = {
            "LYGO_PROFILE": {
                "version": __version__,
                "generated_at": datetime.now().isoformat(),
                "source": features["source_image"],
                "mathematics": features,
                "musical_dna": {
                    "root_key": musical_key,
                    "bpm": bpm,
                    "energy_level": genre_data["energy"],
                    "suggested_genre": genre_data["genre"],
                    "texture_description": genre_data["texture"],
                    "vocal_style": vocal_style,
                },
                "lyrical_framework": {
                    "core_theme": core_theme,
                    "ai_lyric_prompt": lyric_prompt,
                },
                "ai_music_prompt": (
                    f"Create a {genre_data['genre']} track at {bpm} BPM in the key of {musical_key}. "
                    f"The overall energy should feel {genre_data['energy'].lower()}. "
                    f"Sound design and texture: {genre_data['texture']}. "
                    f"Lyrical themes should center around {core_theme}."
                ),
                "production_notes": (
                    f"High contrast and structural complexity suggest strong dynamic range. "
                    f"Consider heavy low-end support and atmospheric layers to match the visual weight."
                ),
            }
        }
        return lygo_profile

    def generate(self, image_path: str, output_json: str = "lygo_profile.json", create_brief: bool = False):
        self._log(f"\nโ•”โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•—")
        self._log(f"โ•‘           LYGO Profile Generator v{__version__}      โ•‘")
        self._log(f"โ•‘     Image โ†’ Musical DNA + Lyrical Frameworkโ•‘")
        self._log(f"โ•šโ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•\n")

        features = self.analyze_image(image_path)
        profile = self.translate_to_lygo(features)

        # Save JSON
        with open(output_json, "w") as f:
            json.dump(profile, f, indent=2)

        self._log(json.dumps(profile, indent=2))
        self._log(f"\n[+] LYGO Profile saved โ†’ {output_json}")

        if create_brief:
            brief_path = Path(output_json).with_suffix(".brief.txt")
            self._create_creative_brief(profile, brief_path)
            self._log(f"[+] Creative Brief saved โ†’ {brief_path}")

    def _create_creative_brief(self, profile: Dict, path: Path):
        data = profile["LYGO_PROFILE"]
        brief = f"""LYGO CREATIVE BRIEF
Generated: {data['generated_at']}
Source Image: {data['source']}

โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•
MUSICAL DNA
โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•
Key: {data['musical_dna']['root_key']}
BPM: {data['musical_dna']['bpm']}
Energy: {data['musical_dna']['energy_level']}
Genre Direction: {data['musical_dna']['suggested_genre']}

Texture & Vibe:
{data['musical_dna']['texture_description']}

Vocal Approach: {data['musical_dna']['vocal_style']}

โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•
LYRICAL DIRECTION
โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•
Core Theme: {data['lyrical_framework']['core_theme']}

AI Prompt:
{data['lyrical_framework']['ai_lyric_prompt']}

โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•
FULL AI MUSIC PROMPT (Copy-Paste Ready)
โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•
{data['ai_music_prompt']}

Production Notes:
{data['production_notes']}
"""
        path.write_text(brief, encoding='utf-8')


def main():
    parser = argparse.ArgumentParser(
        description="LYGO Profile Generator โ€” Turn any image into structured musical + lyrical creative direction"
    )
    parser.add_argument("image", help="Path to input image")
    parser.add_argument("-o", "--output", default="lygo_profile.json", help="Output JSON file")
    parser.add_argument("--brief", action="store_true", help="Also generate a human-readable .brief.txt file")
    parser.add_argument("--batch", action="store_true", help="Process all images in a folder")
    parser.add_argument("--quiet", action="store_true", help="Suppress console output")
    args = parser.parse_args()

    generator = LYGOProfileGenerator(verbose=not args.quiet)

    if args.batch:
        folder = Path(args.image)
        if not folder.is_dir():
            print("Error: --batch requires a folder path")
            return
        images = sorted(folder.glob("*.jpg")) + sorted(folder.glob("*.png")) + sorted(folder.glob("*.jpeg"))
        if not images:
            print("No images found in folder")
            return
        for img in images:
            print(f"\nProcessing: {img.name}")
            out_json = f"lygo_profile_{img.stem}.json"
            generator.generate(str(img), out_json, create_brief=args.brief)
        return

    generator.generate(args.image, args.output, create_brief=args.brief)


if __name__ == "__main__":
    main()

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