flen-crypto commited on
Commit
07b0db1
·
verified ·
1 Parent(s): 87ca0d5

Upload folder using huggingface_hub

Browse files
Files changed (1) hide show
  1. index.html +364 -1067
index.html CHANGED
@@ -1,1150 +1,447 @@
1
- <!DOCTYPE html>
2
- <html lang="en">
3
- <head>
4
- <meta charset="UTF-8">
5
- <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
- <title>Mr.FLEN — Fiverr Music Creator (Local)</title>
7
- <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css">
8
- <style>
9
- :root {
10
- --bg: #0f1220;
11
- --card: #151a2e;
12
- --text: #e8ecff;
13
- --muted: #aab3da;
14
- --accent: #4a68d8;
15
- --success: #2dd4bf;
16
- --warn: #ffb400;
17
- --danger: #ff5a7a;
18
- --border: #263055;
19
- --radius: 14px;
20
- }
21
-
22
- * {
23
- box-sizing: border-box;
24
- margin: 0;
25
- padding: 0;
26
- }
27
-
28
- body {
29
- margin: 0;
30
- font-family: system-ui, -apple-system, "Segoe UI", Roboto, sans-serif;
31
- background: linear-gradient(180deg, #0b0d17, var(--bg));
32
- color: var(--text);
33
- }
34
-
35
- header {
36
- padding: 18px 16px 10px;
37
- border-bottom: 1px solid var(--border);
38
- background: radial-gradient(900px 260px at 20% 0%, rgba(74, 104, 216, 0.35), transparent 60%);
39
- }
40
-
41
- h1 {
42
- margin: 0;
43
- font-size: 1.25rem;
44
- }
45
-
46
- header p {
47
- margin: 6px 0 0;
48
- color: var(--muted);
49
- max-width: 1100px;
50
- }
51
-
52
- main {
53
- max-width: 1280px;
54
- margin: 0 auto;
55
- padding: 16px;
56
- display: grid;
57
- grid-template-columns: 1.15fr 0.85fr;
58
- gap: 16px;
59
- }
60
-
61
- @media (max-width: 980px) {
62
- main {
63
- grid-template-columns: 1fr;
64
  }
65
- }
66
-
67
- .card {
68
- background: var(--card);
69
- border: 1px solid var(--border);
70
- border-radius: var(--radius);
71
- padding: 14px;
72
- box-shadow: 0 10px 20px rgba(0, 0, 0, 0.35);
73
- }
74
-
75
- .card h2 {
76
- margin: 0 0 10px;
77
- font-size: 1.05rem;
78
- }
79
-
80
- .grid2 {
81
- display: grid;
82
- grid-template-columns: 1fr 1fr;
83
- gap: 10px;
84
- }
85
 
86
- @media (max-width: 680px) {
87
- .grid2 {
88
- grid-template-columns: 1fr;
89
  }
90
- }
91
-
92
- label {
93
- display: block;
94
- font-size: 0.82rem;
95
- color: var(--muted);
96
- margin: 8px 0 4px;
97
- }
98
-
99
- input, textarea, select {
100
- width: 100%;
101
- padding: 10px;
102
- border-radius: 10px;
103
- border: 1px solid var(--border);
104
- background: #0f1430;
105
- color: var(--text);
106
- font-size: 0.92rem;
107
- }
108
-
109
- textarea {
110
- min-height: 84px;
111
- resize: vertical;
112
- }
113
-
114
- .btnrow {
115
- display: flex;
116
- flex-wrap: wrap;
117
- gap: 10px;
118
- margin-top: 12px;
119
- }
120
-
121
- button {
122
- cursor: pointer;
123
- border: 0;
124
- border-radius: 999px;
125
- padding: 10px 14px;
126
- font-weight: 650;
127
- font-size: 0.92rem;
128
- display: inline-flex;
129
- align-items: center;
130
- gap: 6px;
131
- user-select: none;
132
- }
133
-
134
- .primary {
135
- background: var(--accent);
136
- color: #fff;
137
- }
138
-
139
- .ghost {
140
- background: transparent;
141
- color: var(--text);
142
- border: 1px solid var(--border);
143
- }
144
-
145
- .success {
146
- background: var(--success);
147
- color: #0c4641;
148
- border: 1px solid rgba(45, 212, 191, 0.35);
149
- }
150
-
151
- .warn {
152
- background: rgba(255, 174, 0, 0.16);
153
- color: #ffe2a8;
154
- border: 1px solid rgba(255, 174, 0, 0.35);
155
- }
156
-
157
- .danger {
158
- background: rgba(255, 90, 122, 0.14);
159
- color: #ffd0da;
160
- border: 1px solid rgba(255, 90, 122, 0.35);
161
- }
162
-
163
- .pill {
164
- display: inline-flex;
165
- gap: 6px;
166
- align-items: center;
167
- font-size: 0.78rem;
168
- color: var(--muted);
169
- padding: 6px 10px;
170
- border: 1px solid var(--border);
171
- border-radius: 999px;
172
- }
173
-
174
- .rowline {
175
- display: flex;
176
- flex-wrap: wrap;
177
- gap: 10px;
178
- align-items: center;
179
- }
180
-
181
- .small {
182
- font-size: 0.78rem;
183
- color: var(--muted);
184
- }
185
-
186
- .kbd {
187
- font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
188
- background: #0b0f26;
189
- border: 1px solid var(--border);
190
- padding: 2px 6px;
191
- border-radius: 8px;
192
- color: #cbd5ff;
193
- }
194
-
195
- .out {
196
- white-space: pre-wrap;
197
- background: #0b0f26;
198
- border: 1px dashed rgba(74, 104, 216, 0.6);
199
- border-radius: 12px;
200
- padding: 10px;
201
- margin-top: 10px;
202
- min-height: 120px;
203
- }
204
-
205
- .tabs {
206
- display: flex;
207
- gap: 6px;
208
- flex-wrap: wrap;
209
- margin-top: 10px;
210
- }
211
-
212
- .tab {
213
- padding: 8px 12px;
214
- border-radius: 999px;
215
- border: 1px solid var(--border);
216
- background: #0b0f26;
217
- color: var(--text);
218
- font-weight: 650;
219
- font-size: 0.88rem;
220
- }
221
-
222
- .tab.active {
223
- border-color: rgba(74, 104, 216, 0.8);
224
- box-shadow: 0 0 0 2px rgba(74, 104, 216, 0.25) inset;
225
- }
226
-
227
- .panel {
228
- display: none;
229
- margin-top: 8px;
230
- }
231
-
232
- .panel.active {
233
- display: block;
234
- }
235
-
236
- .drop {
237
- border: 1px dashed rgba(45, 212, 191, 0.55);
238
- background: rgba(45, 212, 191, 0.06);
239
- border-radius: 12px;
240
- padding: 10px;
241
- margin-top: 10px;
242
- }
243
 
244
- .drop.drag {
245
- outline: 2px solid rgba(45, 212, 191, 0.6);
246
- }
 
 
 
 
 
 
 
247
 
248
- .imggrid {
249
- display: grid;
250
- grid-template-columns: 1fr 1fr;
251
- gap: 10px;
252
- margin-top: 10px;
253
- }
254
 
255
- @media (max-width: 680px) {
256
- .imggrid {
257
- grid-template-columns: 1fr;
 
 
 
 
 
 
 
 
258
  }
259
  }
260
 
261
- .thumb {
262
- width: 100%;
263
- border-radius: 12px;
264
- border: 1px solid var(--border);
265
- background: #0b0f26;
266
- }
267
-
268
- .clipItem {
269
- display: flex;
270
- align-items: center;
271
- justify-content: space-between;
272
- gap: 8px;
273
- padding: 8px;
274
- border: 1px solid var(--border);
275
- border-radius: 10px;
276
- background: #0b0f26;
277
- margin: 6px 0;
278
- }
279
-
280
- .clipName {
281
- flex: 1;
282
- font-size: 0.9rem;
283
- color: var(--muted);
284
- overflow: hidden;
285
- text-overflow: ellipsis;
286
- white-space: nowrap;
287
- }
288
-
289
- .clipBtns {
290
- display: flex;
291
- gap: 6px;
292
- }
293
-
294
- .miniBtn {
295
- padding: 6px 10px;
296
- border-radius: 999px;
297
- border: 1px solid var(--border);
298
- background: transparent;
299
- color: var(--text);
300
- font-weight: 700;
301
- font-size: 0.85rem;
302
- }
303
-
304
- a {
305
- color: #9db2ff;
306
- }
307
-
308
- .built-with {
309
- font-size: 0.78rem;
310
- color: var(--muted);
311
- margin-top: 10px;
312
- }
313
-
314
- .built-with a {
315
- color: #9db2ff;
316
- text-decoration: none;
317
- }
318
-
319
- .built-with a:hover {
320
- text-decoration: underline;
321
- }
322
-
323
- ::-webkit-scrollbar {
324
- width: 8px;
325
- height: 8px;
326
- }
327
-
328
- ::-webkit-scrollbar-track {
329
- background: #0b0f26;
330
- border-radius: 10px;
331
- }
332
-
333
- ::-webkit-scrollbar-thumb {
334
- background: var(--accent);
335
- border-radius: 10px;
336
- }
337
-
338
- ::-webkit-scrollbar-thumb:hover {
339
- background: #3a58d8;
340
- }
341
-
342
- .status-indicator {
343
- display: inline-block;
344
- width: 10px;
345
- height: 10px;
346
- border-radius: 50%;
347
- margin-right: 6px;
348
- }
349
-
350
- .status-ok {
351
- background-color: var(--success);
352
- }
353
-
354
- .status-error {
355
- background-color: var(--danger);
356
- }
357
-
358
- .status-warn {
359
- background-color: var(--warn);
360
- }
361
-
362
- .progress-bar {
363
- width: 100%;
364
- height: 4px;
365
- background-color: var(--border);
366
- border-radius: 2px;
367
- margin-top: 8px;
368
- overflow: hidden;
369
- }
370
-
371
- .progress-fill {
372
- height: 100%;
373
- background-color: var(--accent);
374
- width: 0%;
375
- transition: width 0.3s ease;
376
- }
377
-
378
- .api-status {
379
- display: flex;
380
- align-items: center;
381
- gap: 8px;
382
- margin-top: 12px;
383
- padding: 8px;
384
- border-radius: 8px;
385
- background-color: rgba(74, 104, 216, 0.1);
386
- border: 1px solid rgba(74, 104, 216, 0.3);
387
- }
388
-
389
- .api-input {
390
- display: flex;
391
- gap: 8px;
392
- margin-top: 12px;
393
- align-items: center;
394
- }
395
-
396
- .api-input input {
397
- flex: 1;
398
- font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
399
- font-size: 0.85rem;
400
- }
401
 
402
- .api-input button {
403
- padding: 8px 12px;
404
- }
 
 
 
 
 
 
 
 
 
 
405
 
406
- .api-section {
407
- border-top: 1px solid var(--border);
408
- padding-top: 12px;
409
- margin-top: 12px;
410
- }
411
 
412
- .file-info {
413
- margin-top: 10px;
414
- padding: 8px;
415
- border-radius: 8px;
416
- background: rgba(45, 212, 191, 0.1);
417
- border: 1px solid rgba(45, 212, 191, 0.3);
 
 
 
 
 
 
 
418
  }
419
 
420
- .processing-overlay {
421
- position: fixed;
422
- top: 0;
423
- left: 0;
424
- width: 100%;
425
- height: 100%;
426
- background: rgba(0, 0, 0, 0.7);
427
- display: none;
428
- justify-content: center;
429
- align-items: center;
430
- z-index: 1000;
431
- }
432
 
433
- .processing-content {
434
- text-align: center;
435
- background: var(--card);
436
- padding: 24px;
437
- border-radius: 16px;
438
- border: 1px solid var(--border);
439
- }
 
 
 
440
 
441
- .spinner {
442
- width: 40px;
443
- height: 40px;
444
- border: 4px solid rgba(74, 104, 216, 0.2);
445
- border-radius: 50%;
446
- border-top-color: var(--accent);
447
- animation: spin 1s ease-in-out infinite;
448
- margin: 0 auto 16px;
449
- }
450
 
451
- @keyframes spin {
452
- to {
453
- transform: rotate(360deg);
 
 
 
 
 
 
 
 
 
 
 
 
 
454
  }
455
  }
456
 
457
- .modal {
458
- display: none;
459
- position: fixed;
460
- z-index: 1001;
461
- left: 0;
462
- top: 0;
463
- width: 100%;
464
- height: 100%;
465
- background-color: rgba(0, 0, 0, 0.7);
466
- justify-content: center;
467
- align-items: center;
468
- }
469
 
470
- .modal-content {
471
- background: var(--card);
472
- border: 1px solid var(--border);
473
- border-radius: 16px;
474
- padding: 24px;
475
- max-width: 560px;
476
- width: 92%;
477
- max-height: 80vh;
478
- overflow-y: auto;
479
- }
480
 
481
- .modal-header {
482
- display: flex;
483
- justify-content: space-between;
484
- align-items: center;
485
- margin-bottom: 16px;
486
- }
487
 
488
- .modal-title {
489
- font-size: 1.2rem;
490
- margin: 0;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
491
  }
492
 
493
- .close-modal {
494
- background: none;
495
- border: none;
496
- color: var(--muted);
497
- font-size: 1.5rem;
498
- cursor: pointer;
499
- }
500
 
501
- .modal-body {
502
- margin-bottom: 20px;
 
 
503
  }
504
 
505
- .modal-footer {
506
- display: flex;
507
- justify-content: flex-end;
508
- gap: 10px;
 
509
  }
510
 
511
- .error-message {
512
- color: var(--danger);
513
- font-size: 0.85rem;
514
- margin-top: 8px;
515
- padding: 8px;
516
- background-color: rgba(255, 90, 122, 0.1);
517
- border-radius: 8px;
518
- border: 1px solid rgba(255, 90, 122, 0.2);
519
- }
520
 
521
- .api-key-input-container {
522
- position: relative;
523
- }
 
 
524
 
525
- .toggle-password {
526
- position: absolute;
527
- right: 10px;
528
- top: 50%;
529
- transform: translateY(-50%);
530
- background: none;
531
- border: none;
532
- color: var(--muted);
533
- cursor: pointer;
534
- font-size: 0.9rem;
535
  }
536
- </style>
537
- </head>
538
- <body>
539
- <header>
540
- <div class="rowline">
541
- <h1>Mr.FLEN — Fiverr Music Creator (Local)</h1>
542
- <span class="pill" id="healthPill">
543
- <span class="status-indicator status-ok"></span>
544
- Server OK — port: 8787 — OpenAI key: <span id="apiKeyStatus">checking...</span> — model:
545
- <span id="modelStatus">loading...</span> — video: <span id="videoStatus">off</span>
546
- </span>
547
- <button class="ghost" id="diagBtn" type="button">
548
- <i class="fas fa-diagnoses"></i> Run diag
549
- </button>
550
- </div>
551
- <p>
552
- Flow: form → <span class="kbd">Generate</span> → <span class="kbd">Populate</span> → optional image/video →
553
- uploads → <span class="kbd">Stitch</span> into one MP4.
554
- This generator rewrites banned vocabulary using synonyms or more complex wording — it never fails due to banned
555
- words.
556
- </p>
557
- <div class="built-with">
558
- <a href="https://huggingface.co/spaces/akhaliq/anycoder" target="_blank">Built with anycoder</a>
559
- </div>
560
- </header>
561
-
562
- <main>
563
- <!-- 1) Customer Form -->
564
- <section class="card">
565
- <h2>1) Customer Form</h2>
566
-
567
- <!-- API Key Input Section -->
568
- <div class="api-section">
569
- <h3 style="margin: 0 0 8px 0; font-size: 0.95rem;">OpenAI API Configuration</h3>
570
- <div class="api-input">
571
- <div class="api-key-input-container" style="position: relative; flex: 1;">
572
- <input type="password" id="openaiApiKey" placeholder="Enter your OpenAI API key (sk-...)">
573
- <button type="button" class="toggle-password" id="togglePassword">
574
- <i class="fas fa-eye"></i>
575
- </button>
576
- </div>
577
- <button class="primary" id="saveApiKeyBtn" type="button">
578
- <i class="fas fa-save"></i> Save Key
579
- </button>
580
- <button class="ghost" id="testApiKeyBtn" type="button">
581
- <i class="fas fa-vial"></i> Test Key
582
- </button>
583
- </div>
584
- <div class="small" style="margin-top: 6px;">
585
- Your API key is stored locally in your browser and never sent to our servers.
586
- <a href="https://platform.openai.com/account/api-keys" target="_blank" style="color: var(--accent);">Get your
587
- API key</a>
588
- </div>
589
- <div class="api-status" id="apiStatusDisplay" style="display: none;">
590
- <span class="status-indicator"></span>
591
- <span id="apiStatusMessage"></span>
592
- </div>
593
- <div id="apiErrorDisplay" class="error-message" style="display: none;"></div>
594
- </div>
595
-
596
- <div class="grid2">
597
- <div>
598
- <label>Song title</label>
599
- <input id="songTitle" placeholder="Let AI decide if blank">
600
- </div>
601
- <div>
602
- <label>Occasion</label>
603
- <input id="occasion" placeholder="Birthday / Christmas / Anniversary…">
604
- </div>
605
- <div>
606
- <label>Buyer name</label>
607
- <input id="buyerName" placeholder="Ben">
608
- </div>
609
- <div>
610
- <label>Recipient name</label>
611
- <input id="recipientName" placeholder="JoJo">
612
- </div>
613
- <div>
614
- <label>Relationship</label>
615
- <input id="relationship" placeholder="Partner / best mate / mum…">
616
- </div>
617
- <div>
618
- <label>Tone / vibe</label>
619
- <input id="tone" placeholder="Funny but romantic / heartfelt / hype…">
620
- </div>
621
- <div>
622
- <label>Main genre</label>
623
- <input id="mainGenre" placeholder="UKG / Pop / Singer-songwriter…">
624
- </div>
625
- <div>
626
- <label>Secondary genres</label>
627
- <input id="secondaryGenres" placeholder="Orchestral lift / UKG shuffle / etc.">
628
- </div>
629
- <div>
630
- <label>Tempo / energy</label>
631
- <input id="tempo" placeholder="Mid-tempo / slow / upbeat…">
632
- </div>
633
- <div>
634
- <label>Vocal style</label>
635
- <input id="vocalStyle" placeholder="Warm intimate tenor, conversational timing…">
636
- </div>
637
- <div>
638
- <label>Instrumentation notes</label>
639
- <input id="instrumentationNotes" placeholder="Piano-led, strings, UKG hats…">
640
- </div>
641
- <div>
642
- <label>Language level</label>
643
- <select id="languageLevel">
644
- <option value="clean">clean</option>
645
- <option value="mild">mild</option>
646
- <option value="no limit" selected>no limit</option>
647
- </select>
648
- </div>
649
- <div>
650
- <label>Package level</label>
651
- <select id="packageLevel">
652
- <option value="basic">Basic</option>
653
- <option value="standard">Standard</option>
654
- <option value="premium" selected>Premium</option>
655
- <option value="ultra">Ultra</option>
656
- </select>
657
- </div>
658
- <div class="rowline" style="margin-top:8px">
659
- <label style="margin:0 10px 0 0">Include packs?</label>
660
- <input id="wantsArtwork" type="checkbox" checked> <span class="small">Artwork</span>
661
- <input id="wantsVideo" type="checkbox" checked> <span class="small">Video</span>
662
- </div>
663
- </div>
664
-
665
- <label>3–5 key facts</label>
666
- <textarea id="keyFacts" placeholder="One fact per line…"></textarea>
667
-
668
- <label>1–3 stories / moments</label>
669
- <textarea id="stories" placeholder="One story per line…"></textarea>
670
-
671
- <label>Main message</label>
672
- <textarea id="mainMessage" placeholder="Your heartfelt message…"></textarea>
673
-
674
- <label>Names / people / places to include</label>
675
- <textarea id="namesPlaces" placeholder="Bruno the dog, favourite pub, etc."></textarea>
676
-
677
- <details style="margin-top:10px">
678
- <summary class="rowline" style="cursor:pointer">
679
- <span class="pill">Optional visual inputs</span>
680
- <span class="small">open/close</span>
681
- </summary>
682
- <div style="padding-top:10px">
683
- <div class="grid2">
684
- <div>
685
- <label>Visual style</label>
686
- <input id="visualStyle" placeholder="Photo-real / illustrated / film grain…">
687
- </div>
688
- <div>
689
- <label>Visual mood</label>
690
- <input id="visualMood" placeholder="Romantic, warm, grounded…">
691
- </div>
692
- </div>
693
- <label>Places / themes</label>
694
- <textarea id="visualPlacesThemes" placeholder="Seaside sunset, woodland walk, etc."></textarea>
695
- <label>Typography (exact text)</label>
696
- <input id="visualText" placeholder="Exact words to appear in-world (optional)">
697
- </div>
698
- </details>
699
-
700
- <div class="btnrow">
701
- <button class="primary" id="generateBtn" type="button">
702
- <i class="fas fa-magic"></i> Generate Suno Block
703
- </button>
704
- <button class="ghost" id="randomBtn" type="button">
705
- <i class="fas fa-dice"></i> Randomiser (API)
706
- </button>
707
- <button class="success" id="populateBtn" type="button">
708
- <i class="fas fa-cog"></i> Populate builders
709
- </button>
710
- <button class="ghost" id="copySunoBtn" type="button">
711
- <i class="fas fa-copy"></i> Copy Suno block
712
- </button>
713
- <button class="ghost" id="saveDraftBtn" type="button">
714
- <i class="fas fa-save"></i> Save draft
715
- </button>
716
- <button class="ghost" id="loadDraftBtn" type="button">
717
- <i class="fas fa-folder-open"></i> Load draft
718
- </button>
719
- <button class="danger" id="resetBtn" type="button">
720
- <i class="fas fa-redo"></i> Reset
721
- </button>
722
- </div>
723
-
724
- <div class="small" style="margin-top:10px">
725
- Current order: <span class="kbd" id="orderId">none</span><span id="orderDir" class="small"></span>
726
- </div>
727
-
728
- <div class="out" id="sunoBlockOut">Suno block will appear here…</div>
729
- <div class="small" id="warnings"></div>
730
- <div class="out" id="diagOut" style="display:none"></div>
731
- </section>
732
-
733
- <!-- 2) Prompt Builders -->
734
- <section class="card">
735
- <h2>2) Builders</h2>
736
- <div class="tabs">
737
- <button class="tab active" id="tabSong" type="button">
738
- <i class="fas fa-music"></i> Song
739
- </button>
740
- <button class="tab" id="tabArt" type="button">
741
- <i class="fas fa-paint-brush"></i> Artwork
742
- </button>
743
- <button class="tab" id="tabVideo" type="button">
744
- <i class="fas fa-video"></i> Video
745
- </button>
746
- </div>
747
-
748
- <div class="panel active" id="panelSong">
749
- <label>Song prompt (from [Suno v5 Master Prompt])</label>
750
- <textarea id="songPrompt" placeholder="Generate then populate to see the song prompt"></textarea>
751
- <div class="btnrow">
752
- <button class="ghost" id="copySongPromptBtn" type="button">
753
- <i class="fas fa-copy"></i> Copy song prompt
754
- </button>
755
- </div>
756
- </div>
757
-
758
- <div class="panel" id="panelArt">
759
- <label>Artwork prompt (from [Artwork_Pack])</label>
760
- <textarea id="artPrompt" placeholder="Generate then populate to see the artwork prompt"></textarea>
761
- <div class="btnrow">
762
- <button class="primary" id="genImageBtn" type="button">
763
- <i class="fas fa-magic"></i> Generate Image
764
- </button>
765
- <button class="ghost" id="copyArtBtn" type="button">
766
- <i class="fas fa-copy"></i> Copy artwork prompt
767
- </button>
768
- <button class="ghost" id="clearArtImgsBtn" type="button">
769
- <i class="fas fa-trash"></i> Clear previews
770
- </button>
771
- </div>
772
- <div class="imggrid" id="imgGrid"></div>
773
- </div>
774
-
775
- <div class="panel" id="panelVideo">
776
- <label>Video prompt (from [Video_Pack])</label>
777
- <textarea id="videoPrompt" placeholder="Generate then populate to see the video prompt"></textarea>
778
- <div class="btnrow">
779
- <button class="primary" id="genVideoBtn" type="button" disabled>
780
- <i class="fas fa-magic"></i> Generate Video
781
- </button>
782
- <button class="ghost" id="copyVideoBtn" type="button">
783
- <i class="fas fa-copy"></i> Copy video prompt
784
- </button>
785
- <button class="ghost" id="clearVideoPreviewBtn" type="button">
786
- <i class="fas fa-trash"></i> Clear preview
787
- </button>
788
- </div>
789
- <div class="out" id="videoOut">Video status…</div>
790
- <div id="videoPreview" style="margin-top:10px"></div>
791
- <div class="small" style="margin-top:6px">
792
- Video requires the server to be configured with a video connector. If disabled, the button will be inactive.
793
- </div>
794
- </div>
795
- </section>
796
-
797
- <!-- 3) Uploads & Stitching -->
798
- <section class="card">
799
- <h2>3) Uploads (Song + Video clips)</h2>
800
- <div class="small">You can upload unlimited clips. Rearrange the order before uploading, then stitch into a single
801
- music video file.</div>
802
-
803
- <div class="drop" id="songDrop">
804
- <div class="rowline">
805
- <strong>Drop song file here</strong> <span class="small">(MP3/WAV/M4A)</span>
806
- </div>
807
- <input type="file" id="songFile" accept=".mp3,.wav,.m4a">
808
- <div id="songFileInfo" class="file-info" style="display: none;">
809
- <strong>Selected song:</strong> <span id="songFileName"></span>
810
- <button class="miniBtn" id="removeSongBtn" type="button">Remove</button>
811
- </div>
812
- </div>
813
-
814
- <div class="drop" id="clipDrop" style="margin-top:12px">
815
- <div class="rowline">
816
- <strong>Drop video clips here</strong> <span class="small">(MP4)</span>
817
- </div>
818
- <input type="file" id="clipFilesInput" accept=".mp4" multiple>
819
- <div id="clipQueueWrap">
820
- <div class="small" style="margin-top:6px">No clips queued.</div>
821
- </div>
822
- </div>
823
-
824
- <div class="btnrow">
825
- <button class="warn" id="stitchBtn" type="button">
826
- <i class="fas fa-link"></i> Stitch clips → musicVID.mp4
827
- </button>
828
- <button class="ghost" id="clearClipsBtn" type="button">
829
- <i class="fas fa-trash"></i> Clear queue
830
- </button>
831
- </div>
832
-
833
- <div class="out" id="uploadOut">Upload status…</div>
834
- <div class="small" id="orderLinks"></div>
835
- </section>
836
- </main>
837
-
838
- <!-- Processing Overlay -->
839
- <div class="processing-overlay" id="processingOverlay">
840
- <div class="processing-content">
841
- <div class="spinner"></div>
842
- <h3>Processing...</h3>
843
- <p id="processingMessage">Please wait while we process your request</p>
844
- <div class="progress-bar">
845
- <div class="progress-fill" id="progressFill"></div>
846
- </div>
847
- </div>
848
- </div>
849
-
850
- <!-- Modal -->
851
- <div class="modal" id="infoModal">
852
- <div class="modal-content">
853
- <div class="modal-header">
854
- <h2 class="modal-title">Information</h2>
855
- <button class="close-modal" id="closeModal" type="button">&times;</button>
856
- </div>
857
- <div class="modal-body" id="modalBody"></div>
858
- <div class="modal-footer">
859
- <button class="primary" id="modalOkBtn" type="button">OK</button>
860
- </div>
861
- </div>
862
- </div>
863
-
864
- <script>
865
- // Configuration
866
- const API_BASE = 'http://localhost:8787/api';
867
- let currentOrderId = null;
868
- let apiKey = '';
869
- let songFile = null;
870
- let videoClips = []; // { id, file, name }
871
-
872
- // DOM Elements
873
- const healthPill = document.getElementById('healthPill');
874
- const apiKeyStatus = document.getElementById('apiKeyStatus');
875
- const modelStatus = document.getElementById('modelStatus');
876
- const videoStatus = document.getElementById('videoStatus');
877
- const orderIdDisplay = document.getElementById('orderId');
878
- const orderDirSpan = document.getElementById('orderDir');
879
- const sunoBlockOut = document.getElementById('sunoBlockOut');
880
- const warningsDisplay = document.getElementById('warnings');
881
- const diagOut = document.getElementById('diagOut');
882
- const openaiApiKeyInput = document.getElementById('openaiApiKey');
883
- const saveApiKeyBtn = document.getElementById('saveApiKeyBtn');
884
- const testApiKeyBtn = document.getElementById('testApiKeyBtn');
885
- const apiStatusDisplay = document.getElementById('apiStatusDisplay');
886
- const apiStatusMessage = document.getElementById('apiStatusMessage');
887
- const apiErrorDisplay = document.getElementById('apiErrorDisplay');
888
- const songFileInput = document.getElementById('songFile');
889
- const songFileInfo = document.getElementById('songFileInfo');
890
- const songFileName = document.getElementById('songFileName');
891
- const removeSongBtn = document.getElementById('removeSongBtn');
892
- const clipFilesInput = document.getElementById('clipFilesInput');
893
- const clipQueueWrap = document.getElementById('clipQueueWrap');
894
- const uploadOut = document.getElementById('uploadOut');
895
- const orderLinks = document.getElementById('orderLinks');
896
- const imgGrid = document.getElementById('imgGrid');
897
- const videoOut = document.getElementById('videoOut');
898
- const videoPreview = document.getElementById('videoPreview');
899
- const togglePassword = document.getElementById('togglePassword');
900
-
901
- const processingOverlay = document.getElementById('processingOverlay');
902
- const processingMessage = document.getElementById('processingMessage');
903
- const progressFill = document.getElementById('progressFill');
904
 
