| |
| """ |
| 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: |
| 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 |
|
|
| |
| 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 [] |
|
|
| |
| |
| 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) |
| |
| 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))) |
|
|
| |
| 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 |
|
|
| |
| iter_proxy = float(np.std([cv2.resize(edges, (0,0), fx=1/s, fy=1/s).mean() for s in [1,2,4,8]])) |
|
|
| |
| 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 |
| 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: |
| 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 |
|
|
| 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) |
| |
| 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) |
|
|
| |
| 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) |
|
|
| |
| 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) |
|
|
| |
| 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 |
|
|
| |
| 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 |
|
|
| |
| 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) |
| |
| |
| 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']}") |
|
|
| |
| 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() |