rlackey commited on
Commit
a3ed5bc
·
1 Parent(s): a3fa32f

Fix version mismatch - sdk 4.44.0, huggingface_hub 0.21.4

Browse files
Files changed (3) hide show
  1. README.md +9 -27
  2. app.py +528 -868
  3. requirements.txt +2 -3
README.md CHANGED
@@ -1,36 +1,18 @@
1
  ---
2
- title: VYNL Complete
3
- emoji: '🎸'
4
- colorFrom: orange
5
- colorTo: amber
6
  sdk: gradio
7
  sdk_version: 4.44.0
8
  app_file: app.py
9
- pinned: true
10
  license: mit
 
 
11
  ---
12
 
13
- # VYNL - Complete Music Analysis
14
 
15
- **From raw demo to DAW-ready - in one click**
16
 
17
- Created by R.T. Lackey | Stone and Lantern Music Group
18
-
19
- ## Features
20
-
21
- - **PROCESS**: Single song stem separation, chord detection, DAW export
22
- - **BULK**: Batch processing for YouTube playlists and local files
23
- - **GROOVES**: AI music generation
24
- - **SESSIONS**: Live mixer, setlist creator, karaoke teleprompter
25
- - **TRAINING**: Model training pipeline (Creator only)
26
-
27
- ## License Required
28
-
29
- Enter your VYNL license key to access all features.
30
-
31
- ## Desktop App
32
-
33
- For unlimited offline processing, get the Desktop App:
34
- - Windows, Mac, Linux supported
35
- - $79 lifetime license
36
- - Contact: rlackey.seattle@gmail.com
 
1
  ---
2
+ title: VYNL Backend
3
+ emoji: '🔧'
4
+ colorFrom: gray
5
+ colorTo: gray
6
  sdk: gradio
7
  sdk_version: 4.44.0
8
  app_file: app.py
9
+ pinned: false
10
  license: mit
11
+ hardware: zero-a10g
12
+ private: true
13
  ---
14
 
15
+ # VYNL Backend (Private)
16
 
17
+ Backend processing server for VYNL public demo.
18
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
app.py CHANGED
@@ -20,6 +20,20 @@ from pathlib import Path
20
  from datetime import datetime
21
  import subprocess
22
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
23
  # Import token system