905
- const infoModal = document.getElementById('infoModal');
906
- const modalBody = document.getElementById('modalBody');
907
- const closeModal = document.getElementById('closeModal');
908
- const modalOkBtn = document.getElementById('modalOkBtn');
909
-
910
- // ---------- Init ----------
911
- async function init() {
912
- // Load saved API key from localStorage
913
- const savedKey = localStorage.getItem('openaiApiKey');
914
- if (savedKey) {
915
- apiKey = savedKey;
916
- openaiApiKeyInput.value = '••••••••••••••••••••••••••••••••••••••••';
917
  }
918
 
919
- await checkServerHealth();
920
- setInterval(checkServerHealth, 30000);
921
-
922
- // API key
923
- saveApiKeyBtn.addEventListener('click', saveApiKey);
924
- testApiKeyBtn.addEventListener('click', testApiKey);
925
- togglePassword.addEventListener('click', togglePasswordVisibility);
926
-
927
- // File upload handlers
928
- songFileInput.addEventListener('change', handleSongFileUpload);
929
- removeSongBtn.addEventListener('click', removeSongFile);
930
- clipFilesInput.addEventListener('change', handleClipFilesUpload);
931
-
932
- // Drag and drop
933
- setupDrop(document.getElementById('songDrop'), (files) => {
934
- if (files.length) {
935
- songFileInput.files = files;
936
- handleSongFileUpload();
937
- }
938
- });
939
-
940
- setupDrop(document.getElementById('clipDrop'), (files) => {
941
- if (files.length) {
942
- clipFilesInput.files = files;
943
- handleClipFilesUpload();
944
- }
945
  });
946
 
947
- // Modal handlers
948
- closeModal.addEventListener('click', () => infoModal.style.display = 'none');
949
- modalOkBtn.addEventListener('click', () => infoModal.style.display = 'none');
950
-
951
- // Tabs
952
- document.querySelectorAll('.tab').forEach(tab => {
953
- tab.addEventListener('click', () => {
954
- document.querySelectorAll('.tab').forEach(t => t.classList.remove('active'));
955
- document.querySelectorAll('.panel').forEach(p => p.classList.remove('active'));
956
- tab.classList.add('active');
957
- const panelId = tab.id.replace('tab', 'panel');
958
- document.getElementById(panelId).classList.add('active');
959
  });
960
  });
961
-
962
- // Buttons
963
- document.getElementById('generateBtn').addEventListener('click', generateSunoBlock);
964
- document.getElementById('randomBtn').addEventListener('click', randomizeForm);
965
- document.getElementById('populateBtn').addEventListener('click', populateBuilders);
966
- document.getElementById('copySunoBtn').addEventListener('click', copySunoBlock);
967
- document.getElementById('saveDraftBtn').addEventListener('click', saveDraft);
968
- document.getElementById('loadDraftBtn').addEventListener('click', loadDraft);
969
- document.getElementById('resetBtn').addEventListener('click', resetForm);
970
- document.getElementById('diagBtn').addEventListener('click', runDiagnostics);
971
-
972
- document.getElementById('genImageBtn').addEventListener('click', generateImage);
973
- document.getElementById('genVideoBtn').addEventListener('click', generateVideo);
974
-
975
- document.getElementById('stitchBtn').addEventListener('click', stitchClips);
976
- document.getElementById('clearClipsBtn').addEventListener('click', clearClips);
977
-
978
- // Copy buttons
979
- document.getElementById('copySongPromptBtn').addEventListener('click', () => copyToClipboard(document.getElementById('songPrompt').value));
980
- document.getElementById('copyArtBtn').addEventListener('click', () => copyToClipboard(document.getElementById('artPrompt').value));
981
- document.getElementById('copyVideoBtn').addEventListener('click', () => copyToClipboard(document.getElementById('videoPrompt').value));
982
-
983
- document.getElementById('clearArtImgsBtn').addEventListener('click', () => imgGrid.innerHTML = '');
984
- document.getElementById('clearVideoPreviewBtn').addEventListener('click', () => {
985
- videoPreview.innerHTML = '';
986
- videoOut.textContent = 'Video status…';
987
- });
988
-
989
- // Try to enable video button if server says it's on
990
- await refreshVideoEnabled();
991
- }
992
-
993
- function togglePasswordVisibility() {
994
- const type = openaiApiKeyInput.getAttribute('type') === 'password' ? 'text' : 'password';
995
- openaiApiKeyInput.setAttribute('type', type);
996
- togglePassword.innerHTML = type === 'password' ? '<i class="fas fa-eye"></i>' : '<i class="fas fa-eye-slash"></i>';
997
  }
