#!/usr/bin/env python3 """ LYGO FractalWeaver v0.1 Weaves self-similar visuals (fractals, recursive patterns) into evolving audio textures. Integrates with LYGO RESONANCE engine (falls back to built-in evolving synth). Analyzes fractal self-similarity (dimension, iteration, recursive structure) and maps to audio that evolves organically: - Textures layer and "zoom" recursively over time. - Parameters modulate with fractal depth (e.g., drones shift with scale, glitches branch with iteration). - Self-similar motifs repeat at different time/frequency "octaves". Outputs: - Stereo WAV with evolving texture (default 30-60s). - .fractal.weave.json profile with LYGO mappings (fractal_dimension, self_similarity_evolution, recursive_harmony, etc.). - Optional stems, MIDI with fractal-like self-similar patterns. Usage examples: python fractalweaver.py my_mandelbrot.png --preset fractal-mandel --seed 963 --duration 45 python fractalweaver.py --generate-mandelbrot --output test_fractal.png --width 512 --max_iter 60 python fractalweaver.py --batch ./fractals/ --preset sierpinski-weave Ties to LYGO ecosystem: - Companion to lygo-resonance for fractal visuals. - Compatible with lygo-ollama-army (fractal-weaver or resonance-analyst roles, champions like ARKOS/COSMARA). - Grows profiles to 3-Brain as recursive/self-similar nodes. - P0/Oath/Guardian aware: local-first, seed-locked reproducibility, review before external use. Full instructions in SKILL.md. Links to Resonance site and donation included. """ import cv2 import numpy as np import soundfile as sf import math import argparse import json from pathlib import Path from datetime import datetime from typing import Dict, Any, Optional import sys try: from resonance_engine import ResonanceEngine, PRESETS HAS_FULL_ENGINE = True except ImportError: HAS_FULL_ENGINE = False PRESETS = { "fractal-mandel": {"noise_vol": 0.06, "drone_vol": 0.08, "note_vol": 0.12, "glitch_vol": 0.035}, "sierpinski-weave": {"noise_vol": 0.04, "drone_vol": 0.10, "note_vol": 0.09, "glitch_vol": 0.02}, "julia-harmonic": {"noise_vol": 0.05, "drone_vol": 0.12, "note_vol": 0.14, "glitch_vol": 0.015}, } __version__ = "0.1.0" def generate_fractal_image(fractal_type: str = "mandelbrot", width: int = 512, height: int = 512, max_iter: int = 50, cx: float = -0.7, cy: float = 0.3, zoom: float = 1.0) -> np.ndarray: """Generate a simple fractal image using numpy for demo/testing.""" img = np.zeros((height, width, 3), dtype=np.uint8) if fractal_type == "mandelbrot": for x in range(width): for y in range(height): zx, zy = (x - width / 2) / (0.5 * zoom * width) , (y - height / 2) / (0.5 * zoom * height) c = complex(zx, zy) z = complex(0, 0) for i in range(max_iter): if abs(z) > 2: break z = z * z + c color = int(255 * i / max_iter) img[y, x] = [color, int(color * 0.7), int(color * 0.4)] elif fractal_type == "julia": for x in range(width): for y in range(height): zx = (x - width / 2) / (0.5 * zoom * width) zy = (y - height / 2) / (0.5 * zoom * height) z = complex(zx, zy) c = complex(cx, cy) for i in range(max_iter): if abs(z) > 2: break z = z * z + c color = int(255 * i / max_iter) img[y, x] = [int(color * 0.6), color, int(color * 0.8)] else: # simple recursive tree-like pattern img = np.zeros((height, width, 3), dtype=np.uint8) def draw_tree(x, y, angle, depth, length): if depth == 0: return x2 = int(x + length * math.cos(math.radians(angle))) y2 = int(y - length * math.sin(math.radians(angle))) cv2.line(img, (int(x), int(y)), (x2, y2), (200, 180, 255), 1) draw_tree(x2, y2, angle - 30, depth - 1, length * 0.7) draw_tree(x2, y2, angle + 30, depth - 1, length * 0.7) draw_tree(width // 2, height - 20, -90, 8, 80) return img def analyze_fractal(image_path: str) -> Dict[str, Any]: """Analyze self-similar/fractal properties (builds on LYGO RESONANCE CV + Glyph2Resonance).""" img = cv2.imread(str(image_path)) if img is None: raise FileNotFoundError(f"Could not load fractal 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 total = h * w # Base features avg_blue, avg_green, avg_red, _ = cv2.mean(img) edges = cv2.Canny(gray, 50, 150) edge_density = np.sum(edges > 0) / total contours, _ = cv2.findContours(edges, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE) fast = cv2.FastFeatureDetector_create(threshold=38) keypoints = fast.detect(gray, None) or [] # Fractal / Self-similarity specific # Approximate fractal dimension (box counting simplified) scales = [2, 4, 8, 16] counts = [] for s in scales: boxes = 0 for y in range(0, h, s): for x in range(0, w, s): if np.any(edges[y:y+s, x:x+s] > 0): boxes += 1 counts.append(boxes if boxes > 0 else 1) # log-log slope for dimension logs = np.log(scales) logc = np.log(counts) dim = -np.polyfit(logs, logc, 1)[0] if len(logs) > 1 else 1.5 fractal_dimension = float(max(1.0, min(dim, 2.0))) # Self-similarity score (compare to downscaled versions) sim_scores = [] for factor in [0.5, 0.25]: small = cv2.resize(gray, (0,0), fx=factor, fy=factor) if small.shape[0] > 10 and small.shape[1] > 10: diff = np.abs(gray.astype(float)[:small.shape[0], :small.shape[1]] - cv2.resize(small, (gray.shape[1], gray.shape[0])).astype(float)) score = 1.0 - (np.mean(diff) / 255.0) sim_scores.append(max(0.0, min(1.0, score))) self_similarity = float(np.mean(sim_scores)) if sim_scores else 0.5 # Iteration / recursion proxy (multi-scale complexity) iter_proxy = float(np.std([cv2.resize(edges, (0,0), fx=1/s, fy=1/s).mean() for s in [1,2,4,8]])) # Branching (radial/contour variation) radial_variation = 0.0 if contours: areas = [cv2.contourArea(c) for c in contours] radial_variation = float(np.std(areas) / (np.mean(areas) + 1)) features = { "width": w, "height": h, "avg_red": round(avg_red, 1), "avg_green": round(avg_green, 1), "avg_blue": round(avg_blue, 1), "edge_density": round(edge_density, 5), "fractal_dimension": round(fractal_dimension, 3), "self_similarity": round(self_similarity, 4), "iteration_proxy": round(iter_proxy, 4), "branching": round(radial_variation, 4), "num_contours": len(contours), "num_keypoints": len(keypoints), "source": Path(image_path).name, } return features def map_to_evolving_params(features: Dict[str, Any], preset: str = "fractal-mandel") -> Dict[str, Any]: """Map fractal features to base params + evolution curves for weaving.""" base_root = 30 + features["avg_red"] * 0.12 base_theta = 5.0 + features["avg_green"] * 0.02 dim = features["fractal_dimension"] sim = features["self_similarity"] it = features["iteration_proxy"] br = features["branching"] edge = features["edge_density"] if preset == "fractal-mandel": base_drone = 0.07 + dim * 0.04 base_note = 0.11 + sim * 0.05 base_glitch = 0.025 + it * 0.03 evolution_rate = 0.8 + br * 1.2 # how fast params change weave_layers = int(4 + dim * 3) elif preset == "sierpinski-weave": base_drone = 0.09 + sim * 0.06 base_note = 0.08 + edge * 0.05 base_glitch = 0.015 + br * 0.02 evolution_rate = 1.2 + it * 0.8 weave_layers = int(3 + sim * 4) else: # julia-harmonic base_drone = 0.10 + dim * 0.03 base_note = 0.13 + sim * 0.04 base_glitch = 0.012 + it * 0.02 evolution_rate = 0.6 + br * 0.9 weave_layers = int(5 + dim * 2) base_config = { "sr": 44100, "duration": 40.0, "root_freq_range": (base_root - 2, base_root + 2), "theta_lock_range": (base_theta - 0.5, base_theta + 0.5), "noise_vol": min(0.05 + edge * 0.04, 0.14), "drone_vol": min(base_drone, 0.16), "note_vol": min(base_note, 0.16), "glitch_vol": min(base_glitch, 0.10), "max_drones": max(3, min(weave_layers, 8)), "max_notes": max(4, min(int(weave_layers * 1.5), 12)), "max_glitches": int(10 + it * 15), "random_seed": None, "verbose": True, } lygo_meta = { "fractal_dimension": round(dim, 3), "self_similarity": round(sim, 4), "iteration_evolution": round(it, 4), "recursive_harmony": round((sim + (dim - 1.0)) / 2, 4), "weave_complexity": round(br + edge, 4), "evolution_rate": round(evolution_rate, 2), "suggested_duration": base_config["duration"], "preset_used": preset, } return base_config, lygo_meta def synthesize_evolving_texture(features: Dict, config: Dict, output_wav: str): """Built-in evolving synth (segments with interpolated params for self-similar evolution).""" sr = config["sr"] dur = config.get("duration", 40.0) n = int(sr * dur) audio = np.zeros(n, dtype=np.float32) root = float(np.mean(config["root_freq_range"])) theta = float(np.mean(config.get("theta_lock_range", (5.0, 6.0)))) evo = features.get("iteration_evolution", 0.5) + 0.5 # normalized evolution speed num_segments = max(4, int(evo * 8)) seg_len = n // num_segments for seg in range(num_segments): t = seg / max(1, num_segments - 1) # Evolve params self-similarly (zoom in = more detail, higher freqs, more glitches) cur_root = root + t * (seg % 3) * 4 cur_drone_v = config["drone_vol"] * (0.7 + t * 0.6) cur_glitch_v = config["glitch_vol"] * (0.5 + t * 1.2) cur_note_v = config["note_vol"] * (0.8 + (1 - t) * 0.4) start = seg * seg_len end = min(start + seg_len, n) seg_n = end - start if seg_n <= 0: continue t_arr = np.linspace(0, seg_n / sr, seg_n, False, dtype=np.float32) # Layer 1: Base texture (evolving noise floor) noise = (np.random.uniform(-0.8, 0.8, seg_n) * 0.5).astype(np.float32) audio[start:end] += noise * config["noise_vol"] * (0.6 + t * 0.8) # Layer 2: Drones that "branch" (self-similar freqs) for i in range(config["max_drones"]): f = cur_root + (i * theta * (1 + t * 0.3)) tone = np.sin(2 * np.pi * f * t_arr).astype(np.float32) audio[start:end] += tone * cur_drone_v * (0.8 + (i % 2) * 0.2) # Layer 3: Notes/motifs that repeat at different "scales" (time offsets) for i in range(config["max_notes"]): f = cur_root * 1.8 + (i * theta * 0.8) note_dur = 0.6 + (seg % 3) * 0.2 note_t = np.linspace(0, note_dur, min(int(sr * note_dur), seg_n), False, dtype=np.float32) tone = np.sin(2 * np.pi * f * note_t).astype(np.float32) note_start = start + (i * 1200) % (seg_n - len(tone) if seg_n > len(tone) else 0) note_end = min(note_start + len(tone), end) if note_end > note_start: audio[note_start:note_end] += tone[:note_end - note_start] * cur_note_v # Layer 4: Glitch "recursion" (bursts that echo the structure) for i in range(int(config["max_glitches"] * (0.6 + t * 0.8))): f = cur_root * 3.5 + (i % 7) * 11 g_t = np.linspace(0, 0.04, int(sr * 0.04), False, dtype=np.float32) tone = np.sin(2 * np.pi * f * g_t).astype(np.float32) g_start = start + (i * 700 + int(t * 2000)) % (seg_n - len(g_t) if seg_n > len(g_t) else 0) g_end = min(g_start + len(tone), end) if g_end > g_start: audio[g_start:g_end] += tone[:g_end - g_start] * cur_glitch_v # Final polish audio = np.tanh(audio * 1.5) / 1.5 peak = np.max(np.abs(audio)) if peak > 0: audio = (audio / peak * 0.95).astype(np.float32) sf.write(output_wav, audio, sr) return output_wav def main(): parser = argparse.ArgumentParser( description="LYGO FractalWeaver — Turn self-similar visuals and fractals into evolving resonant audio textures" ) parser.add_argument("image", nargs="?", help="Input fractal/self-similar image") parser.add_argument("--preset", choices=["fractal-mandel", "sierpinski-weave", "julia-harmonic"], default="fractal-mandel") parser.add_argument("--seed", type=int, default=None) parser.add_argument("--duration", type=float, default=40.0) parser.add_argument("-o", "--output", default=None) parser.add_argument("--profile", default=None) parser.add_argument("--batch", action="store_true") parser.add_argument("--generate-mandelbrot", action="store_true") parser.add_argument("--generate-julia", action="store_true") parser.add_argument("--width", type=int, default=512) parser.add_argument("--height", type=int, default=512) parser.add_argument("--max_iter", type=int, default=50) args = parser.parse_args() if args.generate_mandelbrot or args.generate_julia: ftype = "mandelbrot" if args.generate_mandelbrot else "julia" img = generate_fractal_image(ftype, args.width, args.height, args.max_iter) out = f"generated_{ftype}.png" cv2.imwrite(out, img) print(f"Generated test fractal: {out}") return if not args.image: parser.print_help() return img_path = Path(args.image) if args.batch: folder = img_path images = sorted(list(folder.glob("*.png")) + list(folder.glob("*.jpg")) + list(folder.glob("*.jpeg"))) if not images: print("No images found.") return for im in images: print(f"\nWeaving fractal: {im.name}") _process_one(im, args.preset, args.seed, args.duration, args.output, args.profile) return _process_one(img_path, args.preset, args.seed, args.duration, args.output, args.profile) def _process_one(image_path: Path, preset: str, seed: Optional[int], duration: float, out_wav: Optional[str], out_json: Optional[str]): features = analyze_fractal(str(image_path)) config, lygo_meta = map_to_evolving_params(features, preset) if seed is not None: config["random_seed"] = seed config["duration"] = duration wav_path = out_wav or f"fractalweave_{image_path.stem}.wav" json_path = out_json or f"fractalweave_{image_path.stem}.fractal.weave.json" if HAS_FULL_ENGINE: eng_config = config.copy() eng_config.update(PRESETS.get(preset, {})) engine = ResonanceEngine(eng_config) # For evolving effect, we still use our segment-based approach below for now # (full engine can be extended later; this keeps the "weave" logic) synthesize_evolving_texture(features, config, wav_path) full_profile = { "LYGO_FRACTALWEAVER": { "version": __version__, "source_fractal": str(image_path), "preset": preset, "features": features, "audio_config": config, "lygo_mappings": lygo_meta, "generated_at": datetime.now().isoformat(), "reproducible_with_seed": seed, } } with open(json_path, "w", encoding="utf-8") as f: json.dump(full_profile, f, indent=2) print(f"✓ Evolving texture: {wav_path}") print(f"✓ Profile: {json_path}") print(f" dimension={lygo_meta['fractal_dimension']}, self_sim={lygo_meta['self_similarity']}, recursive_harmony={lygo_meta['recursive_harmony']}") # Grow to 3-Brain (recursive/self-similar nodes) try: sys.path.insert(0, str(Path.cwd())) from lyra_brain import LyraThreeBrainMemory brain = LyraThreeBrainMemory(base_dir=Path.cwd(), use_advanced=True) summary = f"FractalWeaver: {image_path.name} → {preset} evolving weave | dim={lygo_meta['fractal_dimension']} sim={lygo_meta['self_similarity']} harmony={lygo_meta['recursive_harmony']}" nid = brain.grow(summary, source="fractalweaver") print(f" Grown to 3-Brain node: {nid}") except Exception: pass if __name__ == "__main__": main()