DeepSeekOracle commited on
Commit
7d29f6d
Β·
verified Β·
1 Parent(s): 1c07387

Upload factory_engine.py with huggingface_hub

Browse files
Files changed (1) hide show
  1. factory_engine.py +386 -0
factory_engine.py ADDED
@@ -0,0 +1,386 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/env python3
2
+ """
3
+ FACTORY DEFAULT RESONANCE ENGINE (Isolated Pure Gradio Factory)
4
+ This is the ORIGINAL working 4-layer sonification engine, completely separate from LYGO/LDQ.
5
+
6
+ - NO LDQ imports, NO use_ldq, NO sidechain, NO genre manifold, NO perceptual, NO fingerprint.
7
+ - Pure image-driven stereo soundscapes using only the classic layers:
8
+ 1. Texture Floor (filtered noise from edge density)
9
+ 2. Drones (saw from structural lines)
10
+ 3. Melody (sine from contours)
11
+ 4. Glitch (short tones from FAST keypoints)
12
+
13
+ Goal: Reliable "industry quality beats / musical" output as the default factory experience.
14
+ This file is the drop-in clean factory. The LYGO tab continues to use the other engine.
15
+
16
+ Based on the clean original implementations that produced good musical results (historical working versions).
17
+ """
18
+
19
+ import cv2
20
+ import numpy as np
21
+ import soundfile as sf
22
+ import math
23
+ from pathlib import Path
24
+ from typing import Optional, Dict, Any
25
+
26
+ __version__ = "factory-1.0.0-isolated"
27
+
28
+ # Artistic Presets tuned for musical / "industry beats" factory output
29
+ # (balanced noise, strong drones + contour melodies for rhythmic feel, tasteful glitch)
30
+ PRESETS = {
31
+ "raw": {
32
+ "noise_vol": 0.06,
33
+ "drone_vol": 0.07,
34
+ "note_vol": 0.12,
35
+ "glitch_vol": 0.02,
36
+ "noise_lowpass_hz": 800,
37
+ },
38
+ "ambient": {
39
+ "noise_vol": 0.04,
40
+ "drone_vol": 0.09,
41
+ "note_vol": 0.10,
42
+ "glitch_vol": 0.008,
43
+ "drone_attack": 5.0,
44
+ "drone_decay": 6.0,
45
+ "note_decay": 0.55,
46
+ "noise_lowpass_hz": 450,
47
+ },
48
+ "glitch": {
49
+ "noise_vol": 0.07,
50
+ "drone_vol": 0.05,
51
+ "note_vol": 0.09,
52
+ "glitch_vol": 0.06,
53
+ "max_glitches": 25,
54
+ "noise_lowpass_hz": 2200,
55
+ },
56
+ "ethereal": {
57
+ "noise_vol": 0.03,
58
+ "drone_vol": 0.08,
59
+ "note_vol": 0.11,
60
+ "glitch_vol": 0.01,
61
+ "note_decay": 0.6,
62
+ "noise_lowpass_hz": 380,
63
+ },
64
+ "cinematic": {
65
+ "noise_vol": 0.05,
66
+ "drone_vol": 0.08,
67
+ "note_vol": 0.13,
68
+ "glitch_vol": 0.025,
69
+ "drone_attack": 4.0,
70
+ "drone_decay": 5.5,
71
+ "note_attack": 0.05,
72
+ "note_decay": 0.38,
73
+ "max_glitches": 18,
74
+ "noise_lowpass_hz": 720,
75
+ "root_freq_range": (28, 72),
76
+ },
77
+ # Strong musical factory default - "industry quality" feel
78
+ "musical": {
79
+ "noise_vol": 0.045,
80
+ "drone_vol": 0.085,
81
+ "note_vol": 0.135,
82
+ "glitch_vol": 0.022,
83
+ "noise_lowpass_hz": 680,
84
+ "drone_attack": 3.8,
85
+ "drone_decay": 5.2,
86
+ "note_decay": 0.36,
87
+ "max_notes": 9,
88
+ "max_glitches": 16,
89
+ },
90
+ }
91
+
92
+
93
+ class FactoryResonanceEngine:
94
+ """Completely isolated factory default engine. No LYGO/LDQ dependencies."""
95
+
96
+ def __init__(self, config: Optional[Dict[str, Any]] = None):
97
+ self.config = {
98
+ "sr": 44100,
99
+ "duration": 15.0,
100
+ "global_fade": 0.65,
101
+ "soft_clip": True,
102
+ "soft_clip_amount": 1.35,
103
+ "max_drones": 5,
104
+ "max_notes": 8,
105
+ "max_glitches": 14,
106
+ "noise_vol": 0.05,
107
+ "drone_vol": 0.07,
108
+ "note_vol": 0.12,
109
+ "glitch_vol": 0.018,
110
+ "root_freq_range": (32, 68),
111
+ "theta_lock_range": (4.8, 10.5),
112
+ "drone_attack": 3.8,
113
+ "drone_decay": 5.0,
114
+ "note_attack": 0.045,
115
+ "note_decay": 0.34,
116
+ "glitch_attack": 0.004,
117
+ "glitch_decay": 0.014,
118
+ "noise_lowpass_hz": 650,
119
+ "random_seed": None,
120
+ "verbose": True,
121
+ "export_stems": False,
122
+ "export_midi": False,
123
+ }
124
+ if config:
125
+ self.config.update(config)
126
+
127
+ def _log(self, msg: str):
128
+ if self.config.get("verbose", True):
129
+ print(msg)
130
+
131
+ def analyze_image(self, image_path: str) -> Dict[str, Any]:
132
+ """Standard computer vision analysis β€” unchanged from original working versions."""
133
+ img = cv2.imread(str(image_path))
134
+ if img is None:
135
+ raise FileNotFoundError(f"Could not load image: {image_path}")
136
+ if len(img.shape) == 2:
137
+ img = cv2.cvtColor(img, cv2.COLOR_GRAY2BGR)
138
+ gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
139
+ h, w = gray.shape
140
+ avg_blue, avg_green, avg_red, _ = cv2.mean(img)
141
+ edges = cv2.Canny(gray, 50, 150)
142
+ edge_density = np.sum(edges > 0) / (h * w)
143
+ contours, _ = cv2.findContours(edges, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)
144
+ lines = cv2.HoughLinesP(edges, 1, np.pi / 180, 50, minLineLength=28, maxLineGap=12)
145
+ fast = cv2.FastFeatureDetector_create(threshold=38)
146
+ keypoints = fast.detect(gray, None)
147
+ return {
148
+ "width": w, "height": h,
149
+ "avg_red": avg_red, "avg_green": avg_green, "avg_blue": avg_blue,
150
+ "edge_density": edge_density,
151
+ "contours": contours,
152
+ "lines": lines if lines is not None else [],
153
+ "keypoints": keypoints,
154
+ }
155
+
156
+ def _generate_tone(self, freq: float, duration: float, wave_type: str = "sine") -> np.ndarray:
157
+ sr = self.config["sr"]
158
+ n = int(sr * duration)
159
+ if n <= 0:
160
+ return np.zeros(1, dtype=np.float32)
161
+ t = np.linspace(0, duration, n, dtype=np.float32)
162
+ if wave_type == "sine":
163
+ return np.sin(freq * t * 2 * np.pi).astype(np.float32)
164
+ elif wave_type == "sawtooth":
165
+ return (2 * (t * freq - np.floor(0.5 + t * freq)) * 0.58).astype(np.float32)
166
+ elif wave_type == "noise":
167
+ return np.random.uniform(-0.32, 0.32, n).astype(np.float32)
168
+ return np.zeros(n, dtype=np.float32)
169
+
170
+ def _apply_envelope(self, audio: np.ndarray, attack: float, decay: float) -> np.ndarray:
171
+ sr = self.config["sr"]
172
+ n = len(audio)
173
+ if n <= 0:
174
+ return audio
175
+ a = max(1, int(attack * sr))
176
+ d = max(1, int(decay * sr))
177
+ env = np.ones(n, dtype=np.float32)
178
+ if n > a + d:
179
+ env[:a] = np.linspace(0, 1, a, dtype=np.float32)
180
+ env[-d:] = np.linspace(1, 0, d, dtype=np.float32)
181
+ return (audio * env).astype(np.float32)
182
+
183
+ def _stereo_pan(self, mono: np.ndarray, pan: float) -> np.ndarray:
184
+ if mono.ndim == 1:
185
+ mono = mono[:, np.newaxis]
186
+ pan = max(-1.0, min(1.0, pan))
187
+ left = math.cos((pan + 1) * math.pi / 4)
188
+ right = math.sin((pan + 1) * math.pi / 4)
189
+ return np.column_stack((mono * left, mono * right)).astype(np.float32)
190
+
191
+ def _fft_lowpass(self, audio: np.ndarray, cutoff_hz: float) -> np.ndarray:
192
+ if cutoff_hz <= 0 or len(audio) < 32:
193
+ return audio
194
+ sr = self.config["sr"]
195
+ n = len(audio)
196
+ fft = np.fft.rfft(audio)
197
+ freqs = np.fft.rfftfreq(n, 1.0 / sr)
198
+ fft[freqs > cutoff_hz] = 0
199
+ return np.fft.irfft(fft, n=n).real.astype(np.float32)
200
+
201
+ def _soft_limit(self, audio: np.ndarray) -> np.ndarray:
202
+ if self.config["soft_clip"]:
203
+ amt = self.config["soft_clip_amount"]
204
+ return (np.tanh(audio * amt) / np.tanh(amt)).astype(np.float32)
205
+ return audio
206
+
207
+ def synthesize(self, features: Dict[str, Any], output_path: str):
208
+ cfg = self.config
209
+ if cfg["random_seed"] is not None:
210
+ np.random.seed(cfg["random_seed"])
211
+
212
+ sr = cfg["sr"]
213
+ duration = cfg["duration"]
214
+ n_total = int(sr * duration)
215
+ if n_total <= 0:
216
+ sf.write(output_path, np.zeros((1, 2)), sr)
217
+ return
218
+
219
+ audio = np.zeros((n_total, 2), dtype=np.float32)
220
+ root = np.interp(features["avg_red"], [0, 255], cfg["root_freq_range"])
221
+ theta = np.interp(features["avg_green"], [0, 255], cfg["theta_lock_range"])
222
+ w, h = features["width"], features["height"]
223
+
224
+ self._log("🎡 FACTORY ENGINE β€” Pure 4-layer (no LYGO/LDQ)")
225
+
226
+ # Collections for stems
227
+ audio_noise = np.zeros((n_total, 2), dtype=np.float32)
228
+ audio_drone = np.zeros((n_total, 2), dtype=np.float32)
229
+ audio_melody = np.zeros((n_total, 2), dtype=np.float32)
230
+ audio_glitch = np.zeros((n_total, 2), dtype=np.float32)
231
+ melody_events = []
232
+
233
+ # Layer 1: Texture Floor (Noise) β€” filtered for musicality
234
+ if features["edge_density"] > 0.006:
235
+ noise = self._generate_tone(0, duration, "noise")
236
+ if cfg.get("noise_lowpass_hz", 650) > 0:
237
+ noise = self._fft_lowpass(noise, cfg["noise_lowpass_hz"])
238
+ noise = self._apply_envelope(noise, cfg["drone_attack"], cfg["drone_decay"])
239
+ vol = min(features["edge_density"] * 1.35, cfg["noise_vol"])
240
+ stereo_noise = self._stereo_pan(noise, 0.0) * vol
241
+ audio += stereo_noise
242
+ audio_noise += stereo_noise
243
+ self._log(f" Layer1 noise vol={vol:.3f} lowpass={cfg.get('noise_lowpass_hz')}")
244
+
245
+ # Layer 2: Drones (Lines) β€” rhythmic backbone
246
+ for i, line in enumerate(features["lines"][:cfg["max_drones"]]):
247
+ x1, _, x2, _ = line[0]
248
+ length = math.hypot(x2 - x1, 0)
249
+ detune = (i * 0.65) if cfg["random_seed"] is not None else 0
250
+ freq = root + (max(1, int(length / 46)) * theta * 0.58) + detune
251
+ tone = self._generate_tone(freq, duration, "sawtooth")
252
+ tone = self._apply_envelope(tone, cfg["drone_attack"], cfg["drone_decay"])
253
+ pan = (x1 / w) * 2 - 1
254
+ stereo_drone = self._stereo_pan(tone, pan) * cfg["drone_vol"]
255
+ audio += stereo_drone
256
+ audio_drone += stereo_drone
257
+
258
+ # Layer 3: Contours β†’ Melody (main musical/rhythmic content)
259
+ valid = [c for c in features["contours"] if 85 < cv2.contourArea(c) < (w * h * 0.62)]
260
+ valid.sort(key=lambda c: cv2.boundingRect(c)[0])
261
+
262
+ for i, cnt in enumerate(valid[:cfg["max_notes"]]):
263
+ area = cv2.contourArea(cnt)
264
+ verts = len(cv2.approxPolyDP(cnt, 0.04 * cv2.arcLength(cnt, True), True))
265
+ freq = (root * 3.65) + (verts * theta * 1.55)
266
+ dur = min(2.8, 0.18 + (area / 12800))
267
+ tone = self._generate_tone(freq, dur, "sine")
268
+ tone = self._apply_envelope(tone, cfg["note_attack"], cfg["note_decay"])
269
+
270
+ M = cv2.moments(cnt)
271
+ cx = int(M["m10"] / M["m00"]) if M["m00"] != 0 else cv2.boundingRect(cnt)[0]
272
+ start = (cx / w) * (duration - dur)
273
+ idx = int(start * sr)
274
+ end = min(idx + len(tone), n_total)
275
+ pan = (cx / w) * 2 - 1
276
+ stereo_note = self._stereo_pan(tone[:end-idx], pan) * cfg["note_vol"]
277
+ audio[idx:end] += stereo_note
278
+ audio_melody[idx:end] += stereo_note
279
+ melody_events.append((freq, dur, start))
280
+
281
+ # Layer 4: Glitch / Micro events (adds percussive "beat" character)
282
+ for i, kp in enumerate(features["keypoints"][:cfg["max_glitches"]]):
283
+ x, y = kp.pt
284
+ freq = root * 13.2 + (y % 82) * 1.35
285
+ tone = self._generate_tone(freq, 0.048, "sine")
286
+ tone = self._apply_envelope(tone, cfg["glitch_attack"], cfg["glitch_decay"])
287
+ start = (y / h) * (duration - 0.048)
288
+ idx = int(start * sr)
289
+ end = min(idx + len(tone), n_total)
290
+ pan = (x / w) * 2 - 1
291
+ stereo_glitch = self._stereo_pan(tone[:end-idx], pan) * cfg["glitch_vol"]
292
+ audio[idx:end] += stereo_glitch
293
+ audio_glitch[idx:end] += stereo_glitch
294
+
295
+ # Final factory polish (safe, no LDQ)
296
+ audio = self._soft_limit(audio)
297
+ fade = int(cfg["global_fade"] * sr)
298
+ if fade > 0 and n_total > fade * 2:
299
+ audio[:fade] *= np.linspace(0, 1, fade)[:, np.newaxis]
300
+ audio[-fade:] *= np.linspace(1, 0, fade)[:, np.newaxis]
301
+
302
+ peak = np.max(np.abs(audio))
303
+ if peak > 0:
304
+ audio = audio / peak * 0.965
305
+
306
+ sf.write(output_path, audio, sr)
307
+ self._log(f"βœ“ FACTORY Saved: {output_path} | Peak: {peak:.3f}")
308
+
309
+ # Stems (factory feature)
310
+ if cfg.get("export_stems"):
311
+ base = output_path.replace(".wav", "")
312
+ for stem, name in [(audio_noise, "noise"), (audio_drone, "drone"),
313
+ (audio_melody, "melody"), (audio_glitch, "glitch")]:
314
+ mv = np.max(np.abs(stem))
315
+ if mv > 0:
316
+ stem = stem / mv * 0.965
317
+ sf.write(f"{base}_{name}.wav", stem, sr)
318
+ self._log(f"βœ“ Stem: {base}_{name}.wav")
319
+
320
+ # MIDI (factory feature)
321
+ if cfg.get("export_midi") and melody_events:
322
+ try:
323
+ from mido import MidiFile, MidiTrack, Message
324
+ mid = MidiFile()
325
+ track = MidiTrack()
326
+ mid.tracks.append(track)
327
+ ticks_per_beat = 480
328
+ tempo = 128
329
+ tick_offset = 0
330
+ for freq, dur, start in melody_events:
331
+ midi_note = max(0, min(127, int(12 * math.log2(max(20, freq) / 440) + 69)))
332
+ duration_ticks = int(dur * ticks_per_beat * (tempo / 60))
333
+ start_ticks = int(start * ticks_per_beat * (tempo / 60))
334
+ track.append(Message('note_on', note=midi_note, velocity=62, time=start_ticks - tick_offset))
335
+ track.append(Message('note_off', note=midi_note, velocity=62, time=duration_ticks))
336
+ tick_offset = start_ticks + duration_ticks
337
+ mid_path = output_path.replace(".wav", ".mid")
338
+ mid.save(mid_path)
339
+ self._log(f"βœ“ MIDI: {mid_path}")
340
+ except Exception as e:
341
+ self._log(f"MIDI export skipped: {e}")
342
+
343
+ def process(self, image_path: str, output_path: str):
344
+ self._log(f"\n╔════════════════════════════════════════════╗")
345
+ self._log(f"β•‘ FACTORY DEFAULT RESONANCE ENGINE v{__version__} β•‘")
346
+ self._log(f"β•‘ Pure image β†’ musical stereo soundscape β•‘")
347
+ self._log(f"β•šβ•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•\n")
348
+ self._log(f"Analyzing: {image_path}")
349
+ features = self.analyze_image(image_path)
350
+ self.synthesize(features, output_path)
351
+
352
+
353
+ def main():
354
+ import argparse
355
+ parser = argparse.ArgumentParser()
356
+ parser.add_argument("image")
357
+ parser.add_argument("-o", "--output", default=None)
358
+ parser.add_argument("--duration", type=float, default=15.0)
359
+ parser.add_argument("--style", choices=list(PRESETS.keys()), default="cinematic")
360
+ parser.add_argument("--seed", type=int, default=None)
361
+ parser.add_argument("--noise-filter", type=float, default=None)
362
+ parser.add_argument("--stems", action="store_true")
363
+ parser.add_argument("--midi", action="store_true")
364
+ parser.add_argument("--quiet", action="store_true")
365
+ args = parser.parse_args()
366
+
367
+ config = {
368
+ "duration": args.duration,
369
+ "random_seed": args.seed,
370
+ "verbose": not args.quiet,
371
+ "export_stems": args.stems,
372
+ "export_midi": args.midi,
373
+ }
374
+ if args.noise_filter is not None:
375
+ config["noise_lowpass_hz"] = args.noise_filter
376
+
377
+ preset = PRESETS.get(args.style, {})
378
+ config.update(preset)
379
+
380
+ out_path = args.output or f"factory_{Path(args.image).stem}.wav"
381
+ engine = FactoryResonanceEngine(config)
382
+ engine.process(args.image, out_path)
383
+
384
+
385
+ if __name__ == "__main__":
386
+ main()