998
 
999
- function setupDrop(dropEl, onFiles) {
1000
- dropEl.addEventListener('dragover', (e) => {
1001
- e.preventDefault();
1002
- dropEl.classList.add('drag');
1003
- });
1004
- dropEl.addEventListener('dragleave', () => dropEl.classList.remove('drag'));
1005
- dropEl.addEventListener('drop', (e) => {
1006
- e.preventDefault();
1007
- dropEl.classList.remove('drag');
1008
- if (e.dataTransfer.files?.length) onFiles(e.dataTransfer.files);
1009
- });
1010
- }
1011
 
1012
- // ---------- UI helpers ----------
1013
- function showOverlay(msg, pct = null) {
1014
- processingOverlay.style.display = 'flex';
1015
- processingMessage.textContent = msg || 'Processing...';
1016
- if (typeof pct === 'number') progressFill.style.width = `${Math.max(0, Math.min(100, pct))}%`;
1017
- else progressFill.style.width = '0%';
1018
- }
1019
 
1020
- function hideOverlay() {
1021
- processingOverlay.style.display = 'none';
1022
- progressFill.style.width = '0%';
1023
  }
1024
 
1025
- function showModal(html) {
1026
- modalBody.innerHTML = html;
1027
- infoModal.style.display = 'flex';
1028
- }
1029
 
