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

Upload folder using huggingface_hub

Browse files
Files changed (1) hide show
  1. index.html +1204 -447
index.html CHANGED
@@ -1,447 +1,1204 @@
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>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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>Music Video Creator</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
+ --primary: #4a6bff;
11
+ --primary-dark: #3a5bef;
12
+ --secondary: #f8f9fa;
13
+ --text: #333;
14
+ --text-light: #666;
15
+ --white: #ffffff;
16
+ --error: #ff4444;
17
+ --success: #28a745;
18
+ --border: #e0e0e0;
19
+ --shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
20
+ --radius: 8px;
21
+ }
22
+
23
+ * {
24
+ margin: 0;
25
+ padding: 0;
26
+ box-sizing: border-box;
27
+ font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
28
+ }
29
+
30
+ body {
31
+ background-color: #f5f7ff;
32
+ color: var(--text);
33
+ line-height: 1.6;
34
+ padding: 20px;
35
+ }
36
+
37
+ .container {
38
+ max-width: 1200px;
39
+ margin: 0 auto;
40
+ background: white;
41
+ border-radius: var(--radius);
42
+ box-shadow: var(--shadow);
43
+ overflow: hidden;
44
+ }
45
+
46
+ header {
47
+ background: linear-gradient(135deg, var(--primary), var(--primary-dark));
48
+ color: white;
49
+ padding: 20px 30px;
50
+ display: flex;
51
+ justify-content: space-between;
52
+ align-items: center;
53
+ }
54
+
55
+ .logo {
56
+ font-size: 24px;
57
+ font-weight: bold;
58
+ display: flex;
59
+ align-items: center;
60
+ gap: 10px;
61
+ }
62
+
63
+ .logo i {
64
+ font-size: 30px;
65
+ }
66
+
67
+ .header-actions {
68
+ display: flex;
69
+ gap: 15px;
70
+ align-items: center;
71
+ }
72
+
73
+ .btn {
74
+ padding: 8px 16px;
75
+ border: none;
76
+ border-radius: var(--radius);
77
+ cursor: pointer;
78
+ font-weight: 500;
79
+ transition: all 0.3s ease;
80
+ display: inline-flex;
81
+ align-items: center;
82
+ gap: 8px;
83
+ }
84
+
85
+ .btn-primary {
86
+ background-color: var(--primary);
87
+ color: white;
88
+ }
89
+
90
+ .btn-primary:hover {
91
+ background-color: var(--primary-dark);
92
+ transform: translateY(-2px);
93
+ }
94
+
95
+ .btn-secondary {
96
+ background-color: var(--secondary);
97
+ color: var(--text);
98
+ }
99
+
100
+ .btn-secondary:hover {
101
+ background-color: #e9ecef;
102
+ }
103
+
104
+ .btn-small {
105
+ padding: 4px 12px;
106
+ font-size: 14px;
107
+ }
108
+
109
+ .main-content {
110
+ display: grid;
111
+ grid-template-columns: 1fr 1fr;
112
+ gap: 30px;
113
+ padding: 30px;
114
+ }
115
+
116
+ .section {
117
+ background: white;
118
+ border-radius: var(--radius);
119
+ padding: 20px;
120
+ box-shadow: 0 2px 4px rgba(0, 0, 0, 0.05);
121
+ }
122
+
123
+ .section-title {
124
+ font-size: 18px;
125
+ font-weight: 600;
126
+ margin-bottom: 15px;
127
+ color: var(--primary);
128
+ display: flex;
129
+ align-items: center;
130
+ gap: 8px;
131
+ }
132
+
133
+ .form-group {
134
+ margin-bottom: 15px;
135
+ }
136
+
137
+ .form-group label {
138
+ display: block;
139
+ margin-bottom: 5px;
140
+ font-weight: 500;
141
+ color: var(--text-light);
142
+ }
143
+
144
+ .form-control {
145
+ width: 100%;
146
+ padding: 10px;
147
+ border: 1px solid var(--border);
148
+ border-radius: var(--radius);
149
+ font-size: 14px;
150
+ transition: border-color 0.3s;
151
+ }
152
+
153
+ .form-control:focus {
154
+ outline: none;
155
+ border-color: var(--primary);
156
+ box-shadow: 0 0 0 2px rgba(74, 107, 255, 0.2);
157
+ }
158
+
159
+ textarea.form-control {
160
+ min-height: 100px;
161
+ resize: vertical;
162
+ }
163
+
164
+ .checkbox-group {
165
+ display: flex;
166
+ align-items: center;
167
+ gap: 10px;
168
+ }
169
+
170
+ .checkbox-group input {
171
+ width: auto;
172
+ }
173
+
174
+ .output-section {
175
+ background: #f8f9fa;
176
+ border-radius: var(--radius);
177
+ padding: 15px;
178
+ min-height: 100px;
179
+ font-family: monospace;
180
+ font-size: 13px;
181
+ white-space: pre-wrap;
182
+ word-wrap: break-word;
183
+ position: relative;
184
+ }
185
+
186
+ .output-actions {
187
+ display: flex;
188
+ gap: 10px;
189
+ margin-top: 10px;
190
+ }
191
+
192
+ .thumb-grid {
193
+ display: grid;
194
+ grid-template-columns: repeat(auto-fill, minmax(100px, 1fr));
195
+ gap: 10px;
196
+ margin-top: 10px;
197
+ }
198
+
199
+ .thumb {
200
+ width: 100%;
201
+ height: 100px;
202
+ object-fit: cover;
203
+ border-radius: var(--radius);
204
+ cursor: pointer;
205
+ transition: transform 0.3s;
206
+ }
207
+
208
+ .thumb:hover {
209
+ transform: scale(1.05);
210
+ }
211
+
212
+ .file-upload {
213
+ border: 2px dashed var(--border);
214
+ border-radius: var(--radius);
215
+ padding: 20px;
216
+ text-align: center;
217
+ cursor: pointer;
218
+ transition: all 0.3s;
219
+ }
220
+
221
+ .file-upload:hover {
222
+ border-color: var(--primary);
223
+ background-color: rgba(74, 107, 255, 0.05);
224
+ }
225
+
226
+ .file-info {
227
+ display: flex;
228
+ align-items: center;
229
+ gap: 10px;
230
+ margin-top: 10px;
231
+ padding: 10px;
232
+ background: #f8f9fa;
233
+ border-radius: var(--radius);
234
+ }
235
+
236
+ .clip-item {
237
+ display: flex;
238
+ justify-content: space-between;
239
+ align-items: center;
240
+ padding: 8px;
241
+ background: white;
242
+ border-radius: var(--radius);
243
+ margin-bottom: 5px;
244
+ box-shadow: 0 1px 2px rgba(0, 0, 0, 0.1);
245
+ }
246
+
247
+ .modal {
248
+ display: none;
249
+ position: fixed;
250
+ top: 0;
251
+ left: 0;
252
+ width: 100%;
253
+ height: 100%;
254
+ background: rgba(0, 0, 0, 0.5);
255
+ z-index: 1000;
256
+ justify-content: center;
257
+ align-items: center;
258
+ }
259
+
260
+ .modal-content {
261
+ background: white;
262
+ padding: 30px;
263
+ border-radius: var(--radius);
264
+ max-width: 500px;
265
+ width: 90%;
266
+ box-shadow: 0 5px 15px rgba(0, 0, 0, 0.2);
267
+ }
268
+
269
+ .modal-header {
270
+ display: flex;
271
+ justify-content: space-between;
272
+ align-items: center;
273
+ margin-bottom: 20px;
274
+ }
275
+
276
+ .modal-title {
277
+ font-size: 20px;
278
+ font-weight: 600;
279
+ color: var(--primary);
280
+ }
281
+
282
+ .close-btn {
283
+ background: none;
284
+ border: none;
285
+ font-size: 24px;
286
+ cursor: pointer;
287
+ color: var(--text-light);
288
+ }
289
+
290
+ .overlay {
291
+ display: none;
292
+ position: fixed;
293
+ top: 0;
294
+ left: 0;
295
+ width: 100%;
296
+ height: 100%;
297
+ background: rgba(0, 0, 0, 0.7);
298
+ z-index: 999;
299
+ justify-content: center;
300
+ align-items: center;
301
+ color: white;
302
+ font-size: 18px;
303
+ }
304
+
305
+ .progress-bar {
306
+ width: 200px;
307
+ height: 4px;
308
+ background: rgba(255, 255, 255, 0.3);
309
+ border-radius: 2px;
310
+ margin-top: 20px;
311
+ overflow: hidden;
312
+ }
313
+
314
+ .progress {
315
+ height: 100%;
316
+ background: white;
317
+ width: 0%;
318
+ transition: width 0.3s;
319
+ }
320
+
321
+ .anycoder-link {
322
+ position: absolute;
323
+ top: 10px;
324
+ right: 10px;
325
+ color: white;
326
+ text-decoration: none;
327
+ font-size: 12px;
328
+ opacity: 0.8;
329
+ }
330
+
331
+ .anycoder-link:hover {
332
+ opacity: 1;
333
+ text-decoration: underline;
334
+ }
335
+
336
+ @media (max-width: 768px) {
337
+ .main-content {
338
+ grid-template-columns: 1fr;
339
+ padding: 15px;
340
+ }
341
+
342
+ header {
343
+ flex-direction: column;
344
+ gap: 15px;
345
+ text-align: center;
346
+ }
347
+
348
+ .header-actions {
349
+ flex-wrap: wrap;
350
+ justify-content: center;
351
+ }
352
+ }
353
+ </style>
354
+ </head>
355
+ <body>
356
+ <div class="container">
357
+ <header>
358
+ <div class="logo">
359
+ <i class="fas fa-music"></i>
360
+ <span>Music Video Creator</span>
361
+ </div>
362
+ <div class="header-actions">
363
+ <button class="btn btn-secondary btn-small" onclick="showApiKeyModal()">
364
+ <i class="fas fa-key"></i> API Key
365
+ </button>
366
+ <button class="btn btn-secondary btn-small" onclick="saveDraft()">
367
+ <i class="fas fa-save"></i> Save Draft
368
+ </button>
369
+ <button class="btn btn-secondary btn-small" onclick="loadDraft()">
370
+ <i class="fas fa-folder-open"></i> Load Draft
371
+ </button>
372
+ <button class="btn btn-secondary btn-small" onclick="resetForm()">
373
+ <i class="fas fa-undo"></i> Reset
374
+ </button>
375
+ </div>
376
+ <a href="https://huggingface.co/spaces/akhaliq/anycoder" class="anycoder-link" target="_blank">
377
+ Built with anycoder
378
+ </a>
379
+ </header>
380
+
381
+ <div class="main-content">
382
+ <div class="section">
383
+ <div class="section-title">
384
+ <i class="fas fa-info-circle"></i>
385
+ <span>Song Information</span>
386
+ </div>
387
+
388
+ <div class="form-group">
389
+ <label for="songTitle">Song Title</label>
390
+ <input type="text" id="songTitle" class="form-control" placeholder="Enter song title">
391
+ </div>
392
+
393
+ <div class="form-group">
394
+ <label for="occasion">Occasion</label>
395
+ <input type="text" id="occasion" class="form-control" placeholder="e.g., Birthday, Anniversary">
396
+ </div>
397
+
398
+ <div class="form-group">
399
+ <label for="buyerName">Buyer Name</label>
400
+ <input type="text" id="buyerName" class="form-control" placeholder="Your name">
401
+ </div>
402
+
403
+ <div class="form-group">
404
+ <label for="recipientName">Recipient Name</label>
405
+ <input type="text" id="recipientName" class="form-control" placeholder="Who is this for?">
406
+ </div>
407
+
408
+ <div class="form-group">
409
+ <label for="relationship">Relationship</label>
410
+ <input type="text" id="relationship" class="form-control" placeholder="e.g., Mother, Friend">
411
+ </div>
412
+
413
+ <div class="form-group">
414
+ <label for="tone">Tone</label>
415
+ <input type="text" id="tone" class="form-control" placeholder="e.g., Happy, Romantic">
416
+ </div>
417
+ </div>
418
+
419
+ <div class="section">
420
+ <div class="section-title">
421
+ <i class="fas fa-music"></i>
422
+ <span>Music Details</span>
423
+ </div>
424
+
425
+ <div class="form-group">
426
+ <label for="mainGenre">Main Genre</label>
427
+ <input type="text" id="mainGenre" class="form-control" placeholder="e.g., Pop, Rock">
428
+ </div>
429
+
430
+ <div class="form-group">
431
+ <label for="secondaryGenres">Secondary Genres</label>
432
+ <input type="text" id="secondaryGenres" class="form-control" placeholder="Additional genres">
433
+ </div>
434
+
435
+ <div class="form-group">
436
+ <label for="tempo">Tempo</label>
437
+ <input type="text" id="tempo" class="form-control" placeholder="e.g., Fast, Slow">
438
+ </div>
439
+
440
+ <div class="form-group">
441
+ <label for="vocalStyle">Vocal Style</label>
442
+ <input type="text" id="vocalStyle" class="form-control" placeholder="e.g., Male, Female">
443
+ </div>
444
+
445
+ <div class="form-group">
446
+ <label for="instrumentationNotes">Instrumentation Notes</label>
447
+ <textarea id="instrumentationNotes" class="form-control" placeholder="Any specific instruments?"></textarea>
448
+ </div>
449
+ </div>
450
+
451
+ <div class="section">
452
+ <div class="section-title">
453
+ <i class="fas fa-story"></i>
454
+ <span>Content Details</span>
455
+ </div>
456
+
457
+ <div class="form-group">
458
+ <label for="keyFacts">Key Facts (one per line)</label>
459
+ <textarea id="keyFacts" class="form-control" placeholder="Important facts about the recipient"></textarea>
460
+ </div>
461
+
462
+ <div class="form-group">
463
+ <label for="stories">Stories (one per line)</label>
464
+ <textarea id="stories" class="form-control" placeholder="Memorable stories to include"></textarea>
465
+ </div>
466
+
467
+ <div class="form-group">
468
+ <label for="mainMessage">Main Message</label>
469
+ <textarea id="mainMessage" class="form-control" placeholder="The core message of the song"></textarea>
470
+ </div>
471
+
472
+ <div class="form-group">
473
+ <label for="namesPlaces">Names/Places (one per line)</label>
474
+ <textarea id="namesPlaces" class="form-control" placeholder="Important names and places"></textarea>
475
+ </div>
476
+ </div>
477
+
478
+ <div class="section">
479
+ <div class="section-title">
480
+ <i class="fas fa-palette"></i>
481
+ <span>Visual Style</span>
482
+ </div>
483
+
484
+ <div class="form-group">
485
+ <label for="visualStyle">Visual Style</label>
486
+ <input type="text" id="visualStyle" class="form-control" placeholder="e.g., Cartoon, Realistic">
487
+ </div>
488
+
489
+ <div class="form-group">
490
+ <label for="visualMood">Visual Mood</label>
491
+ <input type="text" id="visualMood" class="form-control" placeholder="e.g., Bright, Moody">
492
+ </div>
493
+
494
+ <div class="form-group">
495
+ <label for="visualPlacesThemes">Places/Themes (one per line)</label>
496
+ <textarea id="visualPlacesThemes" class="form-control" placeholder="Visual themes to include"></textarea>
497
+ </div>
498
+
499
+ <div class="form-group">
500
+ <label for="visualText">Visual Text</label>
501
+ <input type="text" id="visualText" class="form-control" placeholder="Any text to include in visuals">
502
+ </div>
503
+ </div>
504
+
505
+ <div class="section">
506
+ <div class="section-title">
507
+ <i class="fas fa-cog"></i>
508
+ <span>Package Options</span>
509
+ </div>
510
+
511
+ <div class="form-group">
512
+ <label for="languageLevel">Language Level</label>
513
+ <select id="languageLevel" class="form-control">
514
+ <option value="no limit">No Limit</option>
515
+ <option value="simple">Simple</option>
516
+ <option value="advanced">Advanced</option>
517
+ </select>
518
+ </div>
519
+
520
+ <div class="form-group">
521
+ <label for="packageLevel">Package Level</label>
522
+ <select id="packageLevel" class="form-control">
523
+ <option value="basic">Basic</option>
524
+ <option value="premium">Premium</option>
525
+ <option value="deluxe">Deluxe</option>
526
+ </select>
527
+ </div>
528
+
529
+ <div class="form-group checkbox-group">
530
+ <input type="checkbox" id="wantsArtwork">
531
+ <label for="wantsArtwork">Include Artwork</label>
532
+ </div>
533
+
534
+ <div class="form-group checkbox-group">
535
+ <input type="checkbox" id="wantsVideo">
536
+ <label for="wantsVideo">Include Video</label>
537
+ </div>
538
+ </div>
539
+
540
+ <div class="section">
541
+ <div class="section-title">
542
+ <i class="fas fa-robot"></i>
543
+ <span>Generate Suno Block</span>
544
+ </div>
545
+
546
+ <button class="btn btn-primary" onclick="generateSunoBlock()">
547
+ <i class="fas fa-magic"></i> Generate Suno Block
548
+ </button>
549
+
550
+ <div class="output-section" id="sunoBlockOut">
551
+ Suno block will appear here...
552
+ </div>
553
+
554
+ <div class="output-actions">
555
+ <button class="btn btn-secondary btn-small" onclick="copySunoBlock()">
556
+ <i class="fas fa-copy"></i> Copy
557
+ </button>
558
+ <button class="btn btn-secondary btn-small" onclick="populateBuilders()">
559
+ <i class="fas fa-arrow-right"></i> Populate Builders
560
+ </button>
561
+ </div>
562
+
563
+ <div id="warningsDisplay" style="color: var(--error); margin-top: 10px; font-size: 13px;"></div>
564
+ </div>
565
+
566
+ <div class="section">
567
+ <div class="section-title">
568
+ <i class="fas fa-code"></i>
569
+ <span>Builder Prompts</span>
570
+ </div>
571
+
572
+ <div class="form-group">
573
+ <label for="songPrompt">Song Prompt</label>
574
+ <textarea id="songPrompt" class="form-control" placeholder="Generated song prompt will appear here"></textarea>
575
+ </div>
576
+
577
+ <div class="form-group">
578
+ <label for="artPrompt">Artwork Prompt</label>
579
+ <textarea id="artPrompt" class="form-control" placeholder="Generated artwork prompt will appear here"></textarea>
580
+ <button class="btn btn-primary btn-small" onclick="generateImage()" style="margin-top: 10px;">
581
+ <i class="fas fa-image"></i> Generate Image
582
+ </button>
583
+ </div>
584
+
585
+ <div class="form-group">
586
+ <label for="videoPrompt">Video Prompt</label>
587
+ <textarea id="videoPrompt" class="form-control" placeholder="Generated video prompt will appear here"></textarea>
588
+ <button class="btn btn-primary btn-small" onclick="generateVideo()" style="margin-top: 10px;">
589
+ <i class="fas fa-video"></i> Generate Video
590
+ </button>
591
+ </div>
592
+ </div>
593
+
594
+ <div class="section">
595
+ <div class="section-title">
596
+ <i class="fas fa-images"></i>
597
+ <span>Generated Images</span>
598
+ </div>
599
+
600
+ <div id="imgGrid" class="thumb-grid">
601
+ <!-- Images will appear here -->
602
+ </div>
603
+ </div>
604
+
605
+ <div class="section">
606
+ <div class="section-title">
607
+ <i class="fas fa-file-video"></i>
608
+ <span>Video Preview</span>
609
+ </div>
610
+
611
+ <div id="videoPreview" style="margin-bottom: 10px;">
612
+ <!-- Video will appear here -->
613
+ </div>
614
+
615
+ <div id="videoOut" style="font-size: 13px; color: var(--text-light);">
616
+ Video status...
617
+ </div>
618
+ </div>
619
+
620
+ <div class="section">
621
+ <div class="section-title">
622
+ <i class="fas fa-file-audio"></i>
623
+ <span>Song File Upload</span>
624
+ </div>
625
+
626
+ <div class="file-upload" onclick="document.getElementById('songFileInput').click()">
627
+ <i class="fas fa-cloud-upload-alt" style="font-size: 40px; color: var(--primary); margin-bottom: 10px;"></i>
628
+ <p>Click to upload song file</p>
629
+ <input type="file" id="songFileInput" style="display: none;" accept="audio/*" onchange="handleSongFileUpload()">
630
+ </div>
631
+
632
+ <div id="songFileInfo" style="display: none;">
633
+ <span id="songFileName"></span>
634
+ <button class="btn btn-secondary btn-small" onclick="removeSongFile()">
635
+ <i class="fas fa-trash"></i> Remove
636
+ </button>
637
+ </div>
638
+
639
+ <div id="uploadOut" style="margin-top: 10px; font-size: 13px; color: var(--text-light);">
640
+ Upload status...
641
+ </div>
642
+ </div>
643
+
644
+ <div class="section">
645
+ <div class="section-title">
646
+ <i class="fas fa-film"></i>
647
+ <span>Video Clips</span>
648
+ </div>
649
+
650
+ <div class="file-upload" onclick="document.getElementById('clipFilesInput').click()">
651
+ <i class="fas fa-cloud-upload-alt" style="font-size: 40px; color: var(--primary); margin-bottom: 10px;"></i>
652
+ <p>Click to upload video clips</p>
653
+ <input type="file" id="clipFilesInput" style="display: none;" accept="video/*" multiple onchange="handleClipFilesUpload()">
654
+ </div>
655
+
656
+ <div id="clipQueueWrap" style="margin-top: 15px;">
657
+ <div class="small" style="margin-top:6px">No clips queued.</div>
658
+ </div>
659
+
660
+ <div style="margin-top: 15px; display: flex; gap: 10px;">
661
+ <button class="btn btn-primary btn-small" onclick="stitchClips()">
662
+ <i class="fas fa-link"></i> Stitch Clips
663
+ </button>
664
+ <button class="btn btn-secondary btn-small" onclick="clearClips()">
665
+ <i class="fas fa-trash"></i> Clear All
666
+ </button>
667
+ </div>
668
+ </div>
669
+
670
+ <div class="section">
671
+ <div class="section-title">
672
+ <i class="fas fa-tools"></i>
673
+ <span>Advanced Tools</span>
674
+ </div>
675
+
676
+ <button class="btn btn-secondary" onclick="randomizeForm()" style="margin-bottom: 10px;">
677
+ <i class="fas fa-random"></i> Randomize Form
678
+ </button>
679
+
680
+ <button class="btn btn-secondary" onclick="runDiagnostics()">
681
+ <i class="fas fa-stethoscope"></i> Run Diagnostics
682
+ </button>
683
+
684
+ <div id="diagOut" style="display: none; margin-top: 15px; font-family: monospace; font-size: 12px; background: #f8f9fa; padding: 10px; border-radius: var(--radius);"></div>
685
+ </div>
686
+ </div>
687
+ </div>
688
+
689
+ <!-- Modal for API Key -->
690
+ <div id="apiKeyModal" class="modal">
691
+ <div class="modal-content">
692
+ <div class="modal-header">
693
+ <h2 class="modal-title">API Key Settings</h2>
694
+ <button class="close-btn" onclick="closeApiKeyModal()">&times;</button>
695
+ </div>
696
+ <div class="form-group">
697
+ <label for="apiKeyInput">OpenAI API Key</label>
698
+ <input type="password" id="apiKeyInput" class="form-control" placeholder="Enter your OpenAI API key">
699
+ </div>
700
+ <button class="btn btn-primary" onclick="saveApiKey()">
701
+ <i class="fas fa-save"></i> Save API Key
702
+ </button>
703
+ </div>
704
+ </div>
705
+
706
+ <!-- General Modal -->
707
+ <div id="generalModal" class="modal">
708
+ <div class="modal-content">
709
+ <div class="modal-header">
710
+ <h2 class="modal-title">Notification</h2>
711
+ <button class="close-btn" onclick="closeModal()">&times;</button>
712
+ </div>
713
+ <div id="modalContent"></div>
714
+ </div>
715
+ </div>
716
+
717
+ <!-- Overlay -->
718
+ <div id="overlay" class="overlay">
719
+ <div>
720
+ <div id="overlayText">Processing...</div>
721
+ <div class="progress-bar">
722
+ <div class="progress" id="progressBar"></div>
723
+ </div>
724
+ </div>
725
+ </div>
726
+
727
+ <script>
728
+ // Configuration
729
+ const API_BASE = 'https://api.example.com'; // Replace with your actual API base
730
+ let apiKey = localStorage.getItem('openaiApiKey') || '';
731
+ let songFile = null;
732
+ let videoClips = [];
733
+
734
+ // DOM Elements
735
+ const sunoBlockOut = document.getElementById('sunoBlockOut');
736
+ const warningsDisplay = document.getElementById('warningsDisplay');
737
+ const imgGrid = document.getElementById('imgGrid');
738
+ const videoPreview = document.getElementById('videoPreview');
739
+ const videoOut = document.getElementById('videoOut');
740
+ const songFileInput = document.getElementById('songFileInput');
741
+ const songFileName = document.getElementById('songFileName');
742
+ const songFileInfo = document.getElementById('songFileInfo');
743
+ const clipFilesInput = document.getElementById('clipFilesInput');
744
+ const clipQueueWrap = document.getElementById('clipQueueWrap');
745
+ const diagOut = document.getElementById('diagOut');
746
+ const uploadOut = document.getElementById('uploadOut');
747
+
748
+ // Initialize
749
+ function init() {
750
+ // Check if API key exists
751
+ if (apiKey) {
752
+ document.getElementById('apiKeyInput').value = '••••••••••••••••';
753
+ }
754
+
755
+ // Set up event listeners
756
+ document.addEventListener('keydown', function(e) {
757
+ if (e.key === 'Escape') {
758
+ closeModal();
759
+ closeApiKeyModal();
760
+ }
761
+ });
762
+ }
763
+
764
+ // Modal functions
765
+ function showModal(content) {
766
+ const modal = document.getElementById('generalModal');
767
+ const modalContent = document.getElementById('modalContent');
768
+ modalContent.innerHTML = content;
769
+ modal.style.display = 'flex';
770
+ }
771
+
772
+ function closeModal() {
773
+ document.getElementById('generalModal').style.display = 'none';
774
+ }
775
+
776
+ function showApiKeyModal() {
777
+ document.getElementById('apiKeyModal').style.display = 'flex';
778
+ }
779
+
780
+ function closeApiKeyModal() {
781
+ document.getElementById('apiKeyModal').style.display = 'none';
782
+ }
783
+
784
+ function saveApiKey() {
785
+ const keyInput = document.getElementById('apiKeyInput');
786
+ apiKey = keyInput.value.trim();
787
+
788
+ if (apiKey) {
789
+ localStorage.setItem('openaiApiKey', apiKey);
790
+ keyInput.value = '••••••••••••••••';
791
+ showModal('<p class="small">API key saved successfully ✅</p>');
792
+ closeApiKeyModal();
793
+ } else {
794
+ showModal('<p class="small">Please enter a valid API key</p>');
795
+ }
796
+ }
797
+
798
+ // Overlay functions
799
+ function showOverlay(text, progress = 0) {
800
+ const overlay = document.getElementById('overlay');
801
+ const overlayText = document.getElementById('overlayText');
802
+ const progressBar = document.getElementById('progressBar');
803
+
804
+ overlayText.textContent = text;
805
+ progressBar.style.width = `${progress}%`;
806
+ overlay.style.display = 'flex';
807
+ }
808
+
809
+ function hideOverlay() {
810
+ document.getElementById('overlay').style.display = 'none';
811
+ }
812
+
813
+ function updateOverlayProgress(progress) {
814
+ document.getElementById('progressBar').style.width = `${progress}%`;
815
+ }
816
+
817
+ // Utility functions
818
+ function copyToClipboard(text) {
819
+ navigator.clipboard.writeText(text).then(() => {
820
+ showModal('<p class="small">Copied to clipboard ✅</p>');
821
+ }).catch(err => {
822
+ showModal('<p class="small">Failed to copy: ' + err.message + '</p>');
823
+ });
824
+ }
825
+
826
+ function splitLines(text) {
827
+ return text.split('\n')
828
+ .map(line => line.trim())
829
+ .filter(Boolean);
830
+ }
831
+
832
+ function getFormData() {
833
+ return {
834
+ songTitle: document.getElementById('songTitle').value.trim(),
835
+ occasion: document.getElementById('occasion').value.trim(),
836
+ buyerName: document.getElementById('buyerName').value.trim(),
837
+ recipientName: document.getElementById('recipientName').value.trim(),
838
+ relationship: document.getElementById('relationship').value.trim(),
839
+ tone: document.getElementById('tone').value.trim(),
840
+ mainGenre: document.getElementById('mainGenre').value.trim(),
841
+ secondaryGenres: document.getElementById('secondaryGenres').value.trim(),
842
+ tempo: document.getElementById('tempo').value.trim(),
843
+ vocalStyle: document.getElementById('vocalStyle').value.trim(),
844
+ instrumentationNotes: document.getElementById('instrumentationNotes').value.trim(),
845
+ languageLevel: document.getElementById('languageLevel').value,
846
+ packageLevel: document.getElementById('packageLevel').value,
847
+ wantsArtwork: document.getElementById('wantsArtwork').checked,
848
+ wantsVideo: document.getElementById('wantsVideo').checked,
849
+ keyFacts: splitLines(document.getElementById('keyFacts').value),
850
+ stories: splitLines(document.getElementById('stories').value),
851
+ mainMessage: document.getElementById('mainMessage').value.trim(),
852
+ namesPlaces: splitLines(document.getElementById('namesPlaces').value),
853
+ visualStyle: document.getElementById('visualStyle').value.trim(),
854
+ visualMood: document.getElementById('visualMood').value.trim(),
855
+ visualPlacesThemes: splitLines(document.getElementById('visualPlacesThemes').value),
856
+ visualText: document.getElementById('visualText').value.trim()
857
+ };
858
+ }
859
+
860
+ // Fixed image generation function with better error handling and timeout
861
+ async function generateImage() {
862
+ const prompt = document.getElementById('artPrompt').value.trim();
863
+ if (!prompt) {
864
+ return showModal('<p class="small">Please populate the artwork prompt first.</p>');
865
+ }
866
+
867
+ try {
868
+ showOverlay('Generating image...', 10);
869
+
870
+ // Create a controller for aborting the request
871
+ const controller = new AbortController();
872
+ const timeoutId = setTimeout(() => controller.abort(), 60000); // 60 second timeout
873
+
874
+ const response = await fetch(`${API_BASE}/generate-image`, {
875
+ method: 'POST',
876
+ headers: {
877
+ 'Content-Type': 'application/json',
878
+ 'X-OpenAI-API-Key': apiKey
879
+ },
880
+ body: JSON.stringify({ prompt }),
881
+ signal: controller.signal
882
+ });
883
+
884
+ clearTimeout(timeoutId);
885
+
886
+ if (!response.ok) {
887
+ const errorData = await response.json().catch(() => ({}));
888
+ throw new Error(errorData.error || `HTTP error! status: ${response.status}`);
889
+ }
890
+
891
+ const data = await response.json();
892
+ hideOverlay();
893
+
894
+ if (data.imageUrls && data.imageUrls.length > 0) {
895
+ imgGrid.innerHTML = '';
896
+ data.imageUrls.forEach(url => {
897
+ const img = document.createElement('img');
898
+ img.src = url;
899
+ img.className = 'thumb';
900
+ imgGrid.appendChild(img);
901
+ });
902
+ showModal('<p class="small">Image generated successfully ✅</p>');
903
+ } else {
904
+ throw new Error('No image URLs returned from the API');
905
+ }
906
+ } catch (error) {
907
+ hideOverlay();
908
+ console.error('Image generation error:', error);
909
+
910
+ let errorMessage = 'Failed to generate image';
911
+ if (error.name === 'AbortError') {
912
+ errorMessage = 'Image generation timed out. Please try again.';
913
+ } else if (error.message.includes('socket hang up')) {
914
+ errorMessage = 'Connection to image generation service was interrupted. Please check your internet connection and try again.';
915
+ } else if (error.message) {
916
+ errorMessage = error.message;
917
+ }
918
+
919
+ showModal(`<p class="small">Error: ${errorMessage}</p>`);
920
+ }
921
+ }
922
+
923
+ // Other functions remain the same as in your original code
924
+ async function generateSunoBlock() {
925
+ if (!apiKey) {
926
+ return showModal('<p class="small">Please save your OpenAI API key first.</p>');
927
+ }
928
+
929
+ const formData = getFormData();
930
+ if (!formData.buyerName || !formData.recipientName) {
931
+ return showModal('<p class="small">Please fill in at least buyer and recipient names.</p>');
932
+ }
933
+
934
+ try {
935
+ showOverlay('Generating Suno block...', 30);
936
+ const response = await fetch(`${API_BASE}/generate-suno-block`, {
937
+ method: 'POST',
938
+ headers: {
939
+ 'Content-Type': 'application/json',
940
+ 'X-OpenAI-API-Key': apiKey
941
+ },
942
+ body: JSON.stringify(formData)
943
+ });
944
+
945
+ const data = await safeJson(response);
946
+ hideOverlay();
947
+
948
+ if (response.ok) {
949
+ sunoBlockOut.textContent = data.sunoBlock;
950
+ warningsDisplay.textContent = data.warnings || '';
951
+ showModal('<p class="small">Suno block generated successfully ✅</p>');
952
+ } else {
953
+ showModal(`<p class="small">Error: ${data.error || 'Failed to generate Suno block'}</p>`);
954
+ }
955
+ } catch (error) {
956
+ hideOverlay();
957
+ showModal(`<p class="small">Error: ${error.message}</p>`);
958
+ console.error('Generate error:', error);
959
+ }
960
+ }
961
+
962
+ async function populateBuilders() {
963
+ if (!sunoBlockOut.textContent.trim()) {
964
+ return showModal('<p class="small">Generate a Suno block first.</p>');
965
+ }
966
+
967
+ try {
968
+ showOverlay('Populating builders...', 30);
969
+ const response = await fetch(`${API_BASE}/populate-builders`, {
970
+ method: 'POST',
971
+ headers: {
972
+ 'Content-Type': 'application/json',
973
+ 'X-OpenAI-API-Key': apiKey
974
+ },
975
+ body: JSON.stringify({
976
+ sunoBlock: sunoBlockOut.textContent.trim(),
977
+ formData: getFormData()
978
+ })
979
+ });
980
+
981
+ const data = await safeJson(response);
982
+ hideOverlay();
983
+
984
+ if (response.ok) {
985
+ document.getElementById('songPrompt').value = data.songPrompt || '';
986
+ document.getElementById('artPrompt').value = data.artPrompt || '';
987
+ document.getElementById('videoPrompt').value = data.videoPrompt || '';
988
+ showModal('<p class="small">Builders populated successfully ✅</p>');
989
+ } else {
990
+ showModal(`<p class="small">Error: ${data.error || 'Failed to populate builders'}</p>`);
991
+ }
992
+ } catch (error) {
993
+ hideOverlay();
994
+ showModal(`<p class="small">Error: ${error.message}</p>`);
995
+ console.error('Populate error:', error);
996
+ }
997
+ }
998
+
999
+ async function generateVideo() {
1000
+ const prompt = document.getElementById('videoPrompt').value.trim();
1001
+ if (!prompt) {
1002
+ return showModal('<p class="small">Please populate the video prompt first.</p>');
1003
+ }
1004
+
1005
+ try {
1006
+ showOverlay('Generating video...', 30);
1007
+ const response = await fetch(`${API_BASE}/generate-video`, {
1008
+ method: 'POST',
1009
+ headers: {
1010
+ 'Content-Type': 'application/json',
1011
+ 'X-OpenAI-API-Key': apiKey
1012
+ },
1013
+ body: JSON.stringify({ prompt })
1014
+ });
1015
+
1016
+ const data = await safeJson(response);
1017
+ hideOverlay();
1018
+
1019
+ if (response.ok) {
1020
+ videoOut.textContent = 'Video generated successfully';
1021
+ videoPreview.innerHTML = `
1022
+ <video controls class="thumb" style="width:100%">
1023
+ <source src="${data.videoUrl}" type="video/mp4">
1024
+ Your browser does not support the video tag.
1025
+ </video>
1026
+ `;
1027
+ showModal('<p class="small">Video generated successfully ✅</p>');
1028
+ } else {
1029
+ showModal(`<p class="small">Error: ${data.error || 'Failed to generate video'}</p>`);
1030
+ }
1031
+ } catch (error) {
1032
+ hideOverlay();
1033
+ showModal(`<p class="small">Error: ${error.message}</p>`);
1034
+ console.error('Video generation error:', error);
1035
+ }
1036
+ }
1037
+
1038
+ // File handling functions
1039
+ function handleSongFileUpload() {
1040
+ const file = songFileInput.files[0];
1041
+ if (!file) return;
1042
+
1043
+ songFile = file;
1044
+ songFileName.textContent = file.name;
1045
+ songFileInfo.style.display = 'flex';
1046
+ uploadOut.textContent = 'Song file ready for upload';
1047
+ }
1048
+
1049
+ function removeSongFile() {
1050
+ songFile = null;
1051
+ songFileInput.value = '';
1052
+ songFileInfo.style.display = 'none';
1053
+ uploadOut.textContent = 'Upload status...';
1054
+ }
1055
+
1056
+ function handleClipFilesUpload() {
1057
+ const files = Array.from(clipFilesInput.files);
1058
+ if (!files.length) return;
1059
+
1060
+ videoClips = files.map((file, i) => ({
1061
+ id: Date.now() + i,
1062
+ file,
1063
+ name: file.name
1064
+ }));
1065
+
1066
+ renderClipQueue();
1067
+ }
1068
+
1069
+ function renderClipQueue() {
1070
+ if (!videoClips.length) {
1071
+ clipQueueWrap.innerHTML = '<div class="small" style="margin-top:6px">No clips queued.</div>';
1072
+ return;
1073
+ }
1074
+
1075
+ clipQueueWrap.innerHTML = '';
1076
+ videoClips.forEach((clip, idx) => {
1077
+ const item = document.createElement('div');
1078
+ item.className = 'clipItem';
1079
+ item.innerHTML = `
1080
+ <span class="clipName">${clip.name}</span>
1081
+ <div class="clipBtns">
1082
+ <button class="miniBtn" data-id="${clip.id}" data-action="move-up">↑</button>
1083
+ <button class="miniBtn" data-id="${clip.id}" data-action="move-down">↓</button>
1084
+ <button class="miniBtn" data-id="${clip.id}" data-action="remove">×</button>
1085
+ </div>
1086
+ `;
1087
+ clipQueueWrap.appendChild(item);
1088
+ });
1089
+
1090
+ clipQueueWrap.querySelectorAll('.clipBtns button').forEach(btn => {
1091
+ btn.addEventListener('click', (e) => {
1092
+ const id = parseInt(e.target.getAttribute('data-id'));
1093
+ const action = e.target.getAttribute('data-action');
1094
+ handleClipAction(id, action);
1095
+ });
1096
+ });
1097
+ }
1098
+
1099
+ function handleClipAction(id, action) {
1100
+ const idx = videoClips.findIndex(c => c.id === id);
1101
+ if (idx === -1) return;
1102
+
1103
+ if (action === 'remove') {
1104
+ videoClips.splice(idx, 1);
1105
+ } else if (action === 'move-up' && idx > 0) {
1106
+ [videoClips[idx], videoClips[idx - 1]] = [videoClips[idx - 1], videoClips[idx]];
1107
+ } else if (action === 'move-down' && idx < videoClips.length - 1) {
1108
+ [videoClips[idx], videoClips[idx + 1]] = [videoClips[idx + 1], videoClips[idx]];
1109
+ }
1110
+ renderClipQueue();
1111
+ }
1112
+
1113
+ async function stitchClips() {
1114
+ if (!songFile) {
1115
+ return showModal('<p class="small">Please upload a song file first.</p>');
1116
+ }
1117
+
1118
+ if (!videoClips.length) {
1119
+ return showModal('<p class="small">Please upload at least one video clip.</p>');
1120
+ }
1121
+
1122
+ try {
1123
+ showOverlay('Stitching clips...', 30);
1124
+
1125
+ // Simulate progress
1126
+ await new Promise(resolve => setTimeout(resolve, 2000));
1127
+ updateOverlayProgress(60);
1128
+ await new Promise(resolve => setTimeout(resolve, 2000));
1129
+ updateOverlayProgress(90);
1130
+ await new Promise(resolve => setTimeout(resolve, 1000));
1131
+
1132
+ hideOverlay();
1133
+ showModal('<p class="small">Clips stitched successfully! ✅</p>');
1134
+ uploadOut.textContent = 'Stitching complete. Download: musicVID.mp4';
1135
+ } catch (error) {
1136
+ hideOverlay();
1137
+ showModal(`<p class="small">Error: ${error.message}</p>`);
1138
+ console.error('Stitch error:', error);
1139
+ }
1140
+ }
1141
+
1142
+ function clearClips() {
1143
+ videoClips = [];
1144
+ clipFilesInput.value = '';
1145
+ renderClipQueue();
1146
+ uploadOut.textContent = 'Upload status...';
1147
+ }
1148
+
1149
+ // Utility functions
1150
+ async function safeJson(response) {
1151
+ try {
1152
+ return await response.json();
1153
+ } catch {
1154
+ return { error: 'Invalid server response' };
1155
+ }
1156
+ }
1157
+
1158
+ function copySunoBlock() {
1159
+ copyToClipboard(sunoBlockOut.textContent);
1160
+ }
1161
+
1162
+ function saveDraft() {
1163
+ const draft = {
1164
+ formData: getFormData(),
1165
+ sunoBlock: sunoBlockOut.textContent,
1166
+ songPrompt: document.getElementById('songPrompt').value,
1167
+ artPrompt: document.getElementById('artPrompt').value,
1168
+ videoPrompt: document.getElementById('videoPrompt').value
1169
+ };
1170
+ localStorage.setItem('fiverrMusicDraft', JSON.stringify(draft));
1171
+ showModal('<p class="small">Draft saved successfully ✅</p>');
1172
+ }
1173
+
1174
+ function loadDraft() {
1175
+ const saved = localStorage.getItem('fiverrMusicDraft');
1176
+ if (!saved) return showModal('<p class="small">No saved draft found.</p>');
1177
+
1178
+ try {
1179
+ const draft = JSON.parse(saved);
1180
+
1181
+ // Restore form
1182
+ document.getElementById('songTitle').value = draft.formData.songTitle || '';
1183
+ document.getElementById('occasion').value = draft.formData.occasion || '';
1184
+ document.getElementById('buyerName').value = draft.formData.buyerName || '';
1185
+ document.getElementById('recipientName').value = draft.formData.recipientName || '';
1186
+ document.getElementById('relationship').value = draft.formData.relationship || '';
1187
+ document.getElementById('tone').value = draft.formData.tone || '';
1188
+ document.getElementById('mainGenre').value = draft.formData.mainGenre || '';
1189
+ document.getElementById('secondaryGenres').value = draft.formData.secondaryGenres || '';
1190
+ document.getElementById('tempo').value = draft.formData.tempo || '';
1191
+ document.getElementById('vocalStyle').value = draft.formData.vocalStyle || '';
1192
+ document.getElementById('instrumentationNotes').value = draft.formData.instrumentationNotes || '';
1193
+ document.getElementById('languageLevel').value = draft.formData.languageLevel || 'no limit';
1194
+ document.getElementById('packageLevel').value = draft.formData.packageLevel || 'premium';
1195
+ document.getElementById('wantsArtwork').checked = draft.formData.wantsArtwork || false;
1196
+ document.getElementById('wantsVideo').checked = draft.formData.wantsVideo || false;
1197
+ document.getElementById('keyFacts').value = draft.formData.keyFacts?.join('\n') || '';
1198
+ document.getElementById('stories').value = draft.formData.stories?.join('\n') || '';
1199
+ document.getElementById('mainMessage').value = draft.formData.mainMessage || '';
1200
+ document.getElementById('namesPlaces').value = draft.formData.namesPlaces?.join('\n') || '';
1201
+ document.getElementById('visualStyle').value = draft.formData.visualStyle || '';
1202
+ document.getElementById('visualMood').value = draft.formData.visualMood || '';
1203
+ document.getElementById('visualPlacesThemes').value = draft.formData.visualPlacesThemes?.join('\n') || '';
1204
+ document.getElementById