Simonc-44 commited on
Commit
d6e665d
·
verified ·
1 Parent(s): 017cb04

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +73 -1065
app.py CHANGED
@@ -1,1065 +1,73 @@
1
- <!DOCTYPE html>
2
- <html lang="fr">
3
- <head>
4
- <meta charset="UTF-8">
5
- <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
- <title>Cygnis Music</title>
7
- <link rel="preconnect" href="https://fonts.googleapis.com">
8
- <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
9
- <link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&display=swap" rel="stylesheet">
10
- <script src="https://unpkg.com/lucide@latest"></script>
11
- <!-- Firebase SDK -->
12
- <script type="module">
13
- import { initializeApp } from "https://www.gstatic.com/firebasejs/10.7.1/firebase-app.js";
14
- import { getAuth, signInWithCustomToken } from "https://www.gstatic.com/firebasejs/10.7.1/firebase-auth.js";
15
- import { getFirestore, collection, addDoc, query, where, getDocs, orderBy, deleteDoc, doc, updateDoc } from "https://www.gstatic.com/firebasejs/10.7.1/firebase-firestore.js";
16
-
17
- // Initialisation Firebase (Config injectée par le proxy)
18
- let db, auth, user;
19
-
20
- if (window.FIREBASE_CONFIG) {
21
- const app = initializeApp(window.FIREBASE_CONFIG);
22
- auth = getAuth(app);
23
- db = getFirestore(app);
24
-
25
- // Connexion automatique
26
- if (window.CUSTOM_TOKEN) {
27
- signInWithCustomToken(auth, window.CUSTOM_TOKEN)
28
- .then((userCredential) => {
29
- user = userCredential.user;
30
- console.log("Connecté:", user.uid);
31
- document.getElementById('login-screen').style.display = 'none';
32
- document.getElementById('app-container').classList.add('visible');
33
- document.getElementById('user-name').textContent = user.email.split('@')[0];
34
- loadTracks();
35
- })
36
- .catch((error) => {
37
- console.error("Erreur connexion:", error);
38
- document.getElementById('login-error').textContent = "Erreur de session. Veuillez rafraîchir.";
39
- document.getElementById('login-error').style.display = 'block';
40
- });
41
- }
42
- }
43
-
44
- // Exposer les fonctions globalement pour le script principal
45
- window.saveTrackToFirestore = async (track) => {
46
- if (!user || !db) return;
47
- try {
48
- const docRef = await addDoc(collection(db, "music_tracks"), {
49
- ...track,
50
- userId: user.uid,
51
- createdAt: new Date().toISOString()
52
- });
53
- return docRef.id;
54
- } catch (e) { console.error("Erreur save:", e); }
55
- };
56
-
57
- window.loadTracksFromFirestore = async () => {
58
- if (!user || !db) return [];
59
- try {
60
- const q = query(collection(db, "music_tracks"), where("userId", "==", user.uid), orderBy("createdAt", "desc"));
61
- const querySnapshot = await getDocs(q);
62
- const tracks = [];
63
- querySnapshot.forEach((doc) => {
64
- tracks.push({ id: doc.id, ...doc.data() });
65
- });
66
- return tracks;
67
- } catch (e) { console.error("Erreur load:", e); return []; }
68
- };
69
-
70
- window.updateTrackLike = async (trackId, liked) => {
71
- if (!user || !db) return;
72
- try {
73
- // Si l'ID est un timestamp (local), on ne peut pas update Firestore
74
- if (typeof trackId === 'string') {
75
- await updateDoc(doc(db, "music_tracks", trackId), { liked: liked });
76
- }
77
- } catch (e) { console.error("Erreur like:", e); }
78
- };
79
- </script>
80
- <style>
81
- :root {
82
- --bg-dark: #030014;
83
- --panel-bg: #0f0f1a;
84
- --primary: #8b5cf6;
85
- --primary-hover: #7c3aed;
86
- --text-main: #ffffff;
87
- --text-muted: #94a3b8;
88
- --border: rgba(255, 255, 255, 0.08);
89
- --player-height: 90px;
90
- }
91
-
92
- * { margin: 0; padding: 0; box-sizing: border-box; }
93
-
94
- body {
95
- background-color: var(--bg-dark);
96
- color: var(--text-main);
97
- font-family: 'Inter', sans-serif;
98
- height: 100vh;
99
- display: flex;
100
- flex-direction: column;
101
- overflow: hidden;
102
- }
103
-
104
- /* --- LOGIN SCREEN --- */
105
- .login-screen {
106
- position: fixed; top: 0; left: 0; width: 100%; height: 100%;
107
- background: var(--bg-dark);
108
- z-index: 1000;
109
- display: flex;
110
- align-items: center;
111
- justify-content: center;
112
- background-image:
113
- radial-gradient(circle at 50% 50%, rgba(139, 92, 246, 0.1), transparent 50%);
114
- }
115
-
116
- .login-card {
117
- width: 400px;
118
- padding: 2.5rem;
119
- background: rgba(15, 23, 42, 0.6);
120
- backdrop-filter: blur(20px);
121
- border: 1px solid var(--border);
122
- border-radius: 24px;
123
- text-align: center;
124
- box-shadow: 0 20px 50px rgba(0,0,0,0.5);
125
- }
126
-
127
- .login-logo {
128
- margin-bottom: 2rem;
129
- display: flex; justify-content: center; align-items: center; gap: 0.5rem;
130
- font-size: 1.5rem; font-weight: 700;
131
- }
132
- .login-logo svg { color: var(--primary); width: 32px; height: 32px; }
133
-
134
- .login-input {
135
- width: 100%;
136
- padding: 0.8rem 1rem;
137
- margin-bottom: 1rem;
138
- background: rgba(255,255,255,0.05);
139
- border: 1px solid var(--border);
140
- border-radius: 8px;
141
- color: white;
142
- font-size: 0.95rem;
143
- }
144
- .login-input:focus { outline: none; border-color: var(--primary); }
145
-
146
- .login-btn {
147
- width: 100%;
148
- padding: 0.8rem;
149
- background: var(--primary);
150
- color: black;
151
- border: none;
152
- border-radius: 8px;
153
- font-weight: 600;
154
- cursor: pointer;
155
- transition: background 0.2s;
156
- }
157
- .login-btn:hover { background: var(--primary-hover); color: white; }
158
-
159
- .login-error { color: #ef4444; font-size: 0.85rem; margin-top: 1rem; display: none; }
160
-
161
- /* --- MAIN LAYOUT (HIDDEN BY DEFAULT) --- */
162
- .app-container {
163
- display: none; /* Hidden until login */
164
- flex: 1;
165
- height: 100%;
166
- overflow: hidden;
167
- }
168
- .app-container.visible { display: flex; flex-direction: column; }
169
-
170
- .main-container { display: flex; flex: 1; overflow: hidden; }
171
-
172
- /* --- SIDEBAR --- */
173
- .sidebar {
174
- width: 240px;
175
- background-color: #000000;
176
- padding: 1.5rem;
177
- display: flex;
178
- flex-direction: column;
179
- gap: 2rem;
180
- border-right: 1px solid var(--border);
181
- }
182
-
183
- .logo {
184
- font-size: 1.25rem;
185
- font-weight: 700;
186
- display: flex;
187
- align-items: center;
188
- gap: 0.5rem;
189
- color: white;
190
- }
191
-
192
- .logo svg { color: var(--primary); }
193
-
194
- .nav-menu {
195
- display: flex;
196
- flex-direction: column;
197
- gap: 0.5rem;
198
- }
199
-
200
- .nav-item {
201
- display: flex;
202
- align-items: center;
203
- gap: 1rem;
204
- padding: 0.75rem 1rem;
205
- color: var(--text-muted);
206
- text-decoration: none;
207
- border-radius: 8px;
208
- transition: all 0.2s;
209
- font-weight: 500;
210
- font-size: 0.9rem;
211
- cursor: pointer;
212
- }
213
-
214
- .nav-item:hover, .nav-item.active {
215
- color: white;
216
- background-color: rgba(255, 255, 255, 0.05);
217
- }
218
-
219
- .nav-item.active svg { color: var(--primary); }
220
-
221
- /* --- CONTENT AREA --- */
222
- .content {
223
- flex: 1;
224
- background: linear-gradient(180deg, #1e1b4b 0%, var(--bg-dark) 40%);
225
- padding: 2rem;
226
- overflow-y: auto;
227
- display: flex;
228
- flex-direction: column;
229
- gap: 2rem;
230
- position: relative;
231
- }
232
-
233
- .view-section { display: none; flex-direction: column; gap: 2rem; height: 100%; }
234
- .view-section.active { display: flex; }
235
-
236
- .header {
237
- display: flex;
238
- justify-content: space-between;
239
- align-items: center;
240
- margin-bottom: 1rem;
241
- }
242
-
243
- .user-pill {
244
- background: rgba(0, 0, 0, 0.5);
245
- padding: 4px 12px 4px 4px;
246
- border-radius: 20px;
247
- display: flex;
248
- align-items: center;
249
- gap: 8px;
250
- font-size: 0.85rem;
251
- font-weight: 600;
252
- cursor: pointer;
253
- }
254
-
255
- .user-avatar {
256
- width: 28px; height: 28px;
257
- background: var(--primary);
258
- border-radius: 50%;
259
- display: flex; align-items: center; justify-content: center;
260
- }
261
-
262
- /* --- GENERATION CARD (HOME) --- */
263
- .generation-card {
264
- display: flex;
265
- gap: 2rem;
266
- align-items: flex-end;
267
- padding-bottom: 2rem;
268
- }
269
-
270
- .cover-art {
271
- width: 232px;
272
- height: 232px;
273
- background: linear-gradient(135deg, #4c1d95, #db2777);
274
- box-shadow: 0 8px 40px rgba(0,0,0,0.5);
275
- display: flex;
276
- align-items: center;
277
- justify-content: center;
278
- position: relative;
279
- overflow: hidden;
280
- }
281
-
282
- .cover-art img {
283
- width: 100%; height: 100%; object-fit: cover;
284
- opacity: 0.8;
285
- }
286
-
287
- .gen-info {
288
- flex: 1;
289
- display: flex;
290
- flex-direction: column;
291
- gap: 1rem;
292
- }
293
-
294
- .gen-type {
295
- font-size: 0.8rem;
296
- font-weight: 700;
297
- text-transform: uppercase;
298
- letter-spacing: 1px;
299
- color: white;
300
- }
301
-
302
- .gen-title {
303
- font-size: 4rem;
304
- font-weight: 900;
305
- line-height: 1;
306
- margin: 0.5rem 0;
307
- letter-spacing: -2px;
308
- }
309
-
310
- .gen-desc {
311
- color: var(--text-muted);
312
- font-size: 1rem;
313
- max-width: 600px;
314
- }
315
-
316
- .input-area {
317
- margin-top: 1rem;
318
- display: flex;
319
- flex-direction: column;
320
- gap: 1rem;
321
- width: 100%;
322
- max-width: 600px;
323
- }
324
-
325
- .prompt-input {
326
- background: rgba(255,255,255,0.1);
327
- border: 1px solid transparent;
328
- border-radius: 4px;
329
- padding: 1rem;
330
- color: white;
331
- font-family: inherit;
332
- font-size: 1rem;
333
- resize: none;
334
- height: 60px;
335
- transition: all 0.2s;
336
- }
337
-
338
- .prompt-input:focus {
339
- outline: none;
340
- background: rgba(255,255,255,0.15);
341
- border-color: rgba(255,255,255,0.2);
342
- }
343
-
344
- .duration-slider {
345
- display: flex;
346
- align-items: center;
347
- gap: 1rem;
348
- color: var(--text-muted);
349
- font-size: 0.85rem;
350
- }
351
-
352
- input[type="range"] {
353
- flex: 1;
354
- height: 4px;
355
- background: rgba(255,255,255,0.2);
356
- border-radius: 2px;
357
- -webkit-appearance: none;
358
- }
359
-
360
- input[type="range"]::-webkit-slider-thumb {
361
- -webkit-appearance: none;
362
- width: 12px; height: 12px;
363
- background: white;
364
- border-radius: 50%;
365
- cursor: pointer;
366
- opacity: 0;
367
- transition: opacity 0.2s;
368
- }
369
-
370
- .duration-slider:hover input[type="range"]::-webkit-slider-thumb { opacity: 1; }
371
-
372
- .action-buttons {
373
- display: flex;
374
- gap: 1rem;
375
- margin-top: 1rem;
376
- position: relative;
377
- }
378
-
379
- .btn-play {
380
- width: 56px; height: 56px;
381
- border-radius: 50%;
382
- background: var(--primary);
383
- border: none;
384
- color: black;
385
- display: flex; align-items: center; justify-content: center;
386
- cursor: pointer;
387
- transition: transform 0.2s, background 0.2s;
388
- }
389
-
390
- .btn-play:hover { transform: scale(1.05); background: var(--primary-hover); }
391
- .btn-play:disabled { background: #333; cursor: not-allowed; transform: none; }
392
-
393
- /* --- LIBRARY & SEARCH --- */
394
- .track-list {
395
- display: flex;
396
- flex-direction: column;
397
- gap: 0.5rem;
398
- }
399
-
400
- .track-item {
401
- display: flex;
402
- align-items: center;
403
- padding: 0.75rem 1rem;
404
- border-radius: 4px;
405
- cursor: pointer;
406
- transition: background 0.2s;
407
- gap: 1rem;
408
- }
409
-
410
- .track-item:hover { background: rgba(255,255,255,0.1); }
411
-
412
- .track-item-img {
413
- width: 40px; height: 40px;
414
- background: #333;
415
- border-radius: 4px;
416
- overflow: hidden;
417
- }
418
- .track-item-img img { width: 100%; height: 100%; object-fit: cover; }
419
-
420
- .track-item-info { flex: 1; }
421
- .track-item-title { font-weight: 600; font-size: 0.95rem; color: white; }
422
- .track-item-desc { font-size: 0.8rem; color: var(--text-muted); }
423
- .track-item-duration { font-size: 0.85rem; color: var(--text-muted); }
424
-
425
- .search-bar {
426
- background: rgba(255,255,255,0.1);
427
- border: none;
428
- border-radius: 20px;
429
- padding: 0.75rem 1.5rem;
430
- color: white;
431
- width: 100%;
432
- max-width: 400px;
433
- font-size: 0.9rem;
434
- margin-bottom: 2rem;
435
- }
436
-
437
- /* --- MENU CONTEXTUEL --- */
438
- .context-menu {
439
- position: absolute;
440
- top: 100%; left: 0;
441
- background: #282828;
442
- border-radius: 4px;
443
- padding: 4px;
444
- box-shadow: 0 4px 12px rgba(0,0,0,0.5);
445
- display: none;
446
- flex-direction: column;
447
- min-width: 160px;
448
- z-index: 200;
449
- }
450
-
451
- .context-menu.show { display: flex; }
452
-
453
- .context-item {
454
- padding: 10px 12px;
455
- font-size: 0.85rem;
456
- color: #eaeaea;
457
- cursor: pointer;
458
- border-radius: 2px;
459
- text-align: left;
460
- background: none; border: none;
461
- }
462
-
463
- .context-item:hover { background: #3e3e3e; }
464
-
465
- /* --- PLAYER BAR --- */
466
- .player-bar {
467
- height: var(--player-height);
468
- background-color: #000000;
469
- border-top: 1px solid var(--border);
470
- padding: 0 1.5rem;
471
- display: flex;
472
- align-items: center;
473
- justify-content: space-between;
474
- z-index: 100;
475
- }
476
-
477
- .track-info {
478
- display: flex;
479
- align-items: center;
480
- gap: 1rem;
481
- width: 30%;
482
- }
483
-
484
- .track-cover {
485
- width: 56px; height: 56px;
486
- background: #333;
487
- border-radius: 4px;
488
- overflow: hidden;
489
- }
490
-
491
- .track-cover img { width: 100%; height: 100%; object-fit: cover; }
492
-
493
- .track-details {
494
- display: flex;
495
- flex-direction: column;
496
- justify-content: center;
497
- }
498
-
499
- .track-name { font-size: 0.9rem; font-weight: 600; color: white; }
500
- .track-artist { font-size: 0.75rem; color: var(--text-muted); }
501
-
502
- .player-controls {
503
- display: flex;
504
- flex-direction: column;
505
- align-items: center;
506
- gap: 0.5rem;
507
- width: 40%;
508
- }
509
-
510
- .control-buttons {
511
- display: flex;
512
- align-items: center;
513
- gap: 1.5rem;
514
- }
515
-
516
- .ctrl-btn {
517
- background: none; border: none; color: var(--text-muted); cursor: pointer; transition: color 0.2s;
518
- }
519
- .ctrl-btn:hover { color: white; }
520
- .ctrl-btn.active { color: var(--primary); }
521
- .ctrl-btn.main {
522
- width: 32px; height: 32px;
523
- background: white; color: black;
524
- border-radius: 50%;
525
- display: flex; align-items: center; justify-content: center;
526
- }
527
- .ctrl-btn.main:hover { transform: scale(1.05); color: black; }
528
-
529
- .progress-container {
530
- width: 100%;
531
- display: flex;
532
- align-items: center;
533
- gap: 0.5rem;
534
- font-size: 0.7rem;
535
- color: var(--text-muted);
536
- }
537
-
538
- .progress-bar {
539
- flex: 1;
540
- height: 4px;
541
- background: rgba(255,255,255,0.1);
542
- border-radius: 2px;
543
- position: relative;
544
- overflow: hidden;
545
- }
546
-
547
- .progress-fill {
548
- height: 100%;
549
- background: white;
550
- width: 0%;
551
- border-radius: 2px;
552
- }
553
-
554
- .progress-fill.generating {
555
- background: var(--primary);
556
- animation: loading 1.5s infinite ease-in-out;
557
- width: 30%;
558
- }
559
-
560
- @keyframes loading {
561
- 0% { transform: translateX(-100%); }
562
- 100% { transform: translateX(400%); }
563
- }
564
-
565
- .volume-controls {
566
- width: 30%;
567
- display: flex;
568
- justify-content: flex-end;
569
- align-items: center;
570
- gap: 0.5rem;
571
- }
572
-
573
- /* --- VISUALIZER --- */
574
- #visualizer {
575
- position: absolute;
576
- bottom: 0; left: 0; width: 100%; height: 100%;
577
- opacity: 0.1;
578
- pointer-events: none;
579
- z-index: 0;
580
- }
581
-
582
- </style>
583
- </head>
584
- <body>
585
-
586
- <!-- LOGIN SCREEN -->
587
- <div id="login-screen" class="login-screen">
588
- <div class="login-card">
589
- <div class="login-logo">
590
- <i data-lucide="music"></i> Cygnis Music
591
- </div>
592
- <p style="color:#94a3b8; margin-bottom:1.5rem;">Connexion en cours...</p>
593
- <div class="spinner" style="width:30px;height:30px;border:3px solid #8b5cf6;border-top-color:transparent;border-radius:50%;animation:spin 1s linear infinite;margin:0 auto;"></div>
594
- <div id="login-error" class="login-error"></div>
595
- </div>
596
- </div>
597
-
598
- <!-- APP CONTAINER -->
599
- <div id="app-container" class="app-container">
600
- <div class="main-container">
601
- <!-- SIDEBAR -->
602
- <aside class="sidebar">
603
- <div class="logo">
604
- <i data-lucide="music"></i> Cygnis Music
605
- </div>
606
- <nav class="nav-menu">
607
- <div class="nav-item active" onclick="showView('home')"><i data-lucide="home"></i> Accueil</div>
608
- <div class="nav-item" onclick="showView('search')"><i data-lucide="search"></i> Rechercher</div>
609
- <div class="nav-item" onclick="showView('library')"><i data-lucide="library"></i> Bibliothèque</div>
610
- </nav>
611
- <div style="margin-top: 1rem; border-top: 1px solid var(--border); padding-top: 1rem;">
612
- <div class="nav-item" onclick="showView('likes')"><i data-lucide="heart"></i> Titres likés</div>
613
- </div>
614
- </aside>
615
-
616
- <!-- CONTENT -->
617
- <main class="content">
618
- <canvas id="visualizer"></canvas>
619
-
620
- <div class="header">
621
- <div style="display:flex; gap:1rem;">
622
- <button style="background:rgba(0,0,0,0.3); border:none; border-radius:50%; width:32px; height:32px; color:white; cursor:pointer;"><i data-lucide="chevron-left"></i></button>
623
- <button style="background:rgba(0,0,0,0.3); border:none; border-radius:50%; width:32px; height:32px; color:white; cursor:pointer;"><i data-lucide="chevron-right"></i></button>
624
- </div>
625
- <div class="user-pill">
626
- <div class="user-avatar"><i data-lucide="user" size="16"></i></div>
627
- <span id="user-name">Utilisateur</span>
628
- </div>
629
- </div>
630
-
631
- <!-- VIEW: HOME -->
632
- <div id="view-home" class="view-section active">
633
- <div class="generation-card">
634
- <div class="cover-art" id="cover-art">
635
- <i data-lucide="music" size="64" color="white"></i>
636
- </div>
637
- <div class="gen-info">
638
- <div class="gen-type">Génération IA</div>
639
- <h1 class="gen-title" id="track-title">Nouvelle Piste</h1>
640
- <div class="gen-desc">Créez une musique unique en décrivant l'ambiance, le style et les instruments.</div>
641
-
642
- <div class="input-area">
643
- <textarea id="prompt" class="prompt-input" placeholder="Ex: Piano mélancolique sous la pluie, lo-fi hip hop..."></textarea>
644
-
645
- <div class="duration-slider">
646
- <span>Durée</span>
647
- <input type="range" id="duration" min="5" max="30" value="10" step="5">
648
- <span id="duration-val">10s</span>
649
- </div>
650
- </div>
651
-
652
- <div class="action-buttons">
653
- <button id="generate-btn" class="btn-play">
654
- <i data-lucide="play" fill="black"></i>
655
- </button>
656
- <button class="ctrl-btn" id="like-btn" style="font-size:2rem;"><i data-lucide="heart"></i></button>
657
- <button class="ctrl-btn" id="more-btn" style="font-size:2rem;"><i data-lucide="more-horizontal"></i></button>
658
-
659
- <!-- Context Menu -->
660
- <div class="context-menu" id="context-menu">
661
- <button class="context-item" id="download-btn">Télécharger</button>
662
- </div>
663
- </div>
664
- </div>
665
- </div>
666
- </div>
667
-
668
- <!-- VIEW: LIBRARY / LIKES / SEARCH -->
669
- <div id="view-list" class="view-section">
670
- <h1 id="list-title" style="font-size: 2rem; font-weight: 700; margin-bottom: 1rem;">Bibliothèque</h1>
671
- <input type="text" id="search-input" class="search-bar" placeholder="Rechercher dans vos titres..." style="display:none;">
672
- <div class="track-list" id="track-list">
673
- <!-- Tracks will be injected here -->
674
- </div>
675
- </div>
676
-
677
- </main>
678
- </div>
679
-
680
- <!-- PLAYER BAR -->
681
- <div class="player-bar">
682
- <div class="track-info">
683
- <div class="track-cover">
684
- <img src="https://images.unsplash.com/photo-1614613535308-eb5fbd3d2c17?q=80&w=100" alt="Cover" id="mini-cover">
685
- </div>
686
- <div class="track-details">
687
- <div class="track-name" id="player-title">En attente...</div>
688
- <div class="track-artist">Cygnis AI</div>
689
- </div>
690
- <button class="ctrl-btn" id="player-like-btn"><i data-lucide="heart" size="16"></i></button>
691
- </div>
692
-
693
- <div class="player-controls">
694
- <div class="control-buttons">
695
- <button class="ctrl-btn"><i data-lucide="shuffle" size="16"></i></button>
696
- <button class="ctrl-btn"><i data-lucide="skip-back" size="20"></i></button>
697
- <button class="ctrl-btn main" id="play-pause-btn"><i data-lucide="play" fill="black" size="16"></i></button>
698
- <button class="ctrl-btn"><i data-lucide="skip-forward" size="20"></i></button>
699
- <button class="ctrl-btn"><i data-lucide="repeat" size="16"></i></button>
700
- </div>
701
- <div class="progress-container">
702
- <span id="current-time">0:00</span>
703
- <div class="progress-bar">
704
- <div class="progress-fill" id="progress-fill"></div>
705
- </div>
706
- <span id="total-time">0:00</span>
707
- </div>
708
- </div>
709
-
710
- <div class="volume-controls">
711
- <i data-lucide="mic-2" size="16" class="ctrl-btn"></i>
712
- <i data-lucide="list-music" size="16" class="ctrl-btn"></i>
713
- <i data-lucide="speaker" size="16" class="ctrl-btn"></i>
714
- <div style="width:100px; height:4px; background:rgba(255,255,255,0.3); border-radius:2px; position:relative;">
715
- <div style="width:70%; height:100%; background:white; border-radius:2px;"></div>
716
- </div>
717
- </div>
718
- </div>
719
- </div>
720
-
721
- <audio id="audio-player" crossorigin="anonymous"></audio>
722
-
723
- <script>
724
- lucide.createIcons();
725
-
726
- // --- STATE ---
727
- let tracks = [];
728
- let currentTrack = null;
729
- let isGenerating = false;
730
- let audioContext, analyser, source;
731
- let currentUser = null;
732
-
733
- // --- DOM ELEMENTS ---
734
- const loginScreen = document.getElementById('login-screen');
735
- const appContainer = document.getElementById('app-container');
736
- const loginError = document.getElementById('login-error');
737
- const userNameEl = document.getElementById('user-name');
738
-
739
- const promptInput = document.getElementById('prompt');
740
- const durationInput = document.getElementById('duration');
741
- const durationVal = document.getElementById('duration-val');
742
- const generateBtn = document.getElementById('generate-btn');
743
- const playPauseBtn = document.getElementById('play-pause-btn');
744
- const audioPlayer = document.getElementById('audio-player');
745
- const progressFill = document.getElementById('progress-fill');
746
- const trackTitle = document.getElementById('track-title');
747
- const playerTitle = document.getElementById('player-title');
748
- const currentTimeEl = document.getElementById('current-time');
749
- const totalTimeEl = document.getElementById('total-time');
750
- const canvas = document.getElementById('visualizer');
751
- const ctx = canvas.getContext('2d');
752
- const likeBtn = document.getElementById('like-btn');
753
- const playerLikeBtn = document.getElementById('player-like-btn');
754
- const moreBtn = document.getElementById('more-btn');
755
- const contextMenu = document.getElementById('context-menu');
756
- const downloadBtn = document.getElementById('download-btn');
757
- const trackListEl = document.getElementById('track-list');
758
- const searchInput = document.getElementById('search-input');
759
- const listTitle = document.getElementById('list-title');
760
-
761
- // --- AUTHENTICATION & FIRESTORE ---
762
- async function loadTracks() {
763
- if (window.loadTracksFromFirestore) {
764
- const firestoreTracks = await window.loadTracksFromFirestore();
765
- if (firestoreTracks.length > 0) {
766
- tracks = firestoreTracks;
767
- } else {
768
- // Fallback local storage si vide
769
- const savedTracks = localStorage.getItem(`cygnis_tracks_${currentUser.uid}`);
770
- if (savedTracks) tracks = JSON.parse(savedTracks);
771
- }
772
- }
773
- }
774
-
775
- // --- INITIALIZATION ---
776
- durationInput.addEventListener('input', (e) => durationVal.textContent = e.target.value + 's');
777
-
778
- // --- NAVIGATION ---
779
- window.showView = (view) => {
780
- document.querySelectorAll('.view-section').forEach(el => el.classList.remove('active'));
781
- document.querySelectorAll('.nav-item').forEach(el => el.classList.remove('active'));
782
-
783
- if (view === 'home') {
784
- document.getElementById('view-home').classList.add('active');
785
- document.querySelector('.nav-item:nth-child(1)').classList.add('active');
786
- } else {
787
- document.getElementById('view-list').classList.add('active');
788
- searchInput.style.display = view === 'search' ? 'block' : 'none';
789
-
790
- if (view === 'search') {
791
- listTitle.textContent = "Rechercher";
792
- document.querySelector('.nav-item:nth-child(2)').classList.add('active');
793
- renderTracks(tracks);
794
- } else if (view === 'library') {
795
- listTitle.textContent = "Bibliothèque";
796
- document.querySelector('.nav-item:nth-child(3)').classList.add('active');
797
- renderTracks(tracks);
798
- } else if (view === 'likes') {
799
- listTitle.textContent = "Titres Likés";
800
- renderTracks(tracks.filter(t => t.liked));
801
- }
802
- }
803
- };
804
-
805
- // --- AUDIO VISUALIZER ---
806
- function initAudio() {
807
- if (!audioContext) {
808
- audioContext = new (window.AudioContext || window.webkitAudioContext)();
809
- analyser = audioContext.createAnalyser();
810
- source = audioContext.createMediaElementSource(audioPlayer);
811
- source.connect(analyser);
812
- analyser.connect(audioContext.destination);
813
- analyser.fftSize = 256;
814
- drawVisualizer();
815
- }
816
- }
817
-
818
- function drawVisualizer() {
819
- requestAnimationFrame(drawVisualizer);
820
- const bufferLength = analyser.frequencyBinCount;
821
- const dataArray = new Uint8Array(bufferLength);
822
- analyser.getByteFrequencyData(dataArray);
823
-
824
- ctx.clearRect(0, 0, canvas.width, canvas.height);
825
- const barWidth = (canvas.width / bufferLength) * 2.5;
826
- let barHeight;
827
- let x = 0;
828
-
829
- for(let i = 0; i < bufferLength; i++) {
830
- barHeight = dataArray[i] * 2;
831
- const gradient = ctx.createLinearGradient(0, canvas.height, 0, canvas.height - barHeight);
832
- gradient.addColorStop(0, 'rgba(139, 92, 246, 0)');
833
- gradient.addColorStop(1, 'rgba(139, 92, 246, 0.2)');
834
- ctx.fillStyle = gradient;
835
- ctx.fillRect(x, canvas.height - barHeight, barWidth, barHeight);
836
- x += barWidth + 1;
837
- }
838
- }
839
-
840
- // --- PLAYER LOGIC ---
841
- const togglePlay = () => {
842
- if (audioPlayer.src) {
843
- if (audioPlayer.paused) {
844
- audioPlayer.play();
845
- playPauseBtn.innerHTML = '<i data-lucide="pause" fill="black" size="16"></i>';
846
- lucide.createIcons();
847
- initAudio();
848
- } else {
849
- audioPlayer.pause();
850
- playPauseBtn.innerHTML = '<i data-lucide="play" fill="black" size="16"></i>';
851
- lucide.createIcons();
852
- }
853
- }
854
- };
855
-
856
- playPauseBtn.addEventListener('click', togglePlay);
857
-
858
- audioPlayer.ontimeupdate = () => {
859
- const progress = (audioPlayer.currentTime / audioPlayer.duration) * 100;
860
- progressFill.style.width = progress + '%';
861
- currentTimeEl.textContent = formatTime(audioPlayer.currentTime);
862
- };
863
-
864
- audioPlayer.onloadedmetadata = () => {
865
- totalTimeEl.textContent = formatTime(audioPlayer.duration);
866
- };
867
-
868
- audioPlayer.onended = () => {
869
- playPauseBtn.innerHTML = '<i data-lucide="play" fill="black" size="16"></i>';
870
- lucide.createIcons();
871
- progressFill.style.width = '0%';
872
- };
873
-
874
- function formatTime(seconds) {
875
- if (!seconds) return "0:00";
876
- const m = Math.floor(seconds / 60);
877
- const s = Math.floor(seconds % 60);
878
- return `${m}:${s < 10 ? '0' : ''}${s}`;
879
- }
880
-
881
- // --- GENERATION LOGIC ---
882
- generateBtn.addEventListener('click', async () => {
883
- const text = promptInput.value;
884
- const duration = durationInput.value;
885
-
886
- if (!text || isGenerating) return;
887
-
888
- isGenerating = true;
889
- generateBtn.disabled = true;
890
- generateBtn.innerHTML = '<div style="width:20px;height:20px;border:2px solid black;border-top-color:transparent;border-radius:50%;animation:spin 1s linear infinite;"></div>';
891
-
892
- progressFill.classList.add('generating');
893
- playerTitle.textContent = "Génération en cours...";
894
- trackTitle.textContent = "Composition...";
895
-
896
- try {
897
- const response = await fetch('/generate', {
898
- method: 'POST',
899
- headers: { 'Content-Type': 'application/json' },
900
- body: JSON.stringify({ prompt: text, duration: parseInt(duration) })
901
- });
902
-
903
- if (!response.ok) throw new Error("Erreur serveur");
904
-
905
- const data = await response.json();
906
-
907
- if (data.audio_url) {
908
- const newTrack = {
909
- title: text,
910
- url: data.audio_url,
911
- duration: duration,
912
- liked: false,
913
- date: new Date().toLocaleDateString()
914
- };
915
-
916
- // Sauvegarde Firestore
917
- if (window.saveTrackToFirestore) {
918
- const id = await window.saveTrackToFirestore(newTrack);
919
- newTrack.id = id;
920
- } else {
921
- newTrack.id = Date.now();
922
- }
923
-
924
- tracks.unshift(newTrack);
925
- currentTrack = newTrack;
926
-
927
- // Play
928
- audioPlayer.src = data.audio_url;
929
- audioPlayer.play();
930
-
931
- // Update UI
932
- trackTitle.textContent = text;
933
- playerTitle.textContent = text;
934
- playPauseBtn.innerHTML = '<i data-lucide="pause" fill="black" size="16"></i>';
935
- lucide.createIcons();
936
-
937
- // Send to parent
938
- window.parent.postMessage({
939
- type: 'MUSIC_GENERATED',
940
- audioUrl: window.location.origin + data.audio_url,
941
- prompt: text
942
- }, '*');
943
- }
944
-
945
- } catch (error) {
946
- console.error(error);
947
- playerTitle.textContent = "Erreur de génération";
948
- trackTitle.textContent = "Erreur";
949
- } finally {
950
- isGenerating = false;
951
- generateBtn.disabled = false;
952
- generateBtn.innerHTML = '<i data-lucide="play" fill="black"></i>';
953
- lucide.createIcons();
954
- progressFill.classList.remove('generating');
955
- progressFill.style.width = '0%';
956
- }
957
- });
958
-
959
- // --- LIBRARY LOGIC ---
960
- function renderTracks(trackList) {
961
- trackListEl.innerHTML = trackList.map(track => `
962
- <div class="track-item" onclick="playTrack('${track.id}')">
963
- <div class="track-item-img">
964
- <img src="https://images.unsplash.com/photo-1614613535308-eb5fbd3d2c17?q=80&w=100" alt="Cover">
965
- </div>
966
- <div class="track-item-info">
967
- <div class="track-item-title">${track.title}</div>
968
- <div class="track-item-desc">Généré le ${track.date}</div>
969
- </div>
970
- <div class="track-item-duration">${track.duration}s</div>
971
- <div onclick="event.stopPropagation(); toggleLike('${track.id}')">
972
- <i data-lucide="heart" class="${track.liked ? 'text-primary fill-primary' : 'text-muted'}" size="16"></i>
973
- </div>
974
- </div>
975
- `).join('');
976
- lucide.createIcons();
977
- }
978
-
979
- window.playTrack = (id) => {
980
- const track = tracks.find(t => t.id == id);
981
- if (track) {
982
- currentTrack = track;
983
- audioPlayer.src = track.url;
984
- audioPlayer.play();
985
- playerTitle.textContent = track.title;
986
- trackTitle.textContent = track.title;
987
- playPauseBtn.innerHTML = '<i data-lucide="pause" fill="black" size="16"></i>';
988
- updateLikeButtons();
989
- lucide.createIcons();
990
- initAudio();
991
- }
992
- };
993
-
994
- window.toggleLike = (id) => {
995
- const track = id ? tracks.find(t => t.id == id) : currentTrack;
996
- if (track) {
997
- track.liked = !track.liked;
998
- updateLikeButtons();
999
-
1000
- if (window.updateTrackLike) {
1001
- window.updateTrackLike(track.id, track.liked);
1002
- }
1003
-
1004
- if (document.getElementById('view-list').classList.contains('active')) {
1005
- const currentView = listTitle.textContent === "Titres Likés" ? 'likes' : 'library';
1006
- if (currentView === 'likes') renderTracks(tracks.filter(t => t.liked));
1007
- else renderTracks(tracks);
1008
- }
1009
- }
1010
- };
1011
-
1012
- function updateLikeButtons() {
1013
- if (!currentTrack) return;
1014
- const isLiked = currentTrack.liked;
1015
- const iconClass = isLiked ? 'text-primary fill-primary' : 'text-white';
1016
- likeBtn.innerHTML = `<i data-lucide="heart" class="${iconClass}"></i>`;
1017
- playerLikeBtn.innerHTML = `<i data-lucide="heart" class="${iconClass}" size="16"></i>`;
1018
- lucide.createIcons();
1019
- }
1020
-
1021
- likeBtn.addEventListener('click', () => toggleLike());
1022
- playerLikeBtn.addEventListener('click', () => toggleLike());
1023
-
1024
- searchInput.addEventListener('input', (e) => {
1025
- const query = e.target.value.toLowerCase();
1026
- const filtered = tracks.filter(t => t.title.toLowerCase().includes(query));
1027
- renderTracks(filtered);
1028
- });
1029
-
1030
- moreBtn.addEventListener('click', () => {
1031
- contextMenu.classList.toggle('show');
1032
- });
1033
-
1034
- downloadBtn.addEventListener('click', () => {
1035
- if (currentTrack) {
1036
- const a = document.createElement('a');
1037
- a.href = currentTrack.url;
1038
- a.download = `Cygnis_Music_${currentTrack.title}.wav`;
1039
- document.body.appendChild(a);
1040
- a.click();
1041
- document.body.removeChild(a);
1042
- }
1043
- contextMenu.classList.remove('show');
1044
- });
1045
-
1046
- document.addEventListener('click', (e) => {
1047
- if (!moreBtn.contains(e.target) && !contextMenu.contains(e.target)) {
1048
- contextMenu.classList.remove('show');
1049
- }
1050
- });
1051
-
1052
- function resize() {
1053
- canvas.width = canvas.parentElement.clientWidth;
1054
- canvas.height = canvas.parentElement.clientHeight;
1055
- }
1056
- window.addEventListener('resize', resize);
1057
- resize();
1058
-
1059
- const style = document.createElement('style');
1060
- style.innerHTML = '@keyframes spin { 0% { transform: rotate(0deg); } 100% { transform: rotate(360deg); } } .text-primary { color: #8b5cf6; } .fill-primary { fill: #8b5cf6; }';
1061
- document.head.appendChild(style);
1062
-
1063
- </script>
1064
- </body>
1065
- </html>
 
1
+ import uvicorn
2
+ from fastapi import FastAPI, HTTPException
3
+ from fastapi.staticfiles import StaticFiles
4
+ from fastapi.responses import FileResponse
5
+ from pydantic import BaseModel
6
+ from transformers import AutoProcessor, MusicgenForConditionalGeneration
7
+ import scipy.io.wavfile as wavfile
8
+ import torch
9
+ import numpy as np
10
+ import os
11
+ import uuid
12
+
13
+ # --- CONFIGURATION ---
14
+ app = FastAPI()
15
+ device = "cuda" if torch.cuda.is_available() else "cpu"
16
+ print(f"🚀 Chargement du modèle MusicGen sur {device}...")
17
+
18
+ # Chargement du modèle au démarrage
19
+ try:
20
+ processor = AutoProcessor.from_pretrained("facebook/musicgen-small")
21
+ model = MusicgenForConditionalGeneration.from_pretrained("facebook/musicgen-small").to(device)
22
+ print("✅ Modèle chargé avec succès.")
23
+ except Exception as e:
24
+ print(f"❌ Erreur critique chargement modèle: {e}")
25
+ model = None
26
+
27
+ class MusicRequest(BaseModel):
28
+ prompt: str
29
+ duration: int = 10
30
+
31
+ @app.post("/generate")
32
+ async def generate_music(request: MusicRequest):
33
+ print(f"🎵 Génération : {request.prompt} ({request.duration}s)")
34
+
35
+ if not model:
36
+ raise HTTPException(status_code=500, detail="Modèle non chargé")
37
+
38
+ try:
39
+ # Configuration
40
+ duration = min(request.duration, 30) # Max 30s
41
+ max_new_tokens = int(duration * 50)
42
+
43
+ inputs = processor(
44
+ text=[request.prompt],
45
+ padding=True,
46
+ return_tensors="pt",
47
+ ).to(device)
48
+
49
+ # Génération
50
+ audio_values = model.generate(**inputs, max_new_tokens=max_new_tokens)
51
+
52
+ # Sauvegarde
53
+ sampling_rate = model.config.audio_encoder.sampling_rate
54
+ audio_data = audio_values[0, 0].cpu().numpy()
55
+
56
+ filename = f"music_{uuid.uuid4()}.wav"
57
+ wavfile.write(filename, sampling_rate, audio_data)
58
+
59
+ return {"audio_url": f"/{filename}"}
60
+
61
+ except Exception as e:
62
+ print(f"❌ Erreur génération : {str(e)}")
63
+ raise HTTPException(status_code=500, detail=str(e))
64
+
65
+ @app.get("/")
66
+ async def read_index():
67
+ return FileResponse('index.html')
68
+
69
+ # Servir les fichiers statiques (y compris les fichiers audio générés)
70
+ app.mount("/", StaticFiles(directory=".", html=True), name="static")
71
+
72
+ if __name__ == "__main__":
73
+ uvicorn.run(app, host="0.0.0.0", port=7860)