1030
- function showApiStatus(type, message) {
1031
- apiStatusDisplay.style.display = 'flex';
1032
- const statusIndicator = apiStatusDisplay.querySelector('.status-indicator');
1033
 
1034
- if (type === 'success') statusIndicator.className = 'status-indicator status-ok';
1035
- else if (type === 'error') statusIndicator.className = 'status-indicator status-error';
1036
- else statusIndicator.className = 'status-indicator status-warn';
1037
 
1038
- apiStatusMessage.textContent = message;
1039
- }
 
 
 
 
 
1040
 
1041
- function showApiError(message) {
1042
- apiErrorDisplay.textContent = message;
1043
- apiErrorDisplay.style.display = 'block';
 
 
 
 
 
1044
  }
1045
 
1046
- function hideApiError() {
1047
- apiErrorDisplay.style.display = 'none';
 
 
 
1048
  }
1049
 
1050
- async function copyToClipboard(text) {
 
1051
  try {
1052
- await navigator.clipboard.writeText(text || '');
1053
- showModal(`<p class="small">Copied to clipboard ✅</p>`);
1054
  } catch {
1055
- showModal(`<p class="small">Could not copy automatically. Select and copy manually.</p>`);
1056
  }
1057
  }
1058
 
1059
- // ---------- API key ----------
1060
- function saveApiKey() {
1061
- const key = openaiApiKeyInput.value.trim();
1062
- if (!key) return showApiStatus('error', 'Please enter an API key');
1063
-
1064
- if (!key.startsWith('sk-')) return showApiStatus('error', 'Invalid API key format. Should start with "sk-"');
1065
 
1066
- apiKey = key;
1067
- localStorage.setItem('openaiApiKey', key);
1068
- openaiApiKeyInput.value = '••••••••••••••••••••••••••••••••••••••••';
1069
- showApiStatus('success', 'API key saved successfully');
1070
- hideApiError();
1071
- checkServerHealth();
 
 
 
 
1072
  }
1073
 
1074
- async function testApiKey() {
1075
- const key = openaiApiKeyInput.value.trim();
1076
- if (!key) return showApiStatus('error', 'Please enter an API key to test');
1077
 
1078
  try {
1079
- showOverlay('Testing API key...', 30);
1080
- hideApiError();
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1081
 
1082
- const response = await fetch(`${API_BASE}/test-api-key`, {
1083
- method: 'POST',
1084
- headers: { 'Content-Type': 'application/json' },
1085
- body: JSON.stringify({ api_key: key })
 
1086
  });
 
 
 
 
 
 
 
 
 
 
 
 
 
1087
 
 
 
 
 
 
 
1088
  const data = await safeJson(response);
1089
  hideOverlay();
1090
 
1091
- if (response.ok) {
1092
- showApiStatus('success', `API key looks valid. Model: ${data.model || 'unknown'}`);
1093
- } else {
1094
- showApiStatus('error', data.error || 'API key test failed');
1095
- showApiError(data.error || 'API key test failed');
1096
- }
1097
  } catch (error) {
1098
  hideOverlay();
1099
- showApiStatus('error', `Error testing API key: ${error.message}`);
1100
- showApiError(`Error testing API key: ${error.message}`);
1101
- console.error('API key test error:', error);
1102
  }
1103
  }