24
  from token_system import (
25
  user_manager, check_can_process, deduct_token,
@@ -44,550 +58,265 @@ except ImportError:
44
  HAS_YTDLP = False
45
 
46
  # ============================================================================
47
- # RAINBOW VINYL + 1970s STUDIO CSS
48
  # ============================================================================
49
 
50
  RAINBOW_CSS = """
51
- /* ============================================
52
- VYNL RAINBOW VINYL + VINTAGE STUDIO THEME
53
- "1970s studio visited by a rainbow alien"
54
- ============================================ */
55
-
56
  :root {
57
- /* Base - Studio Hardware */
58
  --deep-black: #1A1A1A;
59
  --studio-panel: #2E2520;
60
  --warm-walnut: #4A3728;
61
  --cream-text: #F5E6D3;
62
-
63
- /* Rainbow Vinyl Energy */
64
  --neon-cyan: #00F5FF;
65
  --neon-magenta: #FF00FF;
66
- --electric-yellow: #FFFF00;
67
  --neon-green: #7CFF00;
68
- --coral-orange: #FF6B4A;
69
- --glowing-ring: #FF8C42;
70
-
71
- /* Gradients */
72
- --rainbow-gradient: linear-gradient(45deg, #00F5FF, #FF00FF, #FFFF00, #7CFF00);
73
- --rainbow-horizontal: linear-gradient(90deg, #00F5FF, #FF00FF, #FFFF00);
74
- --coral-gradient: linear-gradient(135deg, #FF6B4A, #FFB74D);
75
  }
76
 
77
- /* Main container - Dark studio with particles */
78
  .gradio-container {
79
- background:
80
- radial-gradient(circle at 20% 30%, rgba(0,245,255,0.08), transparent 40%),
81
- radial-gradient(circle at 80% 70%, rgba(255,0,255,0.08), transparent 40%),
82
- radial-gradient(circle at 50% 50%, rgba(255,255,0,0.05), transparent 50%),
83
- linear-gradient(180deg, #1A1A1A 0%, #2E2520 50%, #1A1A1A 100%) !important;
84
- min-height: 100vh;
85
- font-family: 'Segoe UI', 'Helvetica Neue', sans-serif !important;
86
  }
87
 
88
- /* Floating particles animation */
89
- @keyframes float-particles {
90
- 0%, 100% { transform: translateY(0) rotate(0deg); opacity: 0.6; }
91
- 50% { transform: translateY(-20px) rotate(180deg); opacity: 1; }
92
- }
93
-
94
- /* Header with rainbow vinyl */
95
  .main-header {
96
- background: linear-gradient(180deg, rgba(46,37,32,0.95) 0%, rgba(26,26,26,0.98) 100%);
97
- border-bottom: 3px solid transparent;
98
  border-image: linear-gradient(90deg, #00F5FF, #FF00FF, #FFFF00, #7CFF00) 1;
99
- padding: 30px;
100
  text-align: center;
101
- position: relative;
102
- overflow: hidden;
103
- }
104
-
105
- .main-header::before {
106
- content: '';
107
- position: absolute;
108
- top: 0;
109
- left: 0;
110
- right: 0;
111
- bottom: 0;
112
- background:
113
- radial-gradient(circle at 30% 50%, rgba(0,245,255,0.1), transparent),
114
- radial-gradient(circle at 70% 50%, rgba(255,0,255,0.1), transparent);
115
- pointer-events: none;
116
- }
117
-
118
- /* Rainbow vinyl logo animation */
119
- .vinyl-logo {
120
- width: 120px;
121
- height: 120px;
122
- border-radius: 50%;
123
- background: conic-gradient(from 0deg, #00F5FF, #FF00FF, #FFFF00, #7CFF00, #00F5FF);
124
- animation: spin-vinyl 8s linear infinite;
125
- box-shadow:
126
- 0 0 30px rgba(0,245,255,0.5),
127
- 0 0 60px rgba(255,0,255,0.3),
128
- inset 0 0 30px rgba(0,0,0,0.5);
129
- margin: 0 auto 20px;
130
- position: relative;
131
- }
132
-
133
- .vinyl-logo::after {
134
- content: '';
135
- position: absolute;
136
- top: 50%;
137
- left: 50%;
138
- width: 30px;
139
- height: 30px;
140
- background: #1A1A1A;
141
- border-radius: 50%;
142
- transform: translate(-50%, -50%);
143
- box-shadow: inset 0 0 10px rgba(255,140,66,0.5);
144
- }
145
-
146
- @keyframes spin-vinyl {
147
- from { transform: rotate(0deg); }
148
- to { transform: rotate(360deg); }
149
  }
150
 
151
  .logo-text {
152
  font-family: 'Courier New', monospace;
153
- font-size: 4em;
154
  font-weight: 900;
155
  background: linear-gradient(90deg, #00F5FF, #FF00FF, #FFFF00, #7CFF00);
156
  -webkit-background-clip: text;
157
  -webkit-text-fill-color: transparent;
158
- text-shadow: none;
159
- letter-spacing: 0.3em;
160
  margin: 0;
161
- filter: drop-shadow(0 0 20px rgba(0,245,255,0.5));
162
  }
163
 
164
  .tagline {
165
  color: var(--cream-text);
166
- font-size: 1.1em;
167
- letter-spacing: 0.2em;
168
- margin-top: 10px;
169
- opacity: 0.8;
170
  }
171
 
172
- /* User status bar */
173
  .status-bar {
174
- background: linear-gradient(90deg, rgba(0,245,255,0.1), rgba(255,0,255,0.1), rgba(255,255,0,0.1));
175
  border: 1px solid rgba(0,245,255,0.3);
176
- border-radius: 25px;
177
- padding: 10px 25px;
178
- margin: 20px auto;
179
- max-width: 600px;
180
- text-align: center;
181
- }
182
-
183
- .status-bar .tokens {
184
- color: var(--neon-cyan);
185
- font-weight: bold;
186
- font-size: 1.1em;
187
- }
188
-
189
- .status-bar .demo-badge {
190
- background: var(--coral-gradient);
191
- color: white;
192
- padding: 3px 12px;
193
- border-radius: 15px;
194
- font-size: 0.85em;
195
- margin-left: 10px;
196
- }
197
-
198
- .status-bar .licensed-badge {
199
- background: linear-gradient(90deg, var(--neon-green), var(--neon-cyan));
200
- color: #1A1A1A;
201
- padding: 3px 12px;
202
- border-radius: 15px;
203
- font-size: 0.85em;
204
- margin-left: 10px;
205
- font-weight: bold;
206
- }
207
-
208
- /* Login/Register panel */
209
- .auth-panel {
210
- background: rgba(46,37,32,0.8);
211
- border: 2px solid var(--warm-walnut);
212
- border-radius: 15px;
213
- padding: 25px;
214
- margin: 20px auto;
215
  max-width: 500px;
216
- box-shadow: 0 0 30px rgba(0,0,0,0.5);
217
- }
218
-
219
- /* Tab styling - Rainbow accent */
220
- .tab-nav {
221
- background: var(--studio-panel) !important;
222
- border: none !important;
223
- border-radius: 15px 15px 0 0 !important;
224
- padding: 10px !important;
225
- border-bottom: 2px solid var(--warm-walnut) !important;
226
  }
227
 
 
228
  .tab-nav button {
229
- background: transparent !important;
230
- border: 2px solid transparent !important;
231
- border-radius: 10px !important;
232
  color: var(--cream-text) !important;
233
- font-weight: 600 !important;
234
- letter-spacing: 0.1em !important;
235
- padding: 12px 24px !important;
236
- margin: 3px !important;
237
- transition: all 0.3s ease !important;
238
  }
239
-
240
- .tab-nav button:hover {
241
- background: rgba(0,245,255,0.1) !important;
242
- border-color: var(--neon-cyan) !important;
243
- }
244
-
245
  .tab-nav button.selected {
246
- background: linear-gradient(135deg, rgba(0,245,255,0.2), rgba(255,0,255,0.2)) !important;
247
- border-color: var(--neon-magenta) !important;
248
- box-shadow: 0 0 20px rgba(255,0,255,0.3) !important;
249
- color: white !important;
250
- }
251
-
252
- /* Panel styling - Wood with rainbow accents */
253
- .panel, .gr-panel, .gr-box {
254
- background: linear-gradient(135deg, #2E2520 0%, #3D3028 50%, #2E2520 100%) !important;
255
- border: 2px solid var(--warm-walnut) !important;
256
- border-radius: 12px !important;
257
- box-shadow:
258
- inset 0 1px 0 rgba(255,255,255,0.05),
259
- 0 4px 20px rgba(0,0,0,0.4) !important;
260
- padding: 20px !important;
261
- margin: 10px 0 !important;
262
  }
263
 
264
- /* Input fields - Dark with neon focus */
265
- input[type="text"], input[type="email"], input[type="password"], textarea, select {
266
- background: #1A1A1A !important;
267
- border: 2px solid var(--warm-walnut) !important;
268
  color: var(--cream-text) !important;
269
- border-radius: 8px !important;
270
- padding: 12px !important;
271
- transition: all 0.3s ease !important;
272
  }
273
-
274
- input:focus, textarea:focus, select:focus {
275
  border-color: var(--neon-cyan) !important;
276
- box-shadow: 0 0 15px rgba(0,245,255,0.3) !important;
277
- outline: none !important;
278
- }
279
-
280
- /* Labels - Cream with subtle glow */
281
- label, .label-wrap {
282
- color: var(--cream-text) !important;
283
- font-weight: 600 !important;
284
- letter-spacing: 0.05em !important;
285
  }
286
 
287
- /* Primary buttons - Coral orange gradient */
288
- button.primary, .primary-btn {
289
- background: var(--coral-gradient) !important;
290
  border: none !important;
291
- color: white !important;
292
- font-weight: bold !important;
293
- font-size: 1.1em !important;
294
- letter-spacing: 0.1em !important;
295
- padding: 15px 35px !important;
296
- border-radius: 25px !important;
297
- box-shadow: 0 0 25px rgba(255,107,74,0.5) !important;
298
- transition: all 0.3s ease !important;
299
- cursor: pointer !important;
300
- }
301
-
302
- button.primary:hover, .primary-btn:hover {
303
- box-shadow: 0 0 40px rgba(255,107,74,0.8) !important;
304
- transform: translateY(-2px) !important;
305
- }
306
-
307
- button.primary:active {
308
- transform: translateY(0) !important;
309
- }
310
-
311
- /* Secondary buttons */
312
- button.secondary, .secondary-btn {
313
- background: rgba(74,55,40,0.8) !important;
314
- border: 2px solid var(--warm-walnut) !important;
315
- color: var(--cream-text) !important;
316
- border-radius: 20px !important;
317
- padding: 10px 25px !important;
318
- }
319
-
320
- button.secondary:hover {
321
- border-color: var(--neon-cyan) !important;
322
- box-shadow: 0 0 15px rgba(0,245,255,0.3) !important;
323
- }
324
-
325
- /* Rainbow progress bar */
326
- .rainbow-progress {
327
- background: var(--studio-panel);
328
- border-radius: 20px;
329
- overflow: hidden;
330
- height: 35px;
331
- box-shadow: inset 0 2px 5px rgba(0,0,0,0.3);
332
- margin: 15px 0;
333
- }
334
-
335
- .rainbow-progress .bar {
336
- height: 100%;
337
- background: linear-gradient(90deg, #00F5FF, #FF00FF, #FFFF00);
338
- border-radius: 20px;
339
- transition: width 0.3s ease;
340
- box-shadow: 0 0 20px rgba(255,0,255,0.5);
341
- }
342
-
343
- /* VU Meter styling */
344
- .vu-meter {
345
- background: linear-gradient(90deg,
346
- var(--neon-green) 0%,
347
- var(--neon-green) 60%,
348
- var(--electric-yellow) 60%,
349
- var(--electric-yellow) 80%,
350
- var(--neon-magenta) 80%,
351
- var(--neon-magenta) 100%
352
- );
353
- height: 12px;
354
- border-radius: 6px;
355
- box-shadow: 0 0 10px rgba(124,255,0,0.5);
356
- }
357
-
358
- /* Neon LED indicators */
359
- .neon-led {
360
- display: inline-block;
361
- width: 12px;
362
- height: 12px;
363
- border-radius: 50%;
364
- margin-right: 8px;
365
- }
366
-
367
- .neon-led.cyan {
368
- background: var(--neon-cyan);
369
- box-shadow: 0 0 10px var(--neon-cyan);
370
- }
371
- .neon-led.magenta {
372
- background: var(--neon-magenta);
373
- box-shadow: 0 0 10px var(--neon-magenta);
374
- }
375
- .neon-led.green {
376
- background: var(--neon-green);
377
- box-shadow: 0 0 10px var(--neon-green);
378
- }
379
- .neon-led.yellow {
380
- background: var(--electric-yellow);
381
- box-shadow: 0 0 10px var(--electric-yellow);
382
- }
383
- .neon-led.gray {
384
- background: #555;
385
- box-shadow: none;
386
- }
387
-
388
- /* Audio player */
389
- audio {
390
- border-radius: 10px !important;
391
- background: var(--deep-black) !important;
392
  }
393
-
394
- /* File upload - Dark with rainbow border on hover */
395
- .upload-zone {
396
- background: var(--deep-black) !important;
397
- border: 2px dashed var(--warm-walnut) !important;
398
- border-radius: 12px !important;
399
- transition: all 0.3s ease !important;
400
  }
401
 
402
- .upload-zone:hover {
403
- border-color: var(--neon-cyan) !important;
404
- box-shadow: 0 0 20px rgba(0,245,255,0.2) !important;
 
 
405
  }
406
 
407
- /* Slider with rainbow accent */
408
- input[type="range"] {
409
- accent-color: var(--neon-magenta) !important;
410
- }
411
 
412
  /* Mixer faders */
413
- .fader-channel {
414
- background: linear-gradient(180deg, var(--warm-walnut), var(--studio-panel));
415
- border-radius: 8px;
416
- padding: 15px 10px;
417
- text-align: center;
418
- border: 1px solid rgba(0,245,255,0.2);
419
  }
420
-
421
- .fader-label {
422
- color: var(--cream-text);
423
- font-size: 0.85em;
424
- font-weight: bold;
425
- margin-top: 10px;
426
- }
427
-
428
- /* Chord display */
429
- .chord-box {
430
- background: var(--studio-panel);
431
- border: 2px solid var(--warm-walnut);
432
- border-radius: 10px;
433
- padding: 15px 20px;
434
- margin: 5px;
435
- text-align: center;
436
- transition: all 0.3s ease;
437
- }
438
-
439
- .chord-box.active {
440
- border-color: var(--neon-cyan);
441
- box-shadow: 0 0 20px rgba(0,245,255,0.5);
442
- background: rgba(0,245,255,0.1);
443
- }
444
-
445
- .chord-name {
446
- font-size: 1.8em;
447
- font-weight: bold;
448
- color: var(--cream-text);
449
  }
 
 
450
 
451
  /* Teleprompter */
452
- .teleprompter {
453
- background: #0A0A0A;
454
- border: 3px solid var(--neon-magenta);
455
- border-radius: 15px;
456
- padding: 40px;
457
- min-height: 400px;
458
- box-shadow: 0 0 30px rgba(255,0,255,0.3);
459
- }
460
-
461
- .teleprompter-line {
462
- font-family: 'Courier New', monospace;
463
- font-size: 1.6em;
464
- line-height: 2;
465
- color: var(--cream-text);
466
- transition: all 0.3s ease;
467
- }
468
-
469
- .teleprompter-line.current {
470
- font-size: 2.2em;
471
- color: var(--neon-cyan);
472
- text-shadow: 0 0 20px rgba(0,245,255,0.8);
473
- }
474
-
475
- .teleprompter-chord {
476
- color: var(--neon-magenta);
477
- font-weight: bold;
478
- margin-right: 15px;
479
- }
480
-
481
- /* Desktop download cards */
482
- .download-card {
483
- background: linear-gradient(180deg, var(--warm-walnut), var(--studio-panel));
484
- border: 2px solid var(--warm-walnut);
485
- border-radius: 15px;
486
- padding: 30px;
487
- text-align: center;
488
- transition: all 0.3s ease;
489
- }
490
-
491
- .download-card:hover {
492
- border-color: var(--neon-cyan);
493
- box-shadow: 0 0 25px rgba(0,245,255,0.3);
494
- transform: translateY(-5px);
495
- }
496
-
497
- .download-card h3 {
498
- color: var(--cream-text);
499
- margin: 15px 0;
500
- }
501
-
502
- .download-card .icon {
503
- font-size: 3em;
504
- }
505
-
506
- .download-btn {
507
- display: inline-block;
508
- background: var(--coral-gradient);
509
- color: white;
510
- padding: 12px 25px;
511
- border-radius: 20px;
512
- text-decoration: none;
513
- font-weight: bold;
514
- margin-top: 15px;
515
- border: none;
516
- cursor: pointer;
517
  }
 
 
 
518
 
519
  /* Footer */
520
- .footer {
521
- text-align: center;
522
- padding: 30px;
523
- margin-top: 40px;
524
- border-top: 2px solid var(--warm-walnut);
525
- color: var(--cream-text);
526
- opacity: 0.8;
527
- }
528
 
529
- .footer a {
530
- color: var(--neon-cyan);
531
- text-decoration: none;
532
- }
533
 
534
- /* Scrollbar */
535
- ::-webkit-scrollbar {
536
- width: 10px;
537
- height: 10px;
538
- }
539
- ::-webkit-scrollbar-track {
540
- background: var(--deep-black);
541
- }
542
- ::-webkit-scrollbar-thumb {
543
- background: linear-gradient(180deg, var(--neon-cyan), var(--neon-magenta));
544
- border-radius: 5px;
545
- }
546
 
547
- /* Animations */
548
- @keyframes pulse-glow {
549
- 0%, 100% { box-shadow: 0 0 20px rgba(255,107,74,0.5); }
550
- 50% { box-shadow: 0 0 40px rgba(255,107,74,0.8); }
551
- }
552
 
553
- .processing .vinyl-logo {
554
- animation: spin-vinyl 2s linear infinite;
555
- }
 
556
 
557
- /* Responsive */
558
- @media (max-width: 768px) {
559
- .logo-text { font-size: 2.5em; }
560
- .vinyl-logo { width: 80px; height: 80px; }
561
- .tab-nav button { padding: 8px 12px !important; font-size: 0.85em !important; }
562
- }
563
- """
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
564
 
565
  # ============================================================================
566
- # PROCESSING FUNCTIONS WITH TOKEN SYSTEM
567
  # ============================================================================
568
 
569
  def get_audio_duration(audio_path):
570
  """Get audio duration in seconds"""
571
  if HAS_LIBROSA:
572
  try:
573
- y, sr = librosa.load(audio_path, sr=None, duration=1)
574
  return librosa.get_duration(path=audio_path)
575
  except:
576
  pass
577
- return 180 # Default estimate
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
578
 
579
- def process_song(audio_file, song_name, do_stems, do_chords, do_daw, user_email, progress=gr.Progress()):
580
- """Process a single song with token deduction"""
 
 
 
 
 
 
581
 
582
  if not audio_file:
583
- return "Please upload an audio file", "", None, get_status_display(user_email)
584
 
585
  # Check duration limit
586
  duration = get_audio_duration(audio_file)
587
  can_process, msg, status = check_can_process(user_email, duration)
588
 
589
  if not can_process:
590
- return msg, "", None, get_status_display(user_email)
591
 
592
  if not song_name:
593
  song_name = Path(audio_file).stem
@@ -595,80 +324,81 @@ def process_song(audio_file, song_name, do_stems, do_chords, do_daw, user_email,
595
  # Deduct token
596
  ok, token_msg = deduct_token(user_email)
597
  if not ok:
598
- return token_msg, "", None, get_status_display(user_email)
599
-
600
- status_log = []
601
- status_log.append(f"VYNL Processing: {song_name}")
602
- status_log.append(f"Duration: {duration:.1f}s")
603
- status_log.append(token_msg)
604
- status_log.append("=" * 50)
605
 
 
606
  output_files = []
607
  temp_dir = tempfile.mkdtemp(prefix="vynl_")
608
  output_path = Path(temp_dir)
609
 
610
  try:
 
 
 
 
 
611
  # Stem separation
612
  if do_stems:
613
- progress(0.2, desc="Separating stems...")
614
- status_log.append("\n[STEM SEPARATION]")
615
-
616
  try:
617
  stems_dir = output_path / "stems"
618
  stems_dir.mkdir(exist_ok=True)
619
-
620
- cmd = ['demucs', '--two-stems=vocals', '-o', str(stems_dir), '--mp3', '--mp3-bitrate=320', audio_file]
621
  result = subprocess.run(cmd, capture_output=True, text=True, timeout=600)
622
-
623
  if result.returncode == 0:
624
  for stem in stems_dir.rglob("*.mp3"):
625
  output_files.append(str(stem))
626
- status_log.append(f" Created: {stem.name}")
627
  else:
628
- status_log.append(f" Note: Demucs not available")
629
  except Exception as e:
630
- status_log.append(f" Note: {str(e)[:50]}")
631
 
632
- # Chord detection
633
  if do_chords:
634
- progress(0.5, desc="Detecting chords...")
635
- status_log.append("\n[CHORD DETECTION]")
636
 
637
- if HAS_LIBROSA:
638
- try:
639
- y, sr = librosa.load(audio_file, sr=22050)
640
- tempo, _ = librosa.beat.beat_track(y=y, sr=sr)
641
- if hasattr(tempo, '__iter__'):
642
- tempo = float(tempo[0])
643
 
644
- chroma = librosa.feature.chroma_cqt(y=y, sr=sr)
645
- notes = ['C', 'C#', 'D', 'D#', 'E', 'F', 'F#', 'G', 'G#', 'A', 'A#', 'B']
646
- key_idx = int(np.argmax(np.mean(chroma, axis=1)))
647
- detected_key = notes[key_idx]
648
 
649
- status_log.append(f" Tempo: {tempo:.0f} BPM")
650
- status_log.append(f" Key: {detected_key} Major")
651
 
652
- chart = f"# {song_name}\nTempo: {tempo:.0f} BPM\nKey: {detected_key}\n\n[Generated by VYNL]"
653
- chart_file = output_path / f"{song_name}_CHORDS.txt"
654
- chart_file.write_text(chart)
655
- output_files.append(str(chart_file))
656
-
657
- except Exception as e:
658
- status_log.append(f" Note: {str(e)[:50]}")
659
 
660
  # DAW project
661
  if do_daw:
662
- progress(0.7, desc="Creating DAW project...")
663
- status_log.append("\n[DAW PROJECT]")
664
-
665
- rpp = f'<REAPER_PROJECT 0.1 "6.0">\n TEMPO 120 4 4\n <TRACK>\n NAME "{song_name}"\n >\n>'
666
  rpp_file = output_path / f"{song_name}.RPP"
667
  rpp_file.write_text(rpp)
668
  output_files.append(str(rpp_file))
669
- status_log.append(f" Created: {rpp_file.name}")
 
 
 
 
 
 
 
 
 
670
 
671
- progress(0.9, desc="Packaging...")
672
 
673
  # Create zip
674
  if output_files:
@@ -677,519 +407,449 @@ def process_song(audio_file, song_name, do_stems, do_chords, do_daw, user_email,
677
  for f in output_files:
678
  zf.write(f, Path(f).name)
679
 
680
- status_log.append(f"\n[COMPLETE]")
681
- status_log.append(f"Package: {zip_path.name}")
682
-
683
  progress(1.0, desc="Done!")
684
- return "\n".join(status_log), "\n".join(status_log), str(zip_path), get_status_display(user_email)
685
 
686
  except Exception as e:
687
- return f"Error: {str(e)}", "", None, get_status_display(user_email)
688
 
689
- return "\n".join(status_log), "", None, get_status_display(user_email)
690
 
 
 
 
691
 
692
  def register_user(email, password, name):
693
  """Register new user account"""
 
 
694
  ok, msg = user_manager.create_account(email, password, name)
695
  if ok:
696
- return f"Account created! You have 3 free demo tokens.", get_status_display(email)
697
- return msg, ""
698
-
699
 
700
  def login_user(email, password):
701
  """Login user"""
 
 
702
  ok, user = user_manager.login(email, password)
703
  if ok:
704
  return f"Welcome back, {user['name']}!", get_status_display(email), email
705
  return "Invalid email or password", "", ""
706
 
707
-
708
  def activate_license(email, license_key):
709
  """Activate license for user"""
710
  if not email:
711
- return "Please login first", ""
 
 
712
  ok, msg = user_manager.activate_license(email, license_key)
713
  return msg, get_status_display(email)
714
 
715
-
716
- def master_track(input_audio, reference_audio, target_lufs, preset, user_email, progress=gr.Progress()):
717
- """Master a track with AI"""
718
-
719
- if not input_audio:
720
- return None, "Please upload an audio file to master"
721
-
722
- # Check tokens
723
- can_process, msg, status = check_can_process(user_email, 0)
724
- if not can_process:
725
- return None, msg
726
-
727
- # Deduct token
728
- ok, token_msg = deduct_token(user_email)
729
- if not ok:
730
- return None, token_msg
731
-
732
- progress(0.1, desc="Analyzing input...")
733
-
734
- try:
735
- progress(0.3, desc=f"Applying {preset} preset...")
736
-
737
- # Run mastering
738
- output_path, analysis = master_audio(
739
- input_path=input_audio,
740
- output_path=None,
741
- preset=preset,
742
- reference_path=reference_audio if preset == "Reference Match" else None,
743
- target_lufs=target_lufs
744
- )
745
-
746
- progress(0.9, desc="Finalizing...")
747
-
748
- if output_path:
749
- progress(1.0, desc="Complete!")
750
- result_text = format_analysis(analysis)
751
- result_text += f"\n\n{token_msg}"
752
- return output_path, result_text
753
- else:
754
- return None, f"Mastering failed: {analysis.get('error', 'Unknown error')}"
755
-
756
- except Exception as e:
757
- return None, f"Error: {str(e)}"
758
-
759
 
760
  def process_catalog(playlist_url, files, do_stems, do_chords, user_email, progress=gr.Progress()):
761
  """Batch process from YouTube playlist or files"""
762
 
763
- status = []
764
- status.append("CATALOG PROCESSOR")
765
- status.append("=" * 50)
766
-
767
  songs = []
768
 
769
- # Get songs from YouTube playlist
770
  if playlist_url and HAS_YTDLP:
771
  progress(0.1, desc="Fetching playlist...")
772
- status.append(f"\nFetching: {playlist_url}")
773
-
774
- try:
775
- import yt_dlp
776
- ydl_opts = {'quiet': True, 'extract_flat': True}
777
- with yt_dlp.YoutubeDL(ydl_opts) as ydl:
778
- info = ydl.extract_info(playlist_url, download=False)
779
- if 'entries' in info:
780
- for entry in info['entries'][:10]: # Limit to 10
781
- if entry:
782
- songs.append({
783
- 'title': entry.get('title', 'Unknown'),
784
- 'url': entry.get('url'),
785
- 'source': 'youtube'
786
- })
787
- status.append(f" Found: {entry.get('title', 'Unknown')[:40]}")
788
- except Exception as e:
789
- status.append(f" Error: {str(e)[:50]}")
790
 
791
  # Add uploaded files
792
  if files:
793
  for f in files:
794
- songs.append({
795
- 'title': Path(f).stem,
796
- 'path': f,
797
- 'source': 'local'
798
- })
799
- status.append(f" Added: {Path(f).stem}")
800
 
801
- status.append(f"\nTotal: {len(songs)} songs to process")
802
 
803
- # Process each
804
  for i, song in enumerate(songs):
805
- progress((i + 1) / max(len(songs), 1), desc=f"Processing {song['title'][:20]}...")
806
- status.append(f"\n[{i+1}/{len(songs)}] {song['title']}")
807
 
808
  # Check tokens
809
  can_process, msg, _ = check_can_process(user_email, 0)
810
  if not can_process:
811
- status.append(f" Stopped: {msg}")
812
  break
813
 
814
  deduct_token(user_email)
815
- status.append(" Token used")
816
- status.append(" Processing complete")
817
 
818
- status.append("\n" + "=" * 50)
819
- status.append("CATALOG PROCESSING COMPLETE")
 
 
 
 
 
 
820
 
821
- return "\n".join(status)
822
 
 
 
823
 
824
- def generate_groove(prompt, key, bpm, duration, user_email, progress=gr.Progress()):
825
- """Generate music with GROOVES"""
 
 
 
 
 
826
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
827
  if not prompt:
828
- return None, "Please enter a style prompt"
829
 
830
- # Check tokens (AI generation costs 2)
831
- can_process, msg, status = check_can_process(user_email, 0)
832
  if not can_process:
833
  return None, msg
834
 
835
- # Deduct 2 tokens for AI generation
836
- deduct_token(user_email)
837
  deduct_token(user_email)
 
838
 
839
- progress(0.2, desc="Loading AI model...")
840
-
841
- # Note: Full MusicGen requires GPU
842
- # This is a placeholder for the HF Space
843
- progress(0.5, desc="Generating audio...")
844
- time.sleep(2)
845
-
846
- progress(1.0, desc="Complete!")
847
 
 
848
  return None, f"""GROOVES - AI Music Generation
849
 
850
  Prompt: {prompt}
851
- Key: {key}
852
- BPM: {bpm}
853
- Duration: {duration}s
854
 
855
- Note: Full AI generation requires GPU hardware.
856
- Deploy to HuggingFace Spaces with GPU for MusicGen.
857
-
858
- 2 tokens used for this generation request.
859
  {get_status_display(user_email)}"""
860
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
861
 
862
  # ============================================================================
863
  # BUILD INTERFACE
864
  # ============================================================================
865
 
866
- with gr.Blocks(title="VYNL - Music Production Suite") as demo:
867
 
868
  # State
869
  current_user = gr.State("")
870
 
871
- # Header with rainbow vinyl
872
  gr.HTML("""
873
  <div class="main-header">
874
- <div class="vinyl-logo"></div>
875
  <h1 class="logo-text">VYNL</h1>
876
- <p class="tagline">From raw demo to DAW-ready - in one click</p>
877
  </div>
878
  """)
879
 
880
  # Status bar
881
- with gr.Row():
882
- status_display = gr.HTML(
883
- '<div class="status-bar"><span class="tokens">DEMO MODE</span> - 3 free tokens | 5-min track limit<span class="demo-badge">DEMO</span></div>'
884
- )
885
 
886
- # Auth panel
887
- with gr.Accordion("Login / Register / Activate License", open=False):
888
  with gr.Row():
889
- with gr.Column():
890
- gr.Markdown("### Login")
891
- login_email = gr.Textbox(label="Email", type="email")
892
- login_pass = gr.Textbox(label="Password", type="password")
893
- login_btn = gr.Button("LOGIN", variant="primary")
894
- login_msg = gr.Textbox(label="Status", interactive=False)
895
-
896
- with gr.Column():
897
- gr.Markdown("### Register")
898
- reg_name = gr.Textbox(label="Name")
899
- reg_email = gr.Textbox(label="Email", type="email")
900
- reg_pass = gr.Textbox(label="Password", type="password")
901
- reg_btn = gr.Button("CREATE ACCOUNT", variant="secondary")
902
- reg_msg = gr.Textbox(label="Status", interactive=False)
903
-
904
- with gr.Column():
905
- gr.Markdown("### Activate License")
906
  lic_key = gr.Textbox(label="License Key", placeholder="VYNL-XXXX-XXXX-XXXX-XXXX")
907
- lic_btn = gr.Button("ACTIVATE", variant="secondary")
908
- lic_msg = gr.Textbox(label="Status", interactive=False)
909
 
910
- # Main tabs
911
  with gr.Tabs():
912
 
913
- # ========== VYNL CORE ==========
914
- with gr.Tab("VYNL CORE"):
915
- gr.HTML("""
916
- <div style="text-align: center; padding: 20px;">
917
- <h2 style="color: #F5E6D3;">Audio Analysis Engine</h2>
918
- <p style="color: #B87333;">Stems + Chords + Beatgrid + DAW Export</p>
919
- </div>
920
- """)
921
 
922
  with gr.Row():
923
- with gr.Column():
924
- core_audio = gr.Audio(label="Upload Audio (MP3, WAV, FLAC)", type="filepath")
925
- core_name = gr.Textbox(label="Song Name (optional)")
 
 
926
 
927
  with gr.Row():
928
  core_stems = gr.Checkbox(label="Stems", value=True)
929
  core_chords = gr.Checkbox(label="Chords", value=True)
930
- core_daw = gr.Checkbox(label="DAW Project", value=True)
931
 
932
- core_btn = gr.Button("PROCESS", variant="primary", size="lg")
933
 
934
- with gr.Column():
935
- core_status = gr.Textbox(label="Processing Status", lines=15, interactive=False)
936
- core_output = gr.File(label="Download Output")
937
-
938
- core_btn.click(
939
- fn=process_song,
940
- inputs=[core_audio, core_name, core_stems, core_chords, core_daw, current_user],
941
- outputs=[core_status, core_status, core_output, status_display]
942
- )
943
 
944
  # ========== CATALOG ==========
945
  with gr.Tab("CATALOG"):
946
- gr.HTML("""
947
- <div style="text-align: center; padding: 20px;">
948
- <h2 style="color: #F5E6D3;">Catalog Processor</h2>
949
- <p style="color: #B87333;">Batch process from YouTube playlists or local files</p>
950
- </div>
951
- """)
952
 
953
  with gr.Row():
954
- with gr.Column():
955
  cat_url = gr.Textbox(label="YouTube Playlist URL", placeholder="https://youtube.com/playlist?list=...")
956
  cat_files = gr.File(label="Or Upload Files", file_count="multiple", type="filepath")
957
-
958
  with gr.Row():
959
  cat_stems = gr.Checkbox(label="Stems", value=True)
960
  cat_chords = gr.Checkbox(label="Chords", value=True)
961
-
962
  cat_btn = gr.Button("PROCESS CATALOG", variant="primary")
963
 
964
- with gr.Column():
965
- cat_status = gr.Textbox(label="Status", lines=20, interactive=False)
966
-
967
- # ========== GROOVES ==========
968
- with gr.Tab("GROOVES"):
969
- gr.HTML("""
970
- <div style="text-align: center; padding: 20px;">
971
- <div class="vinyl-logo" style="margin: 0 auto 20px;"></div>
972
- <h2 style="background: linear-gradient(90deg, #00F5FF, #FF00FF, #FFFF00); -webkit-background-clip: text; -webkit-text-fill-color: transparent; font-size: 2em;">GROOVES</h2>
973
- <p style="color: #B87333;">AI Music Generation</p>
974
- </div>
975
- """)
976
-
977
- with gr.Row():
978
- with gr.Column():
979
- groove_prompt = gr.Textbox(
980
- label="Style Prompt",
981
- lines=4,
982
- placeholder="Bluesy rock ballad, John Mayer meets Pink Floyd, atmospheric guitar..."
983
- )
984
-
985
- with gr.Row():
986
- groove_key = gr.Dropdown(["C", "D", "E", "F", "G", "A", "Am", "Dm", "Em"], label="Key", value="G")
987
- groove_bpm = gr.Slider(60, 180, value=120, label="BPM")
988
 
989
- groove_duration = gr.Slider(5, 30, value=10, step=5, label="Duration (seconds)")
990
- groove_btn = gr.Button("GENERATE", variant="primary", size="lg")
991
-
992
- with gr.Column():
993
- groove_audio = gr.Audio(label="Generated Track", type="numpy")
994
- groove_status = gr.Textbox(label="Status", lines=5, interactive=False)
995
-
996
- # ========== SESSION ==========
997
- with gr.Tab("SESSION"):
998
- gr.HTML("""
999
- <div style="text-align: center; padding: 20px; background: linear-gradient(180deg, #2E2520, #1A1A1A); border-radius: 10px;">
1000
- <h2 style="color: #FF8C42; text-shadow: 0 0 20px rgba(255,140,66,0.5);">SESSION</h2>
1001
- <p style="color: #B87333;">Multitrack Mixer + Chord Teleprompter</p>
1002
- </div>
1003
- """)
1004
 
1005
  with gr.Tabs():
 
1006
  with gr.Tab("Mixer"):
1007
  gr.HTML("""
1008
- <div style="display: flex; gap: 15px; padding: 30px; background: #1A1A1A; border-radius: 10px; justify-content: center; flex-wrap: wrap;">
1009
- <div class="fader-channel">
1010
- <input type="range" orient="vertical" style="height: 150px; writing-mode: bt-lr; -webkit-appearance: slider-vertical; accent-color: #00F5FF;">
1011
- <div class="fader-label">DRUMS</div>
1012
- </div>
1013
- <div class="fader-channel">
1014
- <input type="range" orient="vertical" style="height: 150px; writing-mode: bt-lr; -webkit-appearance: slider-vertical; accent-color: #FF00FF;">
1015
- <div class="fader-label">BASS</div>
1016
- </div>
1017
- <div class="fader-channel">
1018
- <input type="range" orient="vertical" style="height: 150px; writing-mode: bt-lr; -webkit-appearance: slider-vertical; accent-color: #FFFF00;">
1019
- <div class="fader-label">GUITAR</div>
1020
- </div>
1021
- <div class="fader-channel">
1022
- <input type="range" orient="vertical" style="height: 150px; writing-mode: bt-lr; -webkit-appearance: slider-vertical; accent-color: #7CFF00;">
1023
- <div class="fader-label">KEYS</div>
1024
- </div>
1025
- <div class="fader-channel">
1026
- <input type="range" orient="vertical" style="height: 150px; writing-mode: bt-lr; -webkit-appearance: slider-vertical; accent-color: #FF6B4A;">
1027
- <div class="fader-label">VOCALS</div>
1028
- </div>
1029
- <div class="fader-channel" style="border-color: #FF8C42;">
1030
- <input type="range" orient="vertical" style="height: 150px; writing-mode: bt-lr; -webkit-appearance: slider-vertical; accent-color: #FF8C42;">
1031
- <div class="fader-label" style="color: #FF8C42; font-weight: bold;">MASTER</div>
1032
- </div>
1033
  </div>
1034
  """)
1035
-
1036
  with gr.Row():
1037
- gr.Button("PLAY", variant="primary")
1038
- gr.Button("PAUSE")
1039
- gr.Button("STOP")
1040
 
 
1041
  with gr.Tab("Library"):
1042
  with gr.Row():
1043
- lib_search = gr.Textbox(label="Search", placeholder="Search by title, artist, key, BPM...")
1044
- lib_filter = gr.Dropdown(["All", "Title", "Artist", "Key", "BPM", "Genre"], value="All", label="Filter")
 
1045
 
1046
- lib_display = gr.Textbox(label="Library", lines=10, interactive=False, value="Upload tracks to build your library")
1047
 
 
1048
  with gr.Tab("Setlist"):
1049
- set_url = gr.Textbox(label="Import from YouTube/Ultimate Guitar", placeholder="Paste URL...")
1050
- set_import_btn = gr.Button("IMPORT", variant="secondary")
1051
- set_list = gr.Textbox(label="Current Setlist", lines=8, interactive=True)
1052
- set_start_btn = gr.Button("START PERFORMANCE", variant="primary", size="lg")
 
 
 
 
 
1053
 
 
 
 
 
 
1054
  with gr.Tab("Teleprompter"):
1055
- gr.HTML("""
1056
- <div class="teleprompter">
1057
- <div class="teleprompter-line" style="opacity: 0.4;"><span class="teleprompter-chord">G</span>Previous line fades out...</div>
1058
- <div class="teleprompter-line current"><span class="teleprompter-chord">Em</span>Current line is highlighted bright</div>
1059
- <div class="teleprompter-line" style="opacity: 0.7;"><span class="teleprompter-chord">C</span>Next line coming up</div>
1060
- <div class="teleprompter-line" style="opacity: 0.5;"><span class="teleprompter-chord">D</span>Following line ready</div>
1061
- <div style="margin-top: 40px; padding-top: 20px; border-top: 1px solid #4A3728; display: flex; gap: 30px;">
1062
- <span style="color: #7CFF00;">BPM: 120</span>
1063
- <span style="color: #00F5FF;">KEY: G Major</span>
1064
- <span style="color: #FF00FF;">NEXT: Em</span>
1065
- </div>
1066
- </div>
1067
- """)
 
 
 
 
 
 
 
 
 
 
1068
 
1069
  # ========== MASTER ==========
1070
  with gr.Tab("MASTER"):
1071
- gr.HTML("""
1072
- <div style="text-align: center; padding: 20px;">
1073
- <h2 style="color: #F5E6D3;">AI Mastering</h2>
1074
- <p style="color: #B87333;">Reference matching + Genre presets</p>
1075
- </div>
1076
- """)
1077
 
1078
  with gr.Row():
1079
  with gr.Column():
1080
  master_input = gr.Audio(label="Unmastered Track", type="filepath")
1081
- master_ref = gr.Audio(label="Reference Track (optional)", type="filepath")
1082
 
1083
  with gr.Column():
1084
- master_lufs = gr.Slider(-18, -8, value=-14, label="Target LUFS")
1085
  master_preset = gr.Dropdown(["Balanced", "Warm", "Bright", "Punchy", "Reference Match"], label="Preset", value="Balanced")
1086
-
1087
- master_btn = gr.Button("MASTER", variant="primary", size="lg")
1088
 
1089
  with gr.Row():
1090
- master_output = gr.Audio(label="Mastered Track")
1091
- master_status = gr.Textbox(label="Analysis", lines=5)
1092
-
1093
- # ========== DESKTOP APP ==========
1094
- with gr.Tab("DESKTOP APP"):
1095
- gr.HTML("""
1096
- <div style="text-align: center; padding: 30px;">
1097
- <h2 style="color: #F5E6D3;">Download VYNL Desktop</h2>
1098
- <p style="color: #B87333; margin-bottom: 30px;">Unlimited offline processing - No internet required</p>
1099
-
1100
- <div style="display: grid; grid-template-columns: repeat(auto-fit, minmax(250px, 1fr)); gap: 20px; max-width: 900px; margin: 0 auto;">
1101
- <div class="download-card">
1102
- <div class="icon">💻</div>
1103
- <h3>Windows</h3>
1104
- <p style="color: #7CFF00; font-size: 0.9em;">Windows 10/11 (64-bit)</p>
1105
- <a href="#" class="download-btn">DOWNLOAD .EXE</a>
1106
- </div>
1107
- <div class="download-card">
1108
- <div class="icon">🍎</div>
1109
- <h3>macOS</h3>
1110
- <p style="color: #7CFF00; font-size: 0.9em;">macOS 11+ (Intel/Apple Silicon)</p>
1111
- <a href="#" class="download-btn">DOWNLOAD .DMG</a>
1112
- </div>
1113
- <div class="download-card">
1114
- <div class="icon">🐧</div>
1115
- <h3>Linux</h3>
1116
- <p style="color: #7CFF00; font-size: 0.9em;">Ubuntu 20.04+ / Debian</p>
1117
- <a href="#" class="download-btn">DOWNLOAD .TAR.GZ</a>
1118
- </div>
1119
- </div>
1120
- </div>
1121
- """)
1122
-
1123
- gr.Markdown("""
1124
- ### Desktop App Features
1125
- - **Unlimited songs** - No token limits
1126
- - **100% offline** - Works without internet
1127
- - **Batch processing** - Process entire folders
1128
- - **Full DAW integration** - Reaper project export
1129
- - **Lifetime updates** - Free forever
1130
-
1131
- **Need a license key?** Contact: rlackey.seattle@gmail.com
1132
- """)
1133
 
1134
  # Footer
1135
- gr.HTML("""
1136
- <div class="footer">
1137
- <p><strong>VYNL v2.1</strong> - Created by <strong>R.T. Lackey</strong></p>
1138
- <p>Stone and Lantern Music Group</p>
1139
- <div style="margin-top: 15px; display: flex; gap: 20px; justify-content: center; flex-wrap: wrap;">
1140
- <span><span class="neon-led cyan"></span> Demucs Stems</span>
1141
- <span><span class="neon-led magenta"></span> Librosa Analysis</span>
1142
- <span><span class="neon-led yellow"></span> MusicGen AI</span>
1143
- <span><span class="neon-led green"></span> Reaper Export</span>
1144
- </div>
1145
- </div>
1146
- """)
1147
-
1148
- # Wire up auth
1149
- login_btn.click(
1150
- fn=login_user,
1151
- inputs=[login_email, login_pass],
1152
- outputs=[login_msg, status_display, current_user]
1153
  )
1154
 
1155
- reg_btn.click(
1156
- fn=register_user,
1157
- inputs=[reg_email, reg_pass, reg_name],
1158
- outputs=[reg_msg, status_display]
1159
- )
1160
 
1161
- lic_btn.click(
1162
- fn=activate_license,
1163
- inputs=[current_user, lic_key],
1164
- outputs=[lic_msg, status_display]
1165
- )
1166
 
1167
- # Wire up catalog
1168
- cat_btn.click(
1169
- fn=process_catalog,
1170
- inputs=[cat_url, cat_files, cat_stems, cat_chords, current_user],
1171
- outputs=[cat_status]
1172
- )
1173
 
1174
- # Wire up grooves
1175
- groove_btn.click(
1176
- fn=generate_groove,
1177
- inputs=[groove_prompt, groove_key, groove_bpm, groove_duration, current_user],
1178
- outputs=[groove_audio, groove_status]
1179
- )
1180
 
1181
- # Wire up mastering
1182
- master_btn.click(
1183
- fn=master_track,
1184
- inputs=[master_input, master_ref, master_lufs, master_preset, current_user],
1185
- outputs=[master_output, master_status]
1186
- )
1187
 
1188
 
1189
  if __name__ == "__main__":
1190
- demo.launch(
1191
- server_name="0.0.0.0",
1192
- server_port=7860,
1193
- css=RAINBOW_CSS,
1194
- theme=gr.themes.Base()
1195
- )
 
20
  from datetime import datetime
21
  import subprocess
22
 
23
+ # ZeroGPU support
24
+ try:
25
+ import spaces
26
+ HAS_ZEROGPU = True
27
+ except ImportError:
28
+ HAS_ZEROGPU = False
29
+ # Dummy decorator
30
+ class spaces:
31
+ @staticmethod
32
+ def GPU(duration=60):
33
+ def decorator(func):
34
+ return func
35
+ return decorator
36
+
37
  # Import token system
38
  from token_system import (
39
  user_manager, check_can_process, deduct_token,
 
58
  HAS_YTDLP = False
59
 
60
  # ============================================================================
61
+ # SLIMMED DOWN CSS - Rainbow Vinyl Theme
62
  # ============================================================================
63
 
64
  RAINBOW_CSS = """
 
 
 
 
 
65
  :root {
 
66
  --deep-black: #1A1A1A;
67
  --studio-panel: #2E2520;
68
  --warm-walnut: #4A3728;
69
  --cream-text: #F5E6D3;
 
 
70
  --neon-cyan: #00F5FF;
71
  --neon-magenta: #FF00FF;
72
+ --neon-yellow: #FFFF00;
73
  --neon-green: #7CFF00;
74
+ --coral: #FF6B4A;
75
+ --copper: #B87333;
 
 
 
 
 
76
  }
77
 
 
78
  .gradio-container {
79
+ background: linear-gradient(180deg, #1A1A1A 0%, #2E2520 50%, #1A1A1A 100%) !important;
80
+ max-width: 1200px !important;
 
 
 
 
 
81
  }
82
 
83
+ /* Compact header */
 
 
 
 
 
 
84
  .main-header {
85
+ background: linear-gradient(180deg, rgba(46,37,32,0.95), rgba(26,26,26,0.98));
86
+ border-bottom: 2px solid;
87
  border-image: linear-gradient(90deg, #00F5FF, #FF00FF, #FFFF00, #7CFF00) 1;
88
+ padding: 12px 20px;
89
  text-align: center;
90
+ margin-bottom: 8px;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
91
  }
92
 
93
  .logo-text {
94
  font-family: 'Courier New', monospace;
95
+ font-size: 2.2em;
96
  font-weight: 900;
97
  background: linear-gradient(90deg, #00F5FF, #FF00FF, #FFFF00, #7CFF00);
98
  -webkit-background-clip: text;
99
  -webkit-text-fill-color: transparent;
100
+ letter-spacing: 0.2em;
 
101
  margin: 0;
 
102
  }
103
 
104
  .tagline {
105
  color: var(--cream-text);
106
+ font-size: 0.85em;
107
+ opacity: 0.7;
108
+ margin-top: 2px;
 
109
  }
110
 
111
+ /* Status bar - compact */
112
  .status-bar {
113
+ background: rgba(0,245,255,0.1);
114
  border: 1px solid rgba(0,245,255,0.3);
115
+ border-radius: 20px;
116
+ padding: 6px 16px;
117
+ margin: 8px auto;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
118
  max-width: 500px;
119
+ text-align: center;
120
+ font-size: 0.9em;
121
+ color: var(--cream-text);
 
 
 
 
 
 
 
122
  }
123
 
124
+ /* Tabs */
125
  .tab-nav button {
126
+ background: var(--studio-panel) !important;
 
 
127
  color: var(--cream-text) !important;
128
+ border: 1px solid var(--warm-walnut) !important;
129
+ padding: 8px 16px !important;
130
+ font-size: 0.9em !important;
 
 
131
  }
 
 
 
 
 
 
132
  .tab-nav button.selected {
133
+ background: var(--coral) !important;
134
+ border-color: var(--coral) !important;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
135
  }
136
 
137
+ /* Inputs */
138
+ input, textarea, select {
139
+ background: var(--deep-black) !important;
140
+ border: 1px solid var(--warm-walnut) !important;
141
  color: var(--cream-text) !important;
142
+ border-radius: 6px !important;
 
 
143
  }
144
+ input:focus, textarea:focus {
 
145
  border-color: var(--neon-cyan) !important;
 
 
 
 
 
 
 
 
 
146
  }
147
 
148
+ /* Buttons */
149
+ button.primary {
150
+ background: var(--coral) !important;
151
  border: none !important;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
152
  }
153
+ button.secondary {
154
+ background: var(--warm-walnut) !important;
155
+ border: 1px solid var(--copper) !important;
 
 
 
 
156
  }
157
 
158
+ /* Panels */
159
+ .gr-box, .gr-panel {
160
+ background: var(--studio-panel) !important;
161
+ border: 1px solid var(--warm-walnut) !important;
162
+ border-radius: 8px !important;
163
  }
164
 
165
+ /* Labels */
166
+ label { color: var(--cream-text) !important; font-size: 0.9em !important; }
 
 
167
 
168
  /* Mixer faders */
169
+ .fader-wrap {
170
+ display: flex; gap: 8px; justify-content: center;
171
+ background: var(--deep-black); padding: 15px; border-radius: 8px;
 
 
 
172
  }
173
+ .fader-ch {
174
+ text-align: center; padding: 8px;
175
+ background: var(--studio-panel); border-radius: 6px;
176
+ border: 1px solid var(--warm-walnut);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
177
  }
178
+ .fader-ch input[type="range"] { width: 40px; height: 100px; }
179
+ .fader-lbl { color: var(--cream-text); font-size: 0.75em; margin-top: 4px; }
180
 
181
  /* Teleprompter */
182
+ .teleprompter-box {
183
+ background: #0A0A0A; border: 2px solid var(--neon-magenta);
184
+ border-radius: 10px; padding: 20px; min-height: 200px;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
185
  }
186
+ .tele-line { font-family: monospace; font-size: 1.1em; line-height: 1.8; color: var(--cream-text); opacity: 0.5; }
187
+ .tele-line.current { font-size: 1.4em; color: var(--neon-cyan); opacity: 1; }
188
+ .tele-chord { color: var(--neon-magenta); font-weight: bold; margin-right: 10px; }
189
 
190
  /* Footer */
191
+ .footer { text-align: center; padding: 15px; color: var(--cream-text); opacity: 0.6; font-size: 0.85em; }
192
+ """
 
 
 
 
 
 
193
 
194
+ # ============================================================================
195
+ # SESSION STORAGE (in-memory for demo, persisted in real deployment)
196
+ # ============================================================================
 
197
 
198
+ session_library = [] # List of dicts: {title, artist, key, bpm, duration, file_path}
199
+ session_setlist = [] # List of song titles in order
 
 
 
 
 
 
 
 
 
 
200
 
201
+ # ============================================================================
202
+ # YOUTUBE DOWNLOAD & ANALYSIS
203
+ # ============================================================================
 
 
204
 
205
+ def download_youtube_audio(url):
206
+ """Download audio from YouTube URL"""
207
+ if not HAS_YTDLP:
208
+ return None, "yt-dlp not installed"
209
 
210
+ try:
211
+ temp_dir = tempfile.mkdtemp(prefix="vynl_yt_")
212
+ ydl_opts = {
213
+ 'format': 'bestaudio/best',
214
+ 'postprocessors': [{
215
+ 'key': 'FFmpegExtractAudio',
216
+ 'preferredcodec': 'mp3',
217
+ 'preferredquality': '192',
218
+ }],
219
+ 'outtmpl': f'{temp_dir}/%(title)s.%(ext)s',
220
+ 'quiet': True,
221
+ }
222
+
223
+ with yt_dlp.YoutubeDL(ydl_opts) as ydl:
224
+ info = ydl.extract_info(url, download=True)
225
+ title = info.get('title', 'Unknown')
226
+
227
+ # Find the mp3 file
228
+ for f in Path(temp_dir).glob('*.mp3'):
229
+ return str(f), title
230
+
231
+ return None, "Download failed"
232
+ except Exception as e:
233
+ return None, str(e)
234
+
235
+ def get_playlist_videos(url):
236
+ """Get videos from YouTube playlist"""
237
+ if not HAS_YTDLP:
238
+ return [], "yt-dlp not installed"
239
+ try:
240
+ ydl_opts = {'quiet': True, 'extract_flat': True}
241
+ with yt_dlp.YoutubeDL(ydl_opts) as ydl:
242
+ info = ydl.extract_info(url, download=False)
243
+ videos = []
244
+ if 'entries' in info:
245
+ for entry in info['entries']:
246
+ if entry:
247
+ videos.append({
248
+ 'title': entry.get('title', 'Unknown'),
249
+ 'url': entry.get('url') or f"https://youtube.com/watch?v={entry.get('id')}"
250
+ })
251
+ return videos, None
252
+ except Exception as e:
253
+ return [], str(e)
254
 
255
  # ============================================================================
256
+ # AUDIO ANALYSIS
257
  # ============================================================================
258
 
259
  def get_audio_duration(audio_path):
260
  """Get audio duration in seconds"""
261
  if HAS_LIBROSA:
262
  try:
 
263
  return librosa.get_duration(path=audio_path)
264
  except:
265
  pass
266
+ return 180
267
+
268
+ def analyze_audio_full(audio_path):
269
+ """Full audio analysis - tempo, key, duration"""
270
+ if not HAS_LIBROSA:
271
+ return {'tempo': 120, 'key': 'C', 'duration': 0, 'error': 'librosa not installed'}
272
+
273
+ try:
274
+ y, sr = librosa.load(audio_path, sr=22050, duration=120)
275
+
276
+ # Tempo
277
+ tempo, _ = librosa.beat.beat_track(y=y, sr=sr)
278
+ if hasattr(tempo, '__iter__'):
279
+ tempo = float(tempo[0])
280
+
281
+ # Key
282
+ chroma = librosa.feature.chroma_cqt(y=y, sr=sr)
283
+ notes = ['C', 'C#', 'D', 'D#', 'E', 'F', 'F#', 'G', 'G#', 'A', 'A#', 'B']
284
+ key_idx = int(np.argmax(np.mean(chroma, axis=1)))
285
+ key = notes[key_idx]
286
+
287
+ # Duration
288
+ duration = librosa.get_duration(y=y, sr=sr)
289
+
290
+ return {'tempo': round(tempo), 'key': key, 'duration': round(duration, 1)}
291
+ except Exception as e:
292
+ return {'tempo': 120, 'key': 'C', 'duration': 0, 'error': str(e)}
293
+
294
+ # ============================================================================
295
+ # PROCESSING FUNCTIONS
296
+ # ============================================================================
297
+
298
+ @spaces.GPU(duration=120)
299
+ def process_song(audio_file, youtube_url, song_name, lyrics, do_stems, do_chords, do_daw, user_email, progress=gr.Progress()):
300
+ """Process a single song - from file or YouTube URL (GPU accelerated)"""
301
 
302
+ # Determine source
303
+ if youtube_url and youtube_url.strip():
304
+ progress(0.1, desc="Downloading from YouTube...")
305
+ audio_file, title = download_youtube_audio(youtube_url.strip())
306
+ if not audio_file:
307
+ return f"YouTube download failed: {title}", None, get_status_display(user_email)
308
+ if not song_name:
309
+ song_name = title
310
 
311
  if not audio_file:
312
+ return "Please upload an audio file or enter a YouTube URL", None, get_status_display(user_email)
313
 
314
  # Check duration limit
315
  duration = get_audio_duration(audio_file)
316
  can_process, msg, status = check_can_process(user_email, duration)
317
 
318
  if not can_process:
319
+ return msg, None, get_status_display(user_email)
320
 
321
  if not song_name:
322
  song_name = Path(audio_file).stem
 
324
  # Deduct token
325
  ok, token_msg = deduct_token(user_email)
326
  if not ok:
327
+ return token_msg, None, get_status_display(user_email)
 
 
 
 
 
 
328
 
329
+ log = [f"Processing: {song_name}", f"Duration: {duration:.1f}s", token_msg, "=" * 40]
330
  output_files = []
331
  temp_dir = tempfile.mkdtemp(prefix="vynl_")
332
  output_path = Path(temp_dir)
333
 
334
  try:
335
+ # Analysis
336
+ progress(0.15, desc="Analyzing audio...")
337
+ analysis = analyze_audio_full(audio_file)
338
+ log.append(f"\n[ANALYSIS]\n Tempo: {analysis['tempo']} BPM\n Key: {analysis['key']}")
339
+
340
  # Stem separation
341
  if do_stems:
342
+ progress(0.3, desc="Separating stems...")
343
+ log.append("\n[STEMS]")
 
344
  try:
345
  stems_dir = output_path / "stems"
346
  stems_dir.mkdir(exist_ok=True)
347
+ cmd = ['demucs', '--two-stems=vocals', '-o', str(stems_dir), '--mp3', audio_file]
 
348
  result = subprocess.run(cmd, capture_output=True, text=True, timeout=600)
 
349
  if result.returncode == 0:
350
  for stem in stems_dir.rglob("*.mp3"):
351
  output_files.append(str(stem))
352
+ log.append(f" Created: {stem.name}")
353
  else:
354
+ log.append(" Demucs not available")
355
  except Exception as e:
356
+ log.append(f" Note: {str(e)[:40]}")
357
 
358
+ # Chords
359
  if do_chords:
360
+ progress(0.6, desc="Creating chord chart...")
361
+ log.append("\n[CHORDS]")
362
 
363
+ # Use provided lyrics or placeholder
364
+ if lyrics and lyrics.strip():
365
+ lyric_content = lyrics.strip()
366
+ else:
367
+ lyric_content = "[No lyrics provided - add in Sessions > Teleprompter]"
 
368
 
369
+ chart = f"""# {song_name}
370
+ Tempo: {analysis['tempo']} BPM | Key: {analysis['key']}
 
 
371
 
372
+ {lyric_content}
 
373
 
374
+ ---
375
+ Generated by VYNL | {datetime.now().strftime('%Y-%m-%d')}
376
+ """
377
+ chart_file = output_path / f"{song_name}_CHORDS.txt"
378
+ chart_file.write_text(chart)
379
+ output_files.append(str(chart_file))
380
+ log.append(f" Created: {chart_file.name}")
381
 
382
  # DAW project
383
  if do_daw:
384
+ progress(0.8, desc="Creating DAW project...")
385
+ log.append("\n[DAW PROJECT]")
386
+ rpp = f'<REAPER_PROJECT 0.1 "6.0">\n TEMPO {analysis["tempo"]} 4 4\n <TRACK>\n NAME "{song_name}"\n >\n>'
 
387
  rpp_file = output_path / f"{song_name}.RPP"
388
  rpp_file.write_text(rpp)
389
  output_files.append(str(rpp_file))
390
+ log.append(f" Created: {rpp_file.name}")
391
+
392
+ # Add to library
393
+ session_library.append({
394
+ 'title': song_name,
395
+ 'artist': 'Unknown',
396
+ 'key': analysis['key'],
397
+ 'bpm': analysis['tempo'],
398
+ 'duration': f"{int(duration//60)}:{int(duration%60):02d}"
399
+ })
400
 
401
+ progress(0.95, desc="Packaging...")
402
 
403
  # Create zip
404
  if output_files:
 
407
  for f in output_files:
408
  zf.write(f, Path(f).name)
409
 
410
+ log.append(f"\n[COMPLETE]\nPackage: {zip_path.name}")
 
 
411
  progress(1.0, desc="Done!")
412
+ return "\n".join(log), str(zip_path), get_status_display(user_email)
413
 
414
  except Exception as e:
415
+ return f"Error: {str(e)}", None, get_status_display(user_email)
416
 
417
+ return "\n".join(log), None, get_status_display(user_email)
418
 
419
+ # ============================================================================
420
+ # AUTH FUNCTIONS - FIXED
421
+ # ============================================================================
422
 
423
  def register_user(email, password, name):
424
  """Register new user account"""
425
+ if not email or not password:
426
+ return "Email and password required", "", ""
427
  ok, msg = user_manager.create_account(email, password, name)
428
  if ok:
429
+ return f"Account created! You have 3 free demo tokens.", get_status_display(email), email
430
+ return msg, "", ""
 
431
 
432
  def login_user(email, password):
433
  """Login user"""
434
+ if not email or not password:
435
+ return "Email and password required", "", ""
436
  ok, user = user_manager.login(email, password)
437
  if ok:
438
  return f"Welcome back, {user['name']}!", get_status_display(email), email
439
  return "Invalid email or password", "", ""
440
 
 
441
  def activate_license(email, license_key):
442
  """Activate license for user"""
443
  if not email:
444
+ return "Please login or register first", ""
445
+ if not license_key:
446
+ return "Enter a license key", ""
447
  ok, msg = user_manager.activate_license(email, license_key)
448
  return msg, get_status_display(email)
449
 
450
+ # ============================================================================
451
+ # CATALOG FUNCTIONS
452
+ # ============================================================================
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
453
 
454
  def process_catalog(playlist_url, files, do_stems, do_chords, user_email, progress=gr.Progress()):
455
  """Batch process from YouTube playlist or files"""
456
 
457
+ results = []
 
 
 
458
  songs = []
459
 
460
+ # Get from YouTube
461
  if playlist_url and HAS_YTDLP:
462
  progress(0.1, desc="Fetching playlist...")
463
+ videos, error = get_playlist_videos(playlist_url)
464
+ if error:
465
+ results.append(f"Playlist error: {error}")
466
+ else:
467
+ for v in videos[:20]: # Limit 20
468
+ songs.append({'title': v['title'], 'url': v['url'], 'source': 'youtube'})
469
+ results.append(f"Found: {v['title'][:50]}")
 
 
 
 
 
 
 
 
 
 
 
470
 
471
  # Add uploaded files
472
  if files:
473
  for f in files:
474
+ songs.append({'title': Path(f).stem, 'path': f, 'source': 'local'})
475
+ results.append(f"Added: {Path(f).stem}")
476
+
477
+ if not songs:
478
+ return "No songs to process. Add a playlist URL or upload files."
 
479
 
480
+ results.append(f"\n{'='*40}\nProcessing {len(songs)} songs...\n")
481
 
 
482
  for i, song in enumerate(songs):
483
+ progress((i+1)/len(songs), desc=f"Processing {song['title'][:25]}...")
 
484
 
485
  # Check tokens
486
  can_process, msg, _ = check_can_process(user_email, 0)
487
  if not can_process:
488
+ results.append(f"Stopped: {msg}")
489
  break
490
 
491
  deduct_token(user_email)
 
 
492
 
493
+ # Add to library
494
+ session_library.append({
495
+ 'title': song['title'],
496
+ 'artist': 'Unknown',
497
+ 'key': 'C',
498
+ 'bpm': 120,
499
+ 'duration': '--:--'
500
+ })
501
 
502
+ results.append(f"[{i+1}/{len(songs)}] {song['title'][:40]} - Done")
503
 
504
+ results.append(f"\n{'='*40}\nCATALOG COMPLETE")
505
+ return "\n".join(results)
506
 
507
+ def get_library_display(sort_by="title"):
508
+ """Get formatted library for display"""
509
+ if not session_library:
510
+ return "Library empty. Process songs to add them."
511
+
512
+ # Sort
513
+ sorted_lib = sorted(session_library, key=lambda x: str(x.get(sort_by, '')).lower())
514
 
515
+ lines = [f"{'Title':<35} {'Artist':<20} {'Key':<5} {'BPM':<5} {'Dur':<6}"]
516
+ lines.append("-" * 75)
517
+ for song in sorted_lib:
518
+ lines.append(f"{song['title'][:34]:<35} {song.get('artist','')[:19]:<20} {song.get('key',''):<5} {str(song.get('bpm','')):<5} {song.get('duration',''):<6}")
519
+
520
+ return "\n".join(lines)
521
+
522
+ def sort_library(sort_by):
523
+ """Sort and return library"""
524
+ return get_library_display(sort_by.lower())
525
+
526
+ # ============================================================================
527
+ # GROOVES
528
+ # ============================================================================
529
+
530
+ @spaces.GPU(duration=60)
531
+ def generate_groove(prompt, key, bpm, duration, user_email, progress=gr.Progress()):
532
+ """Generate music with GROOVES (GPU accelerated)"""
533
  if not prompt:
534
+ return None, "Enter a style prompt"
535
 
536
+ can_process, msg, _ = check_can_process(user_email, 0)
 
537
  if not can_process:
538
  return None, msg
539
 
 
 
540
  deduct_token(user_email)
541
+ deduct_token(user_email) # AI gen costs 2
542
 
543
+ progress(0.5, desc="Generating...")
544
+ time.sleep(2) # Placeholder
 
 
 
 
 
 
545
 
546
+ progress(1.0, desc="Done!")
547
  return None, f"""GROOVES - AI Music Generation
548
 
549
  Prompt: {prompt}
550
+ Key: {key} | BPM: {bpm} | Duration: {duration}s
 
 
551
 
552
+ Note: Full AI generation requires GPU.
553
+ 2 tokens used.
 
 
554
  {get_status_display(user_email)}"""
555
 
556
+ # ============================================================================
557
+ # SESSIONS - LIBRARY, SETLIST, TELEPROMPTER
558
+ # ============================================================================
559
+
560
+ def add_to_setlist(title):
561
+ """Add song to setlist"""
562
+ if title and title.strip():
563
+ session_setlist.append(title.strip())
564
+ return "\n".join([f"{i+1}. {s}" for i, s in enumerate(session_setlist)])
565
+
566
+ def clear_setlist():
567
+ """Clear setlist"""
568
+ session_setlist.clear()
569
+ return ""
570
+
571
+ def import_setlist_url(url):
572
+ """Import setlist from URL"""
573
+ if not url:
574
+ return "Enter a URL"
575
+
576
+ # Placeholder - would scrape ultimate-guitar or youtube
577
+ if 'youtube' in url.lower():
578
+ videos, err = get_playlist_videos(url)
579
+ if videos:
580
+ for v in videos[:15]:
581
+ session_setlist.append(v['title'])
582
+ return "\n".join([f"{i+1}. {s}" for i, s in enumerate(session_setlist)])
583
+
584
+ return "URL import: Add songs manually or paste YouTube playlist URL"
585
+
586
+ def get_teleprompter_display(lyrics_text, current_line=0):
587
+ """Format lyrics for teleprompter display"""
588
+ if not lyrics_text or not lyrics_text.strip():
589
+ return """<div class="teleprompter-box">
590
+ <p style="color: #888; text-align: center;">Paste lyrics above to display in teleprompter</p>
591
+ </div>"""
592
+
593
+ lines = lyrics_text.strip().split('\n')
594
+ html_lines = []
595
+
596
+ for i, line in enumerate(lines):
597
+ if i == current_line:
598
+ html_lines.append(f'<div class="tele-line current"><span class="tele-chord">[C]</span>{line}</div>')
599
+ else:
600
+ html_lines.append(f'<div class="tele-line"><span class="tele-chord">[G]</span>{line}</div>')
601
+
602
+ return f'<div class="teleprompter-box">{"".join(html_lines)}</div>'
603
+
604
+ # ============================================================================
605
+ # MASTERING
606
+ # ============================================================================
607
+
608
+ def master_track(input_audio, reference_audio, target_lufs, preset, user_email, progress=gr.Progress()):
609
+ """Master a track with AI"""
610
+ if not input_audio:
611
+ return None, "Upload an audio file to master"
612
+
613
+ can_process, msg, _ = check_can_process(user_email, 0)
614
+ if not can_process:
615
+ return None, msg
616
+
617
+ ok, token_msg = deduct_token(user_email)
618
+ if not ok:
619
+ return None, token_msg
620
+
621
+ progress(0.3, desc=f"Applying {preset}...")
622
+
623
+ try:
624
+ output_path, analysis = master_audio(
625
+ input_path=input_audio,
626
+ output_path=None,
627
+ preset=preset,
628
+ reference_path=reference_audio if preset == "Reference Match" else None,
629
+ target_lufs=target_lufs
630
+ )
631
+
632
+ progress(1.0, desc="Complete!")
633
+
634
+ if output_path:
635
+ result_text = format_analysis(analysis)
636
+ result_text += f"\n\n{token_msg}"
637
+ return output_path, result_text
638
+ else:
639
+ return None, f"Mastering failed: {analysis.get('error', 'Unknown')}"
640
+
641
+ except Exception as e:
642
+ return None, f"Error: {str(e)}"
643
 
644
  # ============================================================================
645
  # BUILD INTERFACE
646
  # ============================================================================
647
 
648
+ with gr.Blocks(css=RAINBOW_CSS, title="VYNL", theme=gr.themes.Base()) as demo:
649
 
650
  # State
651
  current_user = gr.State("")
652
 
653
+ # Compact Header
654
  gr.HTML("""
655
  <div class="main-header">
 
656
  <h1 class="logo-text">VYNL</h1>
657
+ <p class="tagline">From raw demo to DAW-ready</p>
658
  </div>
659
  """)
660
 
661
  # Status bar
662
+ status_display = gr.HTML('<div class="status-bar">DEMO: 3 tokens | 5-min limit | Login to track usage</div>')
 
 
 
663
 
664
+ # Auth - compact accordion
665
+ with gr.Accordion("Account / License", open=False):
666
  with gr.Row():
667
+ with gr.Column(scale=1):
668
+ login_email = gr.Textbox(label="Email", scale=1)
669
+ login_pass = gr.Textbox(label="Password", type="password", scale=1)
670
+ with gr.Row():
671
+ login_btn = gr.Button("Login", variant="primary", size="sm")
672
+ reg_btn = gr.Button("Register", size="sm")
673
+ auth_msg = gr.Textbox(label="Status", interactive=False, lines=1)
674
+
675
+ with gr.Column(scale=1):
 
 
 
 
 
 
 
 
676
  lic_key = gr.Textbox(label="License Key", placeholder="VYNL-XXXX-XXXX-XXXX-XXXX")
677
+ lic_btn = gr.Button("Activate License", variant="secondary")
678
+ lic_msg = gr.Textbox(label="License Status", interactive=False, lines=1)
679
 
680
+ # Main Tabs
681
  with gr.Tabs():
682
 
683
+ # ========== PROCESS ==========
684
+ with gr.Tab("PROCESS"):
685
+ gr.Markdown("### Analyze & Export - Upload or YouTube URL")
 
 
 
 
 
686
 
687
  with gr.Row():
688
+ with gr.Column(scale=1):
689
+ core_audio = gr.Audio(label="Upload Audio", type="filepath")
690
+ core_yt_url = gr.Textbox(label="Or YouTube URL", placeholder="https://youtube.com/watch?v=...")
691
+ core_name = gr.Textbox(label="Song Name (optional)", placeholder="Auto-detected from file/URL")
692
+ core_lyrics = gr.Textbox(label="Paste Lyrics (for chart)", lines=4, placeholder="Verse 1:\nLyrics here...")
693
 
694
  with gr.Row():
695
  core_stems = gr.Checkbox(label="Stems", value=True)
696
  core_chords = gr.Checkbox(label="Chords", value=True)
697
+ core_daw = gr.Checkbox(label="DAW", value=True)
698
 
699
+ core_btn = gr.Button("PROCESS", variant="primary")
700
 
701
+ with gr.Column(scale=1):
702
+ core_status = gr.Textbox(label="Output Log", lines=18, interactive=False)
703
+ core_output = gr.File(label="Download")
 
 
 
 
 
 
704
 
705
  # ========== CATALOG ==========
706
  with gr.Tab("CATALOG"):
707
+ gr.Markdown("### Batch Process - Playlists & Folders")
 
 
 
 
 
708
 
709
  with gr.Row():
710
+ with gr.Column(scale=1):
711
  cat_url = gr.Textbox(label="YouTube Playlist URL", placeholder="https://youtube.com/playlist?list=...")
712
  cat_files = gr.File(label="Or Upload Files", file_count="multiple", type="filepath")
 
713
  with gr.Row():
714
  cat_stems = gr.Checkbox(label="Stems", value=True)
715
  cat_chords = gr.Checkbox(label="Chords", value=True)
 
716
  cat_btn = gr.Button("PROCESS CATALOG", variant="primary")
717
 
718
+ with gr.Column(scale=1):
719
+ cat_status = gr.Textbox(label="Status", lines=18, interactive=False)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
720
 
721
+ # ========== SESSIONS ==========
722
+ with gr.Tab("SESSIONS"):
723
+ gr.Markdown("### Live Performance - Mixer, Library, Setlist, Teleprompter")
 
 
 
 
 
 
 
 
 
 
 
 
724
 
725
  with gr.Tabs():
726
+ # Mixer
727
  with gr.Tab("Mixer"):
728
  gr.HTML("""
729
+ <div class="fader-wrap">
730
+ <div class="fader-ch"><input type="range" min="0" max="100" value="80" orient="vertical"><div class="fader-lbl">DRUMS</div></div>
731
+ <div class="fader-ch"><input type="range" min="0" max="100" value="75" orient="vertical"><div class="fader-lbl">BASS</div></div>
732
+ <div class="fader-ch"><input type="range" min="0" max="100" value="70" orient="vertical"><div class="fader-lbl">GTR</div></div>
733
+ <div class="fader-ch"><input type="range" min="0" max="100" value="65" orient="vertical"><div class="fader-lbl">KEYS</div></div>
734
+ <div class="fader-ch"><input type="range" min="0" max="100" value="85" orient="vertical"><div class="fader-lbl">VOX</div></div>
735
+ <div class="fader-ch" style="border-color: #FF6B4A;"><input type="range" min="0" max="100" value="90" orient="vertical"><div class="fader-lbl" style="color:#FF6B4A;">MASTER</div></div>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
736
  </div>
737
  """)
 
738
  with gr.Row():
739
+ gr.Button("PLAY", variant="primary", size="sm")
740
+ gr.Button("PAUSE", size="sm")
741
+ gr.Button("STOP", size="sm")
742
 
743
+ # Library
744
  with gr.Tab("Library"):
745
  with gr.Row():
746
+ lib_search = gr.Textbox(label="Search", placeholder="Search...", scale=3)
747
+ lib_sort = gr.Dropdown(["Title", "Artist", "Key", "BPM"], value="Title", label="Sort By", scale=1)
748
+ lib_refresh = gr.Button("Refresh", size="sm", scale=1)
749
 
750
+ lib_display = gr.Textbox(label="Library", lines=12, interactive=False, value="Process songs to add to library")
751
 
752
+ # Setlist
753
  with gr.Tab("Setlist"):
754
+ with gr.Row():
755
+ set_url = gr.Textbox(label="Import from YouTube Playlist", placeholder="Paste URL...", scale=3)
756
+ set_import = gr.Button("Import", size="sm", scale=1)
757
+
758
+ with gr.Row():
759
+ set_add_title = gr.Textbox(label="Add Song Title", placeholder="Song name...", scale=3)
760
+ set_add_btn = gr.Button("Add", size="sm", scale=1)
761
+
762
+ set_list = gr.Textbox(label="Current Setlist", lines=10, interactive=True)
763
 
764
+ with gr.Row():
765
+ gr.Button("START PERFORMANCE", variant="primary")
766
+ set_clear = gr.Button("Clear", variant="secondary")
767
+
768
+ # Teleprompter
769
  with gr.Tab("Teleprompter"):
770
+ tele_lyrics = gr.Textbox(label="Paste Lyrics + Chords", lines=6, placeholder="[G] First line of the verse\n[Em] Second line here\n[C] And so on...")
771
+ tele_display = gr.HTML('<div class="teleprompter-box"><p style="color:#888;text-align:center;">Paste lyrics above</p></div>')
772
+
773
+ with gr.Row():
774
+ gr.Button("PLAY", variant="primary", size="sm")
775
+ gr.Slider(60, 180, value=120, label="BPM", scale=2)
776
+
777
+ # ========== GROOVES ==========
778
+ with gr.Tab("GROOVES"):
779
+ gr.Markdown("### AI Music Generation")
780
+
781
+ with gr.Row():
782
+ with gr.Column():
783
+ groove_prompt = gr.Textbox(label="Style Prompt", lines=3, placeholder="Bluesy rock, John Mayer style...")
784
+ with gr.Row():
785
+ groove_key = gr.Dropdown(["C", "D", "E", "F", "G", "A", "Am", "Dm", "Em"], label="Key", value="G")
786
+ groove_bpm = gr.Slider(60, 180, value=120, label="BPM")
787
+ groove_duration = gr.Slider(5, 30, value=10, step=5, label="Duration (s)")
788
+ groove_btn = gr.Button("GENERATE", variant="primary")
789
+
790
+ with gr.Column():
791
+ groove_audio = gr.Audio(label="Generated", type="numpy")
792
+ groove_status = gr.Textbox(label="Status", lines=4, interactive=False)
793
 
794
  # ========== MASTER ==========
795
  with gr.Tab("MASTER"):
796
+ gr.Markdown("### AI Mastering - Reference Match & Presets")
 
 
 
 
 
797
 
798
  with gr.Row():
799
  with gr.Column():
800
  master_input = gr.Audio(label="Unmastered Track", type="filepath")
801
+ master_ref = gr.Audio(label="Reference (optional)", type="filepath")
802
 
803
  with gr.Column():
 
804
  master_preset = gr.Dropdown(["Balanced", "Warm", "Bright", "Punchy", "Reference Match"], label="Preset", value="Balanced")
805
+ master_lufs = gr.Slider(-18, -8, value=-14, label="Target LUFS")
806
+ master_btn = gr.Button("MASTER", variant="primary")
807
 
808
  with gr.Row():
809
+ master_output = gr.Audio(label="Mastered")
810
+ master_status = gr.Textbox(label="Analysis", lines=6)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
811
 
812
  # Footer
813
+ gr.HTML('''<div class="footer">
814
+ <p><strong>VYNL v2.1</strong> | R.T. Lackey | Stone and Lantern Music Group</p>
815
+ <p style="margin-top:8px;color:#FF6B4A;">Desktop Pre-Order: <strong>$79.99</strong> (First 1000) | rtlackey@icloud.com</p>
816
+ </div>''')
817
+
818
+ # ========== WIRE UP EVENTS ==========
819
+
820
+ # Auth
821
+ login_btn.click(login_user, [login_email, login_pass], [auth_msg, status_display, current_user])
822
+ reg_btn.click(register_user, [login_email, login_pass, login_email], [auth_msg, status_display, current_user])
823
+ lic_btn.click(activate_license, [current_user, lic_key], [lic_msg, status_display])
824
+
825
+ # Process
826
+ core_btn.click(
827
+ process_song,
828
+ [core_audio, core_yt_url, core_name, core_lyrics, core_stems, core_chords, core_daw, current_user],
829
+ [core_status, core_output, status_display]
 
830
  )
831
 
832
+ # Catalog
833
+ cat_btn.click(process_catalog, [cat_url, cat_files, cat_stems, cat_chords, current_user], [cat_status])
 
 
 
834
 
835
+ # Library
836
+ lib_sort.change(sort_library, [lib_sort], [lib_display])
837
+ lib_refresh.click(lambda: get_library_display(), None, [lib_display])
 
 
838
 
839
+ # Setlist
840
+ set_add_btn.click(add_to_setlist, [set_add_title], [set_list])
841
+ set_import.click(import_setlist_url, [set_url], [set_list])
842
+ set_clear.click(clear_setlist, None, [set_list])
 
 
843
 
844
+ # Teleprompter
845
+ tele_lyrics.change(get_teleprompter_display, [tele_lyrics], [tele_display])
 
 
 
 
846
 
847
+ # Grooves
848
+ groove_btn.click(generate_groove, [groove_prompt, groove_key, groove_bpm, groove_duration, current_user], [groove_audio, groove_status])
849
+
850
+ # Master
851
+ master_btn.click(master_track, [master_input, master_ref, master_lufs, master_preset, current_user], [master_output, master_status])
 
852
 
853
 
854
  if __name__ == "__main__":
855
+ demo.launch(server_name="0.0.0.0", server_port=7860)
 
 
 
 
 
requirements.txt CHANGED
@@ -1,4 +1,4 @@
1
- gradio>=4.0.0
2
  numpy>=1.24.0
3
  librosa>=0.10.0
4
  soundfile>=0.12.0
@@ -7,5 +7,4 @@ yt-dlp>=2023.10.0
7
  torch>=2.0.0
8
  torchaudio>=2.0.0
9
  demucs>=4.0.0
10
- transformers>=4.31.0
11
- accelerate>=0.20.0
 
1
+ huggingface_hub==0.21.4
2
  numpy>=1.24.0
3
  librosa>=0.10.0
4
  soundfile>=0.12.0
 
7
  torch>=2.0.0
8
  torchaudio>=2.0.0
9
  demucs>=4.0.0
10
+ pyloudnorm>=0.1.0