teszenofficial commited on
Commit
aa3df58
·
verified ·
1 Parent(s): 603bef4

Create app.py

Browse files
Files changed (1) hide show
  1. app.py +263 -0
app.py ADDED
@@ -0,0 +1,263 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import gradio as gr
2
+ import librosa
3
+ import numpy as np
4
+ from pydub import AudioSegment, effects
5
+ import uuid, os, json
6
+
7
+ # =====================================================
8
+ # ESTADO DEL MODELO (MISMA CARPETA QUE app.py)
9
+ # =====================================================
10
+
11
+ STATE_PATH = "monx_dj_state.json"
12
+
13
+ UPLOAD_DIR = "uploads"
14
+ OUTPUT_DIR = "outputs"
15
+
16
+ os.makedirs(UPLOAD_DIR, exist_ok=True)
17
+ os.makedirs(OUTPUT_DIR, exist_ok=True)
18
+
19
+ # =====================================================
20
+ # UTILIDAD: CONVERSIÓN SEGURA A JSON
21
+ # =====================================================
22
+
23
+ def to_python(obj):
24
+ if isinstance(obj, dict):
25
+ return {k: to_python(v) for k, v in obj.items()}
26
+ if isinstance(obj, list):
27
+ return [to_python(v) for v in obj]
28
+ if isinstance(obj, np.generic):
29
+ return obj.item()
30
+ return obj
31
+
32
+ # =====================================================
33
+ # ESTADO DEL MODELO (IA REAL)
34
+ # =====================================================
35
+
36
+ DEFAULT_STATE = {
37
+ "weights": {
38
+ "bpm": 0.35,
39
+ "energy": 0.30,
40
+ "smooth": 0.20,
41
+ "drop_penalty": 0.15
42
+ },
43
+ "avg_score": 0.5,
44
+ "runs": 0
45
+ }
46
+
47
+ def load_state():
48
+ if os.path.exists(STATE_PATH):
49
+ try:
50
+ with open(STATE_PATH, "r") as f:
51
+ return json.load(f)
52
+ except:
53
+ pass
54
+ return DEFAULT_STATE.copy()
55
+
56
+ def save_state(state):
57
+ with open(STATE_PATH, "w") as f:
58
+ json.dump(to_python(state), f, indent=2)
59
+
60
+ state = load_state()
61
+ weights = state["weights"]
62
+
63
+ # =====================================================
64
+ # ANÁLISIS MUSICAL (ROBUSTO, SIN DEPENDER DE VERSIONES)
65
+ # =====================================================
66
+
67
+ def analyze_audio(path):
68
+ y, sr = librosa.load(path, mono=True)
69
+
70
+ # ---------- BPM SEGURO ----------
71
+ onset_env = librosa.onset.onset_strength(y=y, sr=sr)
72
+
73
+ try:
74
+ tempo = float(librosa.beat.tempo(
75
+ onset_envelope=onset_env, sr=sr
76
+ )[0])
77
+ except Exception:
78
+ tempo = float(np.mean(onset_env) * 60)
79
+
80
+ if tempo <= 0 or np.isnan(tempo):
81
+ tempo = 120.0 # BPM seguro por defecto
82
+
83
+ # ---------- ENERGÍA ----------
84
+ rms = librosa.feature.rms(y=y)[0]
85
+ rms = librosa.util.normalize(rms)
86
+
87
+ # ---------- DROPS ----------
88
+ drops = [
89
+ i for i in range(10, len(rms) - 10)
90
+ if rms[i] > np.mean(rms[i-10:i-1]) * 1.4
91
+ ]
92
+
93
+ return {
94
+ "tempo": tempo,
95
+ "energy": rms,
96
+ "drops": drops
97
+ }
98
+
99
+ # =====================================================
100
+ # FUNCIÓN DE DECISIÓN (IA)
101
+ # =====================================================
102
+
103
+ def score_transition(a, b, t):
104
+ bpm_sim = max(0, 1 - abs(a["tempo"] - b["tempo"]) / 35)
105
+ energy_sim = max(0, 1 - abs(a["energy"][t] - b["energy"][0]))
106
+ drop_penalty = 1 if t in a["drops"] else 0
107
+
108
+ return (
109
+ weights["bpm"] * bpm_sim +
110
+ weights["energy"] * energy_sim +
111
+ weights["smooth"] * ((bpm_sim + energy_sim) / 2) -
112
+ weights["drop_penalty"] * drop_penalty
113
+ )
114
+
115
+ # =====================================================
116
+ # TIME-STRETCH
117
+ # =====================================================
118
+
119
+ def bpm_adjust(segment, from_bpm, to_bpm):
120
+ if from_bpm <= 0 or to_bpm <= 0:
121
+ return segment
122
+
123
+ ratio = from_bpm / to_bpm
124
+ if abs(1 - ratio) > 0.20:
125
+ return segment
126
+
127
+ new_rate = int(segment.frame_rate * ratio)
128
+ return segment._spawn(
129
+ segment.raw_data,
130
+ overrides={"frame_rate": new_rate}
131
+ ).set_frame_rate(segment.frame_rate)
132
+
133
+ # =====================================================
134
+ # IA DJ MIXER
135
+ # =====================================================
136
+
137
+ def auto_dj_mix(files, durations, crossfade_sec):
138
+ log = "🎧 Iniciando MONX DJ (IA)\n"
139
+ yield log, None
140
+
141
+ durs = [float(x.strip()) for x in durations.split(",")]
142
+ crossfade_ms = int(crossfade_sec * 1000)
143
+
144
+ tracks, analyses, scores = [], [], []
145
+
146
+ for i, f in enumerate(files):
147
+ log += f"\n🔍 Analizando canción {i+1}\n"
148
+ yield log, None
149
+
150
+ ext = f.name.split(".")[-1]
151
+ path = os.path.join(UPLOAD_DIR, f"{uuid.uuid4().hex}.{ext}")
152
+
153
+ with open(f.name, "rb") as src, open(path, "wb") as dst:
154
+ dst.write(src.read())
155
+
156
+ audio = effects.normalize(AudioSegment.from_file(path))
157
+ tracks.append(audio)
158
+ analyses.append(analyze_audio(path))
159
+
160
+ mix = AudioSegment.silent(0)
161
+
162
+ for i in range(len(tracks)):
163
+ play_ms = min(int(durs[i] * 1000), len(tracks[i]))
164
+ segment = tracks[i][:play_ms]
165
+
166
+ if i == 0:
167
+ mix = segment
168
+ continue
169
+
170
+ prev, curr = analyses[i - 1], analyses[i]
171
+
172
+ candidates = range(5, min(len(prev["energy"]) - 1, 50))
173
+ best_t = max(candidates, key=lambda t: score_transition(prev, curr, t))
174
+ scores.append(score_transition(prev, curr, best_t))
175
+
176
+ target_bpm = curr["tempo"]
177
+
178
+ mix_adj = bpm_adjust(mix, prev["tempo"], target_bpm)
179
+ seg_adj = bpm_adjust(segment, curr["tempo"], target_bpm)
180
+
181
+ safe_cf = min(crossfade_ms, len(mix_adj), len(seg_adj))
182
+ mix = bpm_adjust(
183
+ mix_adj.append(seg_adj, crossfade=safe_cf),
184
+ target_bpm, curr["tempo"]
185
+ )
186
+
187
+ out_path = os.path.join(
188
+ OUTPUT_DIR, f"monx_dj_mix_{uuid.uuid4().hex}.m4a"
189
+ )
190
+ mix.export(out_path, format="ipod", codec="aac", bitrate="192k")
191
+
192
+ # ---------- APRENDIZAJE AUTOMÁTICO ----------
193
+ if scores:
194
+ new_avg = sum(scores) / len(scores)
195
+ reward = 1 if new_avg > state["avg_score"] else -1
196
+ lr = 0.05
197
+
198
+ for k in weights:
199
+ weights[k] += reward * lr * abs(weights[k])
200
+
201
+ total = sum(abs(v) for v in weights.values())
202
+ for k in weights:
203
+ weights[k] /= total
204
+
205
+ state["avg_score"] = new_avg
206
+ state["runs"] += 1
207
+ state["weights"] = weights
208
+ save_state(state)
209
+
210
+ log += "\n✅ Mix listo. Da feedback para que MONX DJ aprenda."
211
+ yield log, out_path
212
+
213
+ # =====================================================
214
+ # FEEDBACK HUMANO
215
+ # =====================================================
216
+
217
+ def feedback(reward):
218
+ lr = 0.08
219
+ for k in weights:
220
+ weights[k] += reward * lr * abs(weights[k])
221
+
222
+ total = sum(abs(v) for v in weights.values())
223
+ for k in weights:
224
+ weights[k] /= total
225
+
226
+ state["weights"] = weights
227
+ save_state(state)
228
+ return "🧠 Feedback recibido. MONX DJ ha aprendido."
229
+
230
+ # =====================================================
231
+ # UI
232
+ # =====================================================
233
+
234
+ with gr.Blocks(title="MONX DJ") as demo:
235
+ gr.Markdown(
236
+ "<h1 style='text-align:center'>🎚️ MONX DJ</h1>"
237
+ "<p style='text-align:center'>IA DJ con aprendizaje real</p>"
238
+ )
239
+
240
+ files = gr.File(
241
+ label="Sube 2 a 4 canciones",
242
+ file_count="multiple",
243
+ file_types=[".mp3", ".wav", ".flac", ".m4a"]
244
+ )
245
+
246
+ durations = gr.Textbox(label="Duración por canción (seg)", value="90,90")
247
+ crossfade = gr.Slider(6, 20, value=12, step=1, label="Crossfade (seg)")
248
+
249
+ btn = gr.Button("🔥 Auto Mix")
250
+ status = gr.Markdown()
251
+ output = gr.Audio(type="filepath")
252
+
253
+ btn.click(auto_dj_mix, [files, durations, crossfade], [status, output])
254
+
255
+ gr.Markdown("### ¿Te gustó el mix?")
256
+ like = gr.Button("👍 Sí")
257
+ dislike = gr.Button("👎 No")
258
+ fb_status = gr.Markdown()
259
+
260
+ like.click(lambda: feedback(1), None, fb_status)
261
+ dislike.click(lambda: feedback(-1), None, fb_status)
262
+
263
+ demo.launch()