| |
| """ |
| 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 |
|
|
| |
| hsv = cv2.cvtColor(img, cv2.COLOR_BGR2HSV) |
| avg_hue = float(np.mean(hsv[:, :, 0]) * 2) |
| 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) |
|
|
| |
| gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY) |
| brightness = float(np.mean(gray) / 255.0) |
| contrast = float(np.std(gray) / 255.0) |
|
|
| |
| edges = cv2.Canny(gray, 50, 150) |
| edge_density = float(np.count_nonzero(edges) / total_pixels) |
|
|
| |
| 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 = 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) |
|
|
| |
| 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" |
|
|
| |
| 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) |
|
|
| |
| 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() |