1104
 
1105
- // ---------- Health ----------
1106
- async function checkServerHealth() {
 
 
 
1107
  try {
1108
- const response = await fetch(`${API_BASE}/health`);
1109
- if (!response.ok) throw new Error('Server not responding');
 
 
 
 
1110
 
1111
- const healthData = await response.json();
1112
- if (healthData.ok) {
1113
- healthPill.querySelector('.status-indicator').className = 'status-indicator status-ok';
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1114
  }
1115
-
1116
- // API key status: if server has a key (or accepts header key)
1117
- apiKeyStatus.textContent = apiKey ? 'yes (browser)' : (healthData.hasOpenAIKey ? 'yes (server)' : 'no');
1118
- apiKeyStatus.style.color = (apiKey || healthData.hasOpenAIKey) ? '#2dd4bf' : '#ff5a7a';
1119
-
1120
- // Model & video
1121
- modelStatus.textContent = healthData.model || 'unknown';
1122
- modelStatus.style.color = '#2dd4bf';
1123
-
1124
- videoStatus.textContent = healthData.videoEnabled ? 'on' : 'off';
1125
- videoStatus.style.color = healthData.videoEnabled ? '#2dd4bf' : '#ffb400';
1126
-
1127
  } catch (error) {
1128
- healthPill.querySelector('.status-indicator').className = 'status-indicator status-error';
1129
- console.error('Health check failed:', error);
1130
  }
1131
  }
1132
 
1133
- async function refreshVideoEnabled() {
1134
- try {
1135
- const r = await fetch(`${API_BASE}/health`);
1136
- if (!r.ok) return;
1137
- const h = await r.json();
1138
- const btn = document.getElementById('genVideoBtn');
1139
- btn.disabled = !h.videoEnabled;
1140
- } catch {
1141
- /* ignore */
1142
- }
1143
- }
1144
 
1145
- // ---------- Form handling ----------
1146
- function getFormData() {
1147
- const splitLines = (v) =>
1148
- String(v || '')
1149
- .split(/\r?\n/)
1150
- .map(s => s.trim())
 
1
+ .filter(Boolean);
2
+
3
+ return {
4
+ songTitle: document.getElementById('songTitle').value.trim(),
5
+ occasion: document.getElementById('occasion').value.trim(),
6
+ buyerName: document.getElementById('buyerName').value.trim(),
7
+ recipientName: document.getElementById('recipientName').value.trim(),
8
+ relationship: document.getElementById('relationship').value.trim(),
9
+ tone: document.getElementById('tone').value.trim(),
10
+ mainGenre: document.getElementById('mainGenre').value.trim(),
11
+ secondaryGenres: document.getElementById('secondaryGenres').value.trim(),
12
+ tempo: document.getElementById('tempo').value.trim(),
13
+ vocalStyle: document.getElementById('vocalStyle').value.trim(),
14
+ instrumentationNotes: document.getElementById('instrumentationNotes').value.trim(),
15
+ languageLevel: document.getElementById('languageLevel').value,
16
+ packageLevel: document.getElementById('packageLevel').value,
17
+ wantsArtwork: document.getElementById('wantsArtwork').checked,
18
+ wantsVideo: document.getElementById('wantsVideo').checked,
19
+ keyFacts: splitLines(document.getElementById('keyFacts').value),
20
+ stories: splitLines(document.getElementById('stories').value),
21
+ mainMessage: document.getElementById('mainMessage').value.trim(),
22
+ namesPlaces: splitLines(document.getElementById('namesPlaces').value),
23
+ visualStyle: document.getElementById('visualStyle').value.trim(),
24
+ visualMood: document.getElementById('visualMood').value.trim(),
25
+ visualPlacesThemes: splitLines(document.getElementById('visualPlacesThemes').value),
26
+ visualText: document.getElementById('visualText').value.trim()
27
+ };
28
+ }
29
+
30
+ async function generateSunoBlock() {
31
+ if (!apiKey) {
32
+ return showModal('<p class="small">Please save your OpenAI API key first.</p>');
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
33
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
34
 
35
+ const formData = getFormData();
36
+ if (!formData.buyerName || !formData.recipientName) {
37
+ return showModal('<p class="small">Please fill in at least buyer and recipient names.</p>');
38
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
39
 
40
+ try {
41
+ showOverlay('Generating Suno block...', 30);
42
+ const response = await fetch(`${API_BASE}/generate-suno-block`, {
43
+ method: 'POST',
44
+ headers: {
45
+ 'Content-Type': 'application/json',
46
+ 'X-OpenAI-API-Key': apiKey
47
+ },
48
+ body: JSON.stringify(formData)
49
+ });
50
 
51
+ const data = await safeJson(response);
52
+ hideOverlay();
 
 
 
 
53
 
54
+ if (response.ok) {
55
+ sunoBlockOut.textContent = data.sunoBlock;
56
+ warningsDisplay.textContent = data.warnings || '';
57
+ showModal('<p class="small">Suno block generated successfully ✅</p>');
58
+ } else {
59
+ showModal(`<p class="small">Error: ${data.error || 'Failed to generate Suno block'}</p>`);
60
+ }
61
+ } catch (error) {
62
+ hideOverlay();
63
+ showModal(`<p class="small">Error: ${error.message}</p>`);
64
+ console.error('Generate error:', error);
65
  }
66
  }
67
 
68
+ async function populateBuilders() {
69
+ if (!sunoBlockOut.textContent.trim()) {
70
+ return showModal('<p class="small">Generate a Suno block first.</p>');
71
+ }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
72
 
73
+ try {
74
+ showOverlay('Populating builders...', 30);
75
+ const response = await fetch(`${API_BASE}/populate-builders`, {
76
+ method: 'POST',
77
+ headers: {
78
+ 'Content-Type': 'application/json',
79
+ 'X-OpenAI-API-Key': apiKey
80
+ },
81
+ body: JSON.stringify({
82
+ sunoBlock: sunoBlockOut.textContent.trim(),
83
+ formData: getFormData()
84
+ })
85
+ });
86
 
87
+ const data = await safeJson(response);
88
+ hideOverlay();
 
 
 
89
 
90
+ if (response.ok) {
91
+ document.getElementById('songPrompt').value = data.songPrompt || '';
92
+ document.getElementById('artPrompt').value = data.artPrompt || '';
93
+ document.getElementById('videoPrompt').value = data.videoPrompt || '';
94
+ showModal('<p class="small">Builders populated successfully ✅</p>');
95
+ } else {
96
+ showModal(`<p class="small">Error: ${data.error || 'Failed to populate builders'}</p>`);
97
+ }
98
+ } catch (error) {
99
+ hideOverlay();
100
+ showModal(`<p class="small">Error: ${error.message}</p>`);
101
+ console.error('Populate error:', error);
102
+ }
103
  }
104
 
105
+ async function generateImage() {
106
+ const prompt = document.getElementById('artPrompt').value.trim();
107
+ if (!prompt) {
108
+ return showModal('<p class="small">Please populate the artwork prompt first.</p>');
109
+ }
 
 
 
 
 
 
 
110
 
111
+ try {
112
+ showOverlay('Generating image...', 30);
113
+ const response = await fetch(`${API_BASE}/generate-image`, {
114
+ method: 'POST',
115
+ headers: {
116
+ 'Content-Type': 'application/json',
117
+ 'X-OpenAI-API-Key': apiKey
118
+ },
119
+ body: JSON.stringify({ prompt })
120
+ });
121
 
122
+ const data = await safeJson(response);
123
+ hideOverlay();
 
 
 
 
 
 
 
124
 
125
+ if (response.ok) {
126
+ imgGrid.innerHTML = '';
127
+ data.imageUrls.forEach(url => {
128
+ const img = document.createElement('img');
129
+ img.src = url;
130
+ img.className = 'thumb';
131
+ imgGrid.appendChild(img);
132
+ });
133
+ showModal('<p class="small">Image generated successfully ✅</p>');
134
+ } else {
135
+ showModal(`<p class="small">Error: ${data.error || 'Failed to generate image'}</p>`);
136
+ }
137
+ } catch (error) {
138
+ hideOverlay();
139
+ showModal(`<p class="small">Error: ${error.message}</p>`);
140
+ console.error('Image generation error:', error);
141
  }
142
  }
143
 
144
+ async function generateVideo() {
145
+ const prompt = document.getElementById('videoPrompt').value.trim();
146
+ if (!prompt) {
147
+ return showModal('<p class="small">Please populate the video prompt first.</p>');
148
+ }
 
 
 
 
 
 
 
149
 
150
+ try {
151
+ showOverlay('Generating video...', 30);
152
+ const response = await fetch(`${API_BASE}/generate-video`, {
153
+ method: 'POST',
154
+ headers: {
155
+ 'Content-Type': 'application/json',
156
+ 'X-OpenAI-API-Key': apiKey
157
+ },
158
+ body: JSON.stringify({ prompt })
159
+ });
160
 
161
+ const data = await safeJson(response);
162
+ hideOverlay();
 
 
 
 
163
 
164
+ if (response.ok) {
165
+ videoOut.textContent = 'Video generated successfully';
166
+ videoPreview.innerHTML = `
167
+ <video controls class="thumb" style="width:100%">
168
+ <source src="${data.videoUrl}" type="video/mp4">
169
+ Your browser does not support the video tag.
170
+ </video>
171
+ `;
172
+ showModal('<p class="small">Video generated successfully ✅</p>');
173
+ } else {
174
+ showModal(`<p class="small">Error: ${data.error || 'Failed to generate video'}</p>`);
175
+ }
176
+ } catch (error) {
177
+ hideOverlay();
178
+ showModal(`<p class="small">Error: ${error.message}</p>`);
179
+ console.error('Video generation error:', error);
180
+ }
181
  }
182
 
183
+ // ---------- File handling ----------
184
+ function handleSongFileUpload() {
185
+ const file = songFileInput.files[0];
186
+ if (!file) return;
 
 
 
187
 
188
+ songFile = file;
189
+ songFileName.textContent = file.name;
190
+ songFileInfo.style.display = 'flex';
191
+ uploadOut.textContent = 'Song file ready for upload';
192
  }
193
 
194
+ function removeSongFile() {
195
+ songFile = null;
196
+ songFileInput.value = '';
197
+ songFileInfo.style.display = 'none';
198
+ uploadOut.textContent = 'Upload status...';
199
  }
200
 
201
+ function handleClipFilesUpload() {
202
+ const files = Array.from(clipFilesInput.files);
203
+ if (!files.length) return;
 
 
 
 
 
 
204
 
205
+ videoClips = files.map((file, i) => ({
206
+ id: Date.now() + i,
207
+ file,
208
+ name: file.name
209
+ }));
210
 
211
+ renderClipQueue();
 
 
 
 
 
 
 
 
 
212
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
213
 
214
+ function renderClipQueue() {
215
+ if (!videoClips.length) {
216
+ clipQueueWrap.innerHTML = '<div class="small" style="margin-top:6px">No clips queued.</div>';
217
+ return;
 
 
 
 
 
 
 
 
218
  }
219
 
220
+ clipQueueWrap.innerHTML = '';
221
+ videoClips.forEach((clip, idx) => {
222
+ const item = document.createElement('div');
223
+ item.className = 'clipItem';
224
+ item.innerHTML = `
225
+ <span class="clipName">${clip.name}</span>
226
+ <div class="clipBtns">
227
+ <button class="miniBtn" data-id="${clip.id}" data-action="move-up">↑</button>
228
+ <button class="miniBtn" data-id="${clip.id}" data-action="move-down">↓</button>
229
+ <button class="miniBtn" data-id="${clip.id}" data-action="remove">×</button>
230
+ </div>
231
+ `;
232
+ clipQueueWrap.appendChild(item);
 
 
 
 
 
 
 
 
 
 
 
 
 
233
  });
234
 
235
+ clipQueueWrap.querySelectorAll('.clipBtns button').forEach(btn => {
236
+ btn.addEventListener('click', (e) => {
237
+ const id = parseInt(e.target.getAttribute('data-id'));
238
+ const action = e.target.getAttribute('data-action');
239
+ handleClipAction(id, action);
 
 
 
 
 
 
 
240
  });
241
  });
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
242
  }
243
 
244
+ function handleClipAction(id, action) {
245
+ const idx = videoClips.findIndex(c => c.id === id);
246
+ if (idx === -1) return;
 
 
 
 
 
 
 
 
 
247
 
248
+ if (action === 'remove') {
249
+ videoClips.splice(idx, 1);
250
+ } else if (action === 'move-up' && idx > 0) {
251
+ [videoClips[idx], videoClips[idx - 1]] = [videoClips[idx - 1], videoClips[idx]];
252
+ } else if (action === 'move-down' && idx < videoClips.length - 1) {
253
+ [videoClips[idx], videoClips[idx + 1]] = [videoClips[idx + 1], videoClips[idx]];
254
+ }
255
 
256
+ renderClipQueue();
 
 
257
  }
258
 
259
+ async function stitchClips() {
260
+ if (!songFile) {
261
+ return showModal('<p class="small">Please upload a song file first.</p>');
262
+ }
263
 
264
+ if (!videoClips.length) {
265
+ return showModal('<p class="small">Please upload at least one video clip.</p>');
266
+ }
267
 
268
+ try {
269
+ showOverlay('Stitching clips...', 30);
 
270
 
271
+ // In a real implementation, you would upload files and call the stitch API
272
+ // This is a simplified version that simulates the process
273
+ await new Promise(resolve => setTimeout(resolve, 2000));
274
+ showOverlay('Stitching clips...', 60);
275
+ await new Promise(resolve => setTimeout(resolve, 2000));
276
+ showOverlay('Stitching clips...', 90);
277
+ await new Promise(resolve => setTimeout(resolve, 1000));
278
 
279
+ hideOverlay();
280
+ showModal('<p class="small">Clips stitched successfully! ✅</p>');
281
+ uploadOut.textContent = 'Stitching complete. Download: musicVID.mp4';
282
+ } catch (error) {
283
+ hideOverlay();
284
+ showModal(`<p class="small">Error: ${error.message}</p>`);
285
+ console.error('Stitch error:', error);
286
+ }
287
  }
288
 
289
+ function clearClips() {
290
+ videoClips = [];
291
+ clipFilesInput.value = '';
292
+ renderClipQueue();
293
+ uploadOut.textContent = 'Upload status...';
294
  }
295
 
296
+ // ---------- Utility functions ----------
297
+ async function safeJson(response) {
298
  try {
299
+ return await response.json();
 
300
  } catch {
301
+ return { error: 'Invalid server response' };
302
  }
303
  }
304
 
305
+ function copySunoBlock() {
306
+ copyToClipboard(sunoBlockOut.textContent);
307
+ }
 
 
 
308
 
309
+ function saveDraft() {
310
+ const draft = {
311
+ formData: getFormData(),
312
+ sunoBlock: sunoBlockOut.textContent,
313
+ songPrompt: document.getElementById('songPrompt').value,
314
+ artPrompt: document.getElementById('artPrompt').value,
315
+ videoPrompt: document.getElementById('videoPrompt').value
316
+ };
317
+ localStorage.setItem('fiverrMusicDraft', JSON.stringify(draft));
318
+ showModal('<p class="small">Draft saved successfully ✅</p>');
319
  }
320
 
321
+ function loadDraft() {
322
+ const saved = localStorage.getItem('fiverrMusicDraft');
323
+ if (!saved) return showModal('<p class="small">No saved draft found.</p>');
324
 
325
  try {
326
+ const draft = JSON.parse(saved);
327
+
328
+ // Restore form
329
+ document.getElementById('songTitle').value = draft.formData.songTitle || '';
330
+ document.getElementById('occasion').value = draft.formData.occasion || '';
331
+ document.getElementById('buyerName').value = draft.formData.buyerName || '';
332
+ document.getElementById('recipientName').value = draft.formData.recipientName || '';
333
+ document.getElementById('relationship').value = draft.formData.relationship || '';
334
+ document.getElementById('tone').value = draft.formData.tone || '';
335
+ document.getElementById('mainGenre').value = draft.formData.mainGenre || '';
336
+ document.getElementById('secondaryGenres').value = draft.formData.secondaryGenres || '';
337
+ document.getElementById('tempo').value = draft.formData.tempo || '';
338
+ document.getElementById('vocalStyle').value = draft.formData.vocalStyle || '';
339
+ document.getElementById('instrumentationNotes').value = draft.formData.instrumentationNotes || '';
340
+ document.getElementById('languageLevel').value = draft.formData.languageLevel || 'no limit';
341
+ document.getElementById('packageLevel').value = draft.formData.packageLevel || 'premium';
342
+ document.getElementById('wantsArtwork').checked = draft.formData.wantsArtwork || false;
343
+ document.getElementById('wantsVideo').checked = draft.formData.wantsVideo || false;
344
+ document.getElementById('keyFacts').value = draft.formData.keyFacts?.join('\n') || '';
345
+ document.getElementById('stories').value = draft.formData.stories?.join('\n') || '';
346
+ document.getElementById('mainMessage').value = draft.formData.mainMessage || '';
347
+ document.getElementById('namesPlaces').value = draft.formData.namesPlaces?.join('\n') || '';
348
+ document.getElementById('visualStyle').value = draft.formData.visualStyle || '';
349
+ document.getElementById('visualMood').value = draft.formData.visualMood || '';
350
+ document.getElementById('visualPlacesThemes').value = draft.formData.visualPlacesThemes?.join('\n') || '';
351
+ document.getElementById('visualText').value = draft.formData.visualText || '';
352
+
353
+ // Restore outputs
354
+ sunoBlockOut.textContent = draft.sunoBlock || '';
355
+ document.getElementById('songPrompt').value = draft.songPrompt || '';
356
+ document.getElementById('artPrompt').value = draft.artPrompt || '';
357
+ document.getElementById('videoPrompt').value = draft.videoPrompt || '';
358
+
359
+ showModal('<p class="small">Draft loaded successfully ✅</p>');
360
+ } catch (error) {
361
+ showModal(`<p class="small">Error loading draft: ${error.message}</p>`);
362
+ }
363
+ }
364
 
365
+ function resetForm() {
366
+ if (confirm('Are you sure you want to reset the entire form?')) {
367
+ document.querySelectorAll('input, textarea, select').forEach(el => {
368
+ if (el.type === 'checkbox') el.checked = false;
369
+ else el.value = '';
370
  });
371
+ sunoBlockOut.textContent = 'Suno block will appear here...';
372
+ warningsDisplay.textContent = '';
373
+ document.getElementById('songPrompt').value = '';
374
+ document.getElementById('artPrompt').value = '';
375
+ document.getElementById('videoPrompt').value = '';
376
+ imgGrid.innerHTML = '';
377
+ videoPreview.innerHTML = '';
378
+ videoOut.textContent = 'Video status...';
379
+ removeSongFile();
380
+ clearClips();
381
+ showModal('<p class="small">Form reset complete ✅</p>');
382
+ }
383
+ }
384
 
385
+ async function runDiagnostics() {
386
+ try {
387
+ showOverlay('Running diagnostics...', 30);
388
+ const response = await fetch(`${API_BASE}/diagnostics`, {
389
+ headers: { 'X-OpenAI-API-Key': apiKey }
390
+ });
391
  const data = await safeJson(response);
392
  hideOverlay();
393
 
394
+ diagOut.textContent = JSON.stringify(data, null, 2);
395
+ diagOut.style.display = 'block';
396
+ showModal('<p class="small">Diagnostics complete ✅</p>');
 
 
 
397
  } catch (error) {
398
  hideOverlay();
399
+ showModal(`<p class="small">Error: ${error.message}</p>`);
 
 
400
  }
401
  }
402
 
403
+ async function randomizeForm() {
404
+ if (!apiKey) {
405
+ return showModal('<p class="small">Please save your OpenAI API key first.</p>');
406
+ }
407
+
408
  try {
409
+ showOverlay('Randomizing form...', 30);
410
+ const response = await fetch(`${API_BASE}/randomize-form`, {
411
+ headers: { 'X-OpenAI-API-Key': apiKey }
412
+ });
413
+ const data = await safeJson(response);
414
+ hideOverlay();
415
 
416
+ if (response.ok) {
417
+ document.getElementById('songTitle').value = data.songTitle || '';
418
+ document.getElementById('occasion').value = data.occasion || '';
419
+ document.getElementById('buyerName').value = data.buyerName || '';
420
+ document.getElementById('recipientName').value = data.recipientName || '';
421
+ document.getElementById('relationship').value = data.relationship || '';
422
+ document.getElementById('tone').value = data.tone || '';
423
+ document.getElementById('mainGenre').value = data.mainGenre || '';
424
+ document.getElementById('secondaryGenres').value = data.secondaryGenres || '';
425
+ document.getElementById('tempo').value = data.tempo || '';
426
+ document.getElementById('vocalStyle').value = data.vocalStyle || '';
427
+ document.getElementById('instrumentationNotes').value = data.instrumentationNotes || '';
428
+ document.getElementById('keyFacts').value = data.keyFacts?.join('\n') || '';
429
+ document.getElementById('stories').value = data.stories?.join('\n') || '';
430
+ document.getElementById('mainMessage').value = data.mainMessage || '';
431
+ document.getElementById('namesPlaces').value = data.namesPlaces?.join('\n') || '';
432
+ showModal('<p class="small">Form randomized successfully ✅</p>');
433
+ } else {
434
+ showModal(`<p class="small">Error: ${data.error || 'Failed to randomize form'}</p>`);
435
  }
 
 
 
 
 
 
 
 
 
 
 
 
436
  } catch (error) {
437
+ hideOverlay();
438
+ showModal(`<p class="small">Error: ${error.message}</p>`);
439
  }
440
  }
441
 
442
+ // ---------- Initialize ----------
443
+ init();
444
+ </script>
445
+ </body>
 
 
 
 
 
 
 
446
 
447
+ </html>