AZILS commited on
Commit
bfb7a77
·
verified ·
1 Parent(s): 0a665ec

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +569 -139
app.py CHANGED
@@ -5,6 +5,7 @@
5
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
  <title>YouTube Shorts Generator</title>
7
  <script src="https://cdn.tailwindcss.com"></script>
 
8
  <script>
9
  tailwind.config = {
10
  darkMode: 'class',
@@ -35,6 +36,42 @@
35
  border-color: rgba(255, 255, 255, 0.1);
36
  border-left-color: #5D5CDE;
37
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
38
  </style>
39
  </head>
40
  <body class="bg-white dark:bg-gray-900 text-gray-800 dark:text-gray-200 min-h-screen">
@@ -42,62 +79,142 @@
42
  <h1 class="text-3xl font-bold mb-4 text-center text-primary">YouTube Shorts Generator</h1>
43
 
44
  <div class="mb-8 bg-gray-100 dark:bg-gray-800 p-6 rounded-lg shadow-md">
45
- <div class="mb-4">
46
- <label for="niche" class="block text-sm font-medium mb-1">Niche/Topic</label>
47
- <input type="text" id="niche" placeholder="E.g., Fitness tips, Technology facts, Travel destinations" class="w-full px-4 py-2 rounded-md border border-gray-300 dark:border-gray-700 bg-white dark:bg-gray-700 text-base">
48
- </div>
49
-
50
- <div class="mb-4">
51
- <label for="language" class="block text-sm font-medium mb-1">Language</label>
52
- <select id="language" class="w-full px-4 py-2 rounded-md border border-gray-300 dark:border-gray-700 bg-white dark:bg-gray-700 text-base">
53
- <option value="English">English</option>
54
- <option value="Spanish">Spanish</option>
55
- <option value="French">French</option>
56
- <option value="German">German</option>
57
- <option value="Italian">Italian</option>
58
- <option value="Portuguese">Portuguese</option>
59
- <option value="Russian">Russian</option>
60
- <option value="Japanese">Japanese</option>
61
- <option value="Chinese">Chinese</option>
62
- <option value="Hindi">Hindi</option>
63
- </select>
64
- </div>
65
-
66
- <div class="grid grid-cols-1 md:grid-cols-3 gap-4 mb-6">
67
- <div>
68
- <label for="text-generator" class="block text-sm font-medium mb-1">Text Generator</label>
69
- <select id="text-generator" class="w-full px-4 py-2 rounded-md border border-gray-300 dark:border-gray-700 bg-white dark:bg-gray-700 text-base">
70
- <option value="Claude-3.7-Sonnet">Claude-3.7-Sonnet</option>
71
- <option value="GPT-4o">GPT-4o</option>
72
- <option value="GPT-4o-mini">GPT-4o-mini</option>
73
- <option value="Gemini-2.0-Flash">Gemini-2.0-Flash</option>
74
- </select>
75
- </div>
76
-
77
- <div>
78
- <label for="image-generator" class="block text-sm font-medium mb-1">Image Generator</label>
79
- <select id="image-generator" class="w-full px-4 py-2 rounded-md border border-gray-300 dark:border-gray-700 bg-white dark:bg-gray-700 text-base">
80
- <option value="FLUX-pro-1.1">FLUX-pro-1.1</option>
81
- <option value="FLUX-schnell">FLUX-schnell</option>
82
- <option value="Dall-E-3">Dall-E-3</option>
83
- </select>
84
  </div>
85
 
86
- <div>
87
- <label for="voice-generator" class="block text-sm font-medium mb-1">Voice Generator</label>
88
- <select id="voice-generator" class="w-full px-4 py-2 rounded-md border border-gray-300 dark:border-gray-700 bg-white dark:bg-gray-700 text-base">
89
- <option value="ElevenLabs">ElevenLabs</option>
90
- <option value="PlayAI-Dialog">PlayAI-Dialog</option>
 
 
 
 
 
 
 
 
 
 
 
 
 
91
  </select>
92
  </div>
93
  </div>
94
 
95
- <div>
96
- <label for="voice-name" class="block text-sm font-medium mb-1">Voice Name (Optional)</label>
97
- <input type="text" id="voice-name" placeholder="E.g., Sarah, Brian, Lily, Monika Sogam" class="w-full px-4 py-2 rounded-md border border-gray-300 dark:border-gray-700 bg-white dark:bg-gray-700 text-base">
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
98
  </div>
99
 
100
- <button id="generate-btn" class="mt-6 w-full bg-primary hover:bg-opacity-90 text-white py-3 px-4 rounded-md font-medium transition duration-200">
101
  Generate Video
102
  </button>
103
  </div>
@@ -111,10 +228,31 @@
111
  <div id="results-container" class="hidden bg-gray-100 dark:bg-gray-800 p-6 rounded-lg shadow-md">
112
  <h2 class="text-xl font-bold mb-3">Generated Video</h2>
113
 
114
- <div id="video-player-container" class="mb-6 relative pt-[56.25%]">
115
- <video id="video-player" controls class="absolute top-0 left-0 w-full h-full rounded-lg">
116
- Your browser does not support the video tag.
117
- </video>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
118
  </div>
119
 
120
  <div class="grid grid-cols-1 md:grid-cols-2 gap-4">
@@ -127,6 +265,16 @@
127
  <p id="video-description" class="bg-white dark:bg-gray-700 p-3 rounded-md h-24 overflow-y-auto"></p>
128
  </div>
129
  </div>
 
 
 
 
 
 
 
 
 
 
130
  </div>
131
  </div>
132
 
@@ -144,15 +292,171 @@
144
  }
145
  });
146
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
147
  // Handler for generating videos
148
  document.getElementById('generate-btn').addEventListener('click', async function() {
149
  // Get input values
150
  const niche = document.getElementById('niche').value.trim();
151
  const language = document.getElementById('language').value;
 
 
152
  const textGenerator = document.getElementById('text-generator').value;
 
153
  const imageGenerator = document.getElementById('image-generator').value;
154
- const voiceGenerator = document.getElementById('voice-generator').value;
155
- const voiceName = document.getElementById('voice-name').value.trim();
 
 
 
 
 
156
 
157
  // Validation
158
  if (!niche) {
@@ -166,39 +470,54 @@
166
  document.getElementById('results-container').classList.add('hidden');
167
 
168
  try {
169
- // Updates for the loading message
170
- updateProgress('Generating topic...');
171
- await new Promise(resolve => setTimeout(resolve, 1000));
172
 
173
  // Step 1: Generate topic
174
- const topic = await generateTopic(niche, textGenerator);
 
175
 
176
  // Step 2: Generate script
177
  updateProgress('Creating script...');
178
- const script = await generateScript(topic, language, textGenerator);
179
 
180
  // Step 3: Generate metadata
181
  updateProgress('Creating title and description...');
182
- const metadata = await generateMetadata(topic, script, textGenerator);
183
 
184
  // Step 4: Generate image prompts
185
  updateProgress('Creating image prompts...');
186
- const imagePrompts = await generateImagePrompts(topic, script, textGenerator);
187
 
188
  // Step 5: Generate images
189
  updateProgress('Generating images...');
190
- const imageUrls = await generateImages(imagePrompts, imageGenerator);
191
 
192
  // Step 6: Generate speech
193
  updateProgress('Creating voiceover...');
194
- const audioUrl = await generateSpeech(script, language, voiceGenerator, voiceName);
195
 
196
- // Step 7: Generate video
197
- updateProgress('Creating final video...');
198
- const videoUrl = await generateVideo(imageUrls, audioUrl, script);
199
 
200
- // Display results
201
- displayResults(videoUrl, metadata.title, metadata.description);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
202
 
203
  } catch (error) {
204
  console.error('Error:', error);
@@ -212,11 +531,11 @@
212
  }
213
 
214
  // Function to generate topic based on niche
215
- async function generateTopic(niche, textGeneratorModel) {
216
  try {
217
  const prompt = `Please generate a specific video idea that takes about the following topic: ${niche}. Make it exactly one sentence. Only return the topic, nothing else.`;
218
 
219
- // Use Poe API to send user message
220
  const handlerId = 'topic-generation-handler';
221
  let topicResult = '';
222
 
@@ -231,7 +550,7 @@
231
  });
232
 
233
  // Send request to generate topic
234
- await window.Poe.sendUserMessage(`@${textGeneratorModel} ${prompt}`, {
235
  handler: handlerId,
236
  stream: false,
237
  openChat: false
@@ -250,7 +569,7 @@
250
  }
251
 
252
  // Function to generate script based on topic
253
- async function generateScript(topic, language, textGeneratorModel) {
254
  try {
255
  const prompt = `
256
  Generate a script for youtube shorts video, depending on the subject of the video.
@@ -284,7 +603,7 @@
284
  });
285
 
286
  // Send request to generate script
287
- await window.Poe.sendUserMessage(`@${textGeneratorModel} ${prompt}`, {
288
  handler: handlerId,
289
  stream: false,
290
  openChat: false
@@ -303,7 +622,7 @@
303
  }
304
 
305
  // Function to generate metadata (title and description)
306
- async function generateMetadata(topic, script, textGeneratorModel) {
307
  try {
308
  const titlePrompt = `Please generate a YouTube Video Title for the following subject, including hashtags: ${topic}. Only return the title, nothing else. Limit the title under 100 characters.`;
309
 
@@ -322,7 +641,7 @@
322
  });
323
 
324
  // Send request to generate title
325
- await window.Poe.sendUserMessage(`@${textGeneratorModel} ${titlePrompt}`, {
326
  handler: titleHandlerId,
327
  stream: false,
328
  openChat: false
@@ -351,7 +670,7 @@
351
  });
352
 
353
  // Send request to generate description
354
- await window.Poe.sendUserMessage(`@${textGeneratorModel} ${descPrompt}`, {
355
  handler: descHandlerId,
356
  stream: false,
357
  openChat: false
@@ -373,17 +692,17 @@
373
  }
374
 
375
  // Function to generate image prompts
376
- async function generateImagePrompts(topic, script, textGeneratorModel) {
377
  try {
378
  const prompt = `
379
- Generate 5 Image Prompts for AI Image Generation,
380
  depending on the subject of a video.
381
  Subject: ${topic}
382
 
383
  The image prompts are to be returned as
384
  a JSON-Array of strings.
385
 
386
- Each search term should consist of a full sentence,
387
  always add the main subject of the video.
388
 
389
  Be emotional and use interesting adjectives to make the
@@ -411,7 +730,7 @@
411
  });
412
 
413
  // Send request to generate image prompts
414
- await window.Poe.sendUserMessage(`@${textGeneratorModel} ${prompt}`, {
415
  handler: handlerId,
416
  stream: false,
417
  openChat: false
@@ -445,7 +764,7 @@
445
  }
446
 
447
  // Function to generate images based on prompts
448
- async function generateImages(imagePrompts, imageGeneratorModel) {
449
  try {
450
  const imageUrls = [];
451
 
@@ -466,7 +785,7 @@
466
  });
467
 
468
  // Send request to generate image
469
- await window.Poe.sendUserMessage(`@${imageGeneratorModel} ${imagePrompts[i]}`, {
470
  handler: handlerId,
471
  stream: false,
472
  openChat: false
@@ -489,7 +808,7 @@
489
  }
490
 
491
  // Function to generate speech from script
492
- async function generateSpeech(script, language, voiceGeneratorModel, voiceName) {
493
  try {
494
  // Use Poe API to send user message
495
  const handlerId = 'speech-generation-handler';
@@ -507,12 +826,12 @@
507
 
508
  // Prepare the prompt
509
  let prompt = script;
510
- if (voiceName) {
511
- prompt += ` --voice ${voiceName}`;
512
  }
513
 
514
  // Send request to generate speech
515
- await window.Poe.sendUserMessage(`@${voiceGeneratorModel} ${prompt}`, {
516
  handler: handlerId,
517
  stream: false,
518
  openChat: false
@@ -529,45 +848,57 @@
529
  throw new Error('Failed to generate speech audio');
530
  }
531
 
532
- return audioUrl;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
533
  } catch (error) {
534
  console.error('Error generating speech:', error);
535
  throw new Error('Failed to generate speech');
536
  }
537
  }
538
 
539
- // Function to generate video by combining images and audio
540
- async function generateVideo(imageUrls, audioUrl, script) {
541
- // Here we would normally combine everything into a video
542
- // Since we can't actually do video processing in the browser easily,
543
- // we'll simulate it with image slideshow and audio
544
 
545
- try {
546
- // Create a simulated video player that shows images as a slideshow with audio
547
- // This is a simple mockup - in a real application, you would use a video processing service
 
 
 
548
 
549
- // For this demo, we'll just return the audio URL and use the first image
550
- // as a placeholder in the video player
551
-
552
- // In a real implementation, this is where you would call an external video
553
- // processing service or use a server-side component
554
-
555
- // Simulate processing time
556
- await new Promise(resolve => setTimeout(resolve, 3000));
557
 
558
- // Return a mock video URL (which is just the audio URL for this demo)
559
- return {
560
- audioUrl: audioUrl,
561
- imageUrls: imageUrls
562
- };
563
- } catch (error) {
564
- console.error('Error generating video:', error);
565
- throw new Error('Failed to generate video');
566
  }
 
 
567
  }
568
 
569
  // Function to display results
570
- function displayResults(videoData, title, description) {
571
  // Hide loading container
572
  document.getElementById('loading-container').classList.add('hidden');
573
  document.getElementById('loading-container').classList.remove('flex');
@@ -576,39 +907,138 @@
576
  document.getElementById('results-container').classList.remove('hidden');
577
 
578
  // Set title and description
579
- document.getElementById('video-title').textContent = title;
580
- document.getElementById('video-description').textContent = description;
 
 
 
 
 
 
 
 
 
 
 
 
 
581
 
582
- // Set up video player with images slideshow and audio
583
- const videoPlayer = document.getElementById('video-player');
 
 
 
 
 
 
 
 
 
584
 
585
- // Create a simple slideshow with the first image and audio
586
- if (videoData.imageUrls && videoData.imageUrls.length > 0) {
587
- // Set poster to first image
588
- videoPlayer.setAttribute('poster', videoData.imageUrls[0]);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
589
  }
590
 
591
- // Set audio source
592
- videoPlayer.innerHTML = '';
593
- const audioSource = document.createElement('source');
594
- audioSource.src = videoData.audioUrl;
595
- audioSource.type = 'audio/mpeg';
596
- videoPlayer.appendChild(audioSource);
597
 
598
- // Add text explaining this is a simulation
599
- const textOverlay = document.createElement('div');
600
- textOverlay.innerHTML = `
601
- <div class="absolute inset-0 flex items-center justify-center bg-black bg-opacity-50 text-white p-4 text-center">
602
- <div>
603
- <p class="font-bold mb-2">Audio Preview</p>
604
- <p class="text-sm">This is an audio preview. In a full implementation, this would be a video combining the generated images and audio.</p>
605
- </div>
606
- </div>
607
- `;
608
- videoPlayer.parentNode.appendChild(textOverlay);
 
 
 
 
 
 
 
 
 
 
 
609
 
610
- // Load and play
611
- videoPlayer.load();
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
612
  }
613
  </script>
614
  </body>
 
5
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
  <title>YouTube Shorts Generator</title>
7
  <script src="https://cdn.tailwindcss.com"></script>
8
+ <script src="https://cdn.jsdelivr.net/npm/marked/marked.min.js"></script>
9
  <script>
10
  tailwind.config = {
11
  darkMode: 'class',
 
36
  border-color: rgba(255, 255, 255, 0.1);
37
  border-left-color: #5D5CDE;
38
  }
39
+
40
+ .subtitle-word {
41
+ display: inline-block;
42
+ margin-right: 5px;
43
+ padding: 2px 5px;
44
+ border-radius: 4px;
45
+ font-family: 'Helvetica', sans-serif;
46
+ font-weight: bold;
47
+ color: white;
48
+ }
49
+
50
+ .subtitle-word.highlighted {
51
+ background-color: blue;
52
+ transition: background-color 0.1s ease;
53
+ }
54
+
55
+ .subtitle-container {
56
+ position: absolute;
57
+ bottom: 15%;
58
+ left: 10%;
59
+ width: 80%;
60
+ text-align: center;
61
+ z-index: 10;
62
+ font-size: 24px;
63
+ line-height: 1.5;
64
+ }
65
+
66
+ .accordion-content {
67
+ max-height: 0;
68
+ overflow: hidden;
69
+ transition: max-height 0.3s ease;
70
+ }
71
+
72
+ .accordion-content.open {
73
+ max-height: 1000px;
74
+ }
75
  </style>
76
  </head>
77
  <body class="bg-white dark:bg-gray-900 text-gray-800 dark:text-gray-200 min-h-screen">
 
79
  <h1 class="text-3xl font-bold mb-4 text-center text-primary">YouTube Shorts Generator</h1>
80
 
81
  <div class="mb-8 bg-gray-100 dark:bg-gray-800 p-6 rounded-lg shadow-md">
82
+ <!-- Main Required Inputs -->
83
+ <div class="mb-6">
84
+ <div class="mb-4">
85
+ <label for="niche" class="block text-sm font-medium mb-1">Niche/Topic <span class="text-red-500">*</span></label>
86
+ <input type="text" id="niche" placeholder="E.g., Fitness tips, Technology facts, Travel destinations" class="w-full px-4 py-2 rounded-md border border-gray-300 dark:border-gray-700 bg-white dark:bg-gray-700 text-base">
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
87
  </div>
88
 
89
+ <div class="mb-4">
90
+ <label for="language" class="block text-sm font-medium mb-1">Language <span class="text-red-500">*</span></label>
91
+ <select id="language" class="w-full px-4 py-2 rounded-md border border-gray-300 dark:border-gray-700 bg-white dark:bg-gray-700 text-base">
92
+ <option value="English">English</option>
93
+ <option value="Spanish">Spanish</option>
94
+ <option value="French">French</option>
95
+ <option value="German">German</option>
96
+ <option value="Italian">Italian</option>
97
+ <option value="Portuguese">Portuguese</option>
98
+ <option value="Russian">Russian</option>
99
+ <option value="Japanese">Japanese</option>
100
+ <option value="Chinese">Chinese</option>
101
+ <option value="Hindi">Hindi</option>
102
+ <option value="Arabic">Arabic</option>
103
+ <option value="Korean">Korean</option>
104
+ <option value="Dutch">Dutch</option>
105
+ <option value="Swedish">Swedish</option>
106
+ <option value="Turkish">Turkish</option>
107
  </select>
108
  </div>
109
  </div>
110
 
111
+ <!-- Advanced Options Accordion -->
112
+ <div class="mb-6">
113
+ <button id="advanced-options-toggle" class="flex justify-between items-center w-full bg-gray-200 dark:bg-gray-700 p-3 rounded-md mb-2 text-left">
114
+ <span class="font-medium">Advanced Options</span>
115
+ <span class="transition-transform duration-300 transform">▼</span>
116
+ </button>
117
+
118
+ <div id="advanced-options" class="accordion-content bg-gray-50 dark:bg-gray-800 p-4 rounded-md">
119
+ <div class="grid grid-cols-1 md:grid-cols-2 gap-4 mb-6">
120
+ <!-- Text Generator Options -->
121
+ <div>
122
+ <label for="text-generator" class="block text-sm font-medium mb-1">Text Generator</label>
123
+ <select id="text-generator" class="w-full px-4 py-2 rounded-md border border-gray-300 dark:border-gray-700 bg-white dark:bg-gray-700 text-base mb-2">
124
+ <option value="gemini">Google Gemini</option>
125
+ <option value="gpt3">GPT-3.5-Turbo</option>
126
+ <option value="gpt4">GPT-4</option>
127
+ <option value="claude">Claude</option>
128
+ <option value="llama">Llama</option>
129
+ <option value="mistral">Mistral</option>
130
+ <option value="command">Cohere Command</option>
131
+ </select>
132
+
133
+ <label for="text-model" class="block text-sm font-medium mb-1">Text Model</label>
134
+ <select id="text-model" class="w-full px-4 py-2 rounded-md border border-gray-300 dark:border-gray-700 bg-white dark:bg-gray-700 text-base">
135
+ <option value="gemini-2.0-flash">gemini-2.0-flash</option>
136
+ <option value="gemini-2.0-flash-lite">gemini-2.0-flash-lite</option>
137
+ <option value="gemini-1.5-flash">gemini-1.5-flash</option>
138
+ <option value="gemini-1.5-flash-8b">gemini-1.5-flash-8b</option>
139
+ <option value="gemini-1.5-pro">gemini-1.5-pro</option>
140
+ <!-- Other models will be populated based on text generator selection -->
141
+ </select>
142
+ </div>
143
+
144
+ <!-- Image Generator Options -->
145
+ <div>
146
+ <label for="image-generator" class="block text-sm font-medium mb-1">Image Generator</label>
147
+ <select id="image-generator" class="w-full px-4 py-2 rounded-md border border-gray-300 dark:border-gray-700 bg-white dark:bg-gray-700 text-base mb-2">
148
+ <option value="prodia">Prodia</option>
149
+ <option value="hercai">Hercai</option>
150
+ <option value="g4f">G4F</option>
151
+ <option value="segmind">Segmind</option>
152
+ <option value="pollinations">Pollinations</option>
153
+ </select>
154
+
155
+ <label for="image-model" class="block text-sm font-medium mb-1">Image Model</label>
156
+ <select id="image-model" class="w-full px-4 py-2 rounded-md border border-gray-300 dark:border-gray-700 bg-white dark:bg-gray-700 text-base">
157
+ <option value="sdxl">SDXL</option>
158
+ <option value="realvisxl">RealVisXL</option>
159
+ <option value="juggernaut">Juggernaut</option>
160
+ <option value="dalle">DALL-E</option>
161
+ <option value="midjourney">Midjourney</option>
162
+ <!-- Other models will be populated based on image generator selection -->
163
+ </select>
164
+ </div>
165
+ </div>
166
+
167
+ <div class="grid grid-cols-1 md:grid-cols-2 gap-4 mb-6">
168
+ <!-- Speech Generator Options -->
169
+ <div>
170
+ <label for="tts-engine" class="block text-sm font-medium mb-1">Speech Generator</label>
171
+ <select id="tts-engine" class="w-full px-4 py-2 rounded-md border border-gray-300 dark:border-gray-700 bg-white dark:bg-gray-700 text-base mb-2">
172
+ <option value="elevenlabs">ElevenLabs</option>
173
+ <option value="bark">Bark</option>
174
+ <option value="gtts">Google TTS</option>
175
+ <option value="openai">OpenAI TTS</option>
176
+ <option value="edge">Edge TTS</option>
177
+ <option value="local_tts">Local TTS</option>
178
+ <option value="xtts">XTTS</option>
179
+ <option value="rvc">RVC</option>
180
+ </select>
181
+
182
+ <label for="tts-voice" class="block text-sm font-medium mb-1">Voice</label>
183
+ <input type="text" id="tts-voice" placeholder="E.g., Sarah, Brian, Lily, Monika Sogam" class="w-full px-4 py-2 rounded-md border border-gray-300 dark:border-gray-700 bg-white dark:bg-gray-700 text-base">
184
+ </div>
185
+
186
+ <!-- Subtitle Options -->
187
+ <div>
188
+ <label for="subtitle-font" class="block text-sm font-medium mb-1">Subtitle Font</label>
189
+ <select id="subtitle-font" class="w-full px-4 py-2 rounded-md border border-gray-300 dark:border-gray-700 bg-white dark:bg-gray-700 text-base mb-2">
190
+ <option value="Helvetica-Bold">Helvetica Bold</option>
191
+ <option value="Arial-Bold">Arial Bold</option>
192
+ <option value="Roboto-Bold">Roboto Bold</option>
193
+ <option value="Verdana-Bold">Verdana Bold</option>
194
+ </select>
195
+
196
+ <div class="grid grid-cols-2 gap-2">
197
+ <div>
198
+ <label for="subtitle-color" class="block text-sm font-medium mb-1">Text Color</label>
199
+ <input type="color" id="subtitle-color" value="#FFFFFF" class="w-full h-10 px-1 py-1 rounded-md border border-gray-300 dark:border-gray-700 bg-white dark:bg-gray-700">
200
+ </div>
201
+ <div>
202
+ <label for="highlight-color" class="block text-sm font-medium mb-1">Highlight Color</label>
203
+ <input type="color" id="highlight-color" value="#0000FF" class="w-full h-10 px-1 py-1 rounded-md border border-gray-300 dark:border-gray-700 bg-white dark:bg-gray-700">
204
+ </div>
205
+ </div>
206
+ </div>
207
+ </div>
208
+
209
+ <!-- Image Prompt Options -->
210
+ <div class="mb-4">
211
+ <label for="prompt-count" class="block text-sm font-medium mb-1">Number of Image Prompts</label>
212
+ <input type="number" id="prompt-count" min="3" max="10" value="5" class="w-full px-4 py-2 rounded-md border border-gray-300 dark:border-gray-700 bg-white dark:bg-gray-700 text-base">
213
+ </div>
214
+ </div>
215
  </div>
216
 
217
+ <button id="generate-btn" class="w-full bg-primary hover:bg-opacity-90 text-white py-3 px-4 rounded-md font-medium transition duration-200">
218
  Generate Video
219
  </button>
220
  </div>
 
228
  <div id="results-container" class="hidden bg-gray-100 dark:bg-gray-800 p-6 rounded-lg shadow-md">
229
  <h2 class="text-xl font-bold mb-3">Generated Video</h2>
230
 
231
+ <div id="video-player-container" class="mb-6 relative pt-[56.25%] bg-black rounded-lg">
232
+ <!-- Video Player -->
233
+ <div id="video-container" class="absolute top-0 left-0 w-full h-full rounded-lg overflow-hidden">
234
+ <!-- Video or Images will be displayed here -->
235
+ <div id="image-slideshow" class="w-full h-full"></div>
236
+
237
+ <!-- Subtitles will be displayed here -->
238
+ <div id="subtitle-container" class="subtitle-container"></div>
239
+
240
+ <!-- Audio Player -->
241
+ <audio id="audio-player" class="hidden"></audio>
242
+ </div>
243
+
244
+ <!-- Video Controls -->
245
+ <div id="video-controls" class="absolute bottom-4 left-0 right-0 mx-auto flex justify-center items-center space-x-4">
246
+ <button id="play-btn" class="bg-white bg-opacity-20 hover:bg-opacity-30 text-white p-2 rounded-full">
247
+ <svg xmlns="http://www.w3.org/2000/svg" class="h-6 w-6" fill="none" viewBox="0 0 24 24" stroke="currentColor">
248
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M14.752 11.168l-3.197-2.132A1 1 0 0010 9.87v4.263a1 1 0 001.555.832l3.197-2.132a1 1 0 000-1.664z" />
249
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M21 12a9 9 0 11-18 0 9 9 0 0118 0z" />
250
+ </svg>
251
+ </button>
252
+ <div class="w-48 h-1 bg-white bg-opacity-20 rounded-full">
253
+ <div id="progress-bar" class="h-full bg-white rounded-full" style="width: 0%"></div>
254
+ </div>
255
+ </div>
256
  </div>
257
 
258
  <div class="grid grid-cols-1 md:grid-cols-2 gap-4">
 
265
  <p id="video-description" class="bg-white dark:bg-gray-700 p-3 rounded-md h-24 overflow-y-auto"></p>
266
  </div>
267
  </div>
268
+
269
+ <div class="mt-6">
270
+ <h3 class="font-medium mb-2">Script</h3>
271
+ <div id="video-script" class="bg-white dark:bg-gray-700 p-3 rounded-md"></div>
272
+ </div>
273
+
274
+ <div class="mt-6">
275
+ <h3 class="font-medium mb-2">Image Prompts</h3>
276
+ <div id="image-prompts" class="bg-white dark:bg-gray-700 p-3 rounded-md"></div>
277
+ </div>
278
  </div>
279
  </div>
280
 
 
292
  }
293
  });
294
 
295
+ // Advanced options accordion
296
+ document.getElementById('advanced-options-toggle').addEventListener('click', function() {
297
+ const content = document.getElementById('advanced-options');
298
+ content.classList.toggle('open');
299
+ this.querySelector('span:last-child').classList.toggle('rotate-180');
300
+ });
301
+
302
+ // Populate models based on generator selection
303
+ document.getElementById('text-generator').addEventListener('change', function() {
304
+ const modelSelect = document.getElementById('text-model');
305
+ modelSelect.innerHTML = '';
306
+
307
+ switch(this.value) {
308
+ case 'gemini':
309
+ addOptions(modelSelect, [
310
+ {value: 'gemini-2.0-flash', text: 'gemini-2.0-flash'},
311
+ {value: 'gemini-2.0-flash-lite', text: 'gemini-2.0-flash-lite'},
312
+ {value: 'gemini-1.5-flash', text: 'gemini-1.5-flash'},
313
+ {value: 'gemini-1.5-flash-8b', text: 'gemini-1.5-flash-8b'},
314
+ {value: 'gemini-1.5-pro', text: 'gemini-1.5-pro'}
315
+ ]);
316
+ break;
317
+ case 'gpt3':
318
+ addOptions(modelSelect, [
319
+ {value: 'gpt-3.5-turbo', text: 'gpt-3.5-turbo'},
320
+ {value: 'gpt-3.5-turbo-16k', text: 'gpt-3.5-turbo-16k'}
321
+ ]);
322
+ break;
323
+ case 'gpt4':
324
+ addOptions(modelSelect, [
325
+ {value: 'gpt-4', text: 'gpt-4'},
326
+ {value: 'gpt-4o', text: 'gpt-4o'},
327
+ {value: 'gpt-4-turbo', text: 'gpt-4-turbo'}
328
+ ]);
329
+ break;
330
+ case 'claude':
331
+ addOptions(modelSelect, [
332
+ {value: 'claude-3-opus-20240229', text: 'Claude 3 Opus'},
333
+ {value: 'claude-3-sonnet-20240229', text: 'Claude 3 Sonnet'},
334
+ {value: 'claude-3-haiku-20240307', text: 'Claude 3 Haiku'}
335
+ ]);
336
+ break;
337
+ case 'llama':
338
+ addOptions(modelSelect, [
339
+ {value: 'llama-3-70b-chat', text: 'Llama 3 70B'},
340
+ {value: 'llama-3-8b-chat', text: 'Llama 3 8B'},
341
+ {value: 'llama-2-70b-chat', text: 'Llama 2 70B'}
342
+ ]);
343
+ break;
344
+ case 'mistral':
345
+ addOptions(modelSelect, [
346
+ {value: 'mistral-large-latest', text: 'Mistral Large'},
347
+ {value: 'mistral-medium-latest', text: 'Mistral Medium'},
348
+ {value: 'mistral-small-latest', text: 'Mistral Small'}
349
+ ]);
350
+ break;
351
+ case 'command':
352
+ addOptions(modelSelect, [
353
+ {value: 'command-r', text: 'Command R'},
354
+ {value: 'command-r-plus', text: 'Command R+'},
355
+ {value: 'command-light', text: 'Command Light'}
356
+ ]);
357
+ break;
358
+ }
359
+ });
360
+
361
+ document.getElementById('image-generator').addEventListener('change', function() {
362
+ const modelSelect = document.getElementById('image-model');
363
+ modelSelect.innerHTML = '';
364
+
365
+ switch(this.value) {
366
+ case 'prodia':
367
+ addOptions(modelSelect, [
368
+ {value: 'sdxl', text: 'SDXL'},
369
+ {value: 'realvisxl', text: 'RealVisXL V4.0'},
370
+ {value: 'juggernaut', text: 'Juggernaut XL'},
371
+ {value: 'dreamshaper', text: 'DreamShaper 8'},
372
+ {value: 'portraitplus', text: 'Portrait+ V1'}
373
+ ]);
374
+ break;
375
+ case 'hercai':
376
+ addOptions(modelSelect, [
377
+ {value: 'v1', text: 'Stable Diffusion v1'},
378
+ {value: 'v2', text: 'Stable Diffusion v2'},
379
+ {value: 'v3', text: 'Stable Diffusion v3'},
380
+ {value: 'lexica', text: 'Lexica Diffusion'}
381
+ ]);
382
+ break;
383
+ case 'g4f':
384
+ addOptions(modelSelect, [
385
+ {value: 'dall-e-3', text: 'DALL-E 3'},
386
+ {value: 'dall-e-2', text: 'DALL-E 2'},
387
+ {value: 'imageapi', text: 'ImageAPI v1'}
388
+ ]);
389
+ break;
390
+ case 'segmind':
391
+ addOptions(modelSelect, [
392
+ {value: 'sdxl-turbo', text: 'SDXL Turbo'},
393
+ {value: 'realistic-vision', text: 'Realistic Vision'},
394
+ {value: 'sd3', text: 'Stable Diffusion 3'}
395
+ ]);
396
+ break;
397
+ case 'pollinations':
398
+ addOptions(modelSelect, [
399
+ {value: 'default', text: 'Default Model'}
400
+ ]);
401
+ break;
402
+ }
403
+ });
404
+
405
+ document.getElementById('tts-engine').addEventListener('change', function() {
406
+ const voiceInput = document.getElementById('tts-voice');
407
+
408
+ switch(this.value) {
409
+ case 'elevenlabs':
410
+ voiceInput.placeholder = 'E.g., Sarah, Brian, Lily, Monika Sogam';
411
+ break;
412
+ case 'openai':
413
+ voiceInput.placeholder = 'E.g., alloy, echo, fable, onyx, nova, shimmer';
414
+ break;
415
+ case 'edge':
416
+ voiceInput.placeholder = 'E.g., en-US-JennyNeural, en-US-GuyNeural';
417
+ break;
418
+ case 'gtts':
419
+ voiceInput.placeholder = 'Language code (en, es, fr, etc.)';
420
+ break;
421
+ case 'xtts':
422
+ voiceInput.placeholder = 'Speaker name or reference audio path';
423
+ break;
424
+ default:
425
+ voiceInput.placeholder = 'Voice name or identifier';
426
+ }
427
+ });
428
+
429
+ function addOptions(selectElement, options) {
430
+ options.forEach(option => {
431
+ const optElement = document.createElement('option');
432
+ optElement.value = option.value;
433
+ optElement.textContent = option.text;
434
+ selectElement.appendChild(optElement);
435
+ });
436
+ }
437
+
438
+ // Initialize model selects
439
+ document.getElementById('text-generator').dispatchEvent(new Event('change'));
440
+ document.getElementById('image-generator').dispatchEvent(new Event('change'));
441
+ document.getElementById('tts-engine').dispatchEvent(new Event('change'));
442
+
443
  // Handler for generating videos
444
  document.getElementById('generate-btn').addEventListener('click', async function() {
445
  // Get input values
446
  const niche = document.getElementById('niche').value.trim();
447
  const language = document.getElementById('language').value;
448
+
449
+ // Get advanced options
450
  const textGenerator = document.getElementById('text-generator').value;
451
+ const textModel = document.getElementById('text-model').value;
452
  const imageGenerator = document.getElementById('image-generator').value;
453
+ const imageModel = document.getElementById('image-model').value;
454
+ const ttsEngine = document.getElementById('tts-engine').value;
455
+ const ttsVoice = document.getElementById('tts-voice').value.trim();
456
+ const subtitleFont = document.getElementById('subtitle-font').value;
457
+ const subtitleColor = document.getElementById('subtitle-color').value;
458
+ const highlightColor = document.getElementById('highlight-color').value;
459
+ const promptCount = document.getElementById('prompt-count').value;
460
 
461
  // Validation
462
  if (!niche) {
 
470
  document.getElementById('results-container').classList.add('hidden');
471
 
472
  try {
473
+ // For the Poe environment, we'll create a simulated process that generates a video
474
+ // using text generation, image generation, and speech generation
 
475
 
476
  // Step 1: Generate topic
477
+ updateProgress('Generating topic...');
478
+ const topic = await generateTopic(niche, language);
479
 
480
  // Step 2: Generate script
481
  updateProgress('Creating script...');
482
+ const script = await generateScript(topic, language);
483
 
484
  // Step 3: Generate metadata
485
  updateProgress('Creating title and description...');
486
+ const metadata = await generateMetadata(topic, script);
487
 
488
  // Step 4: Generate image prompts
489
  updateProgress('Creating image prompts...');
490
+ const imagePrompts = await generateImagePrompts(topic, script, promptCount);
491
 
492
  // Step 5: Generate images
493
  updateProgress('Generating images...');
494
+ const imageUrls = await generateImages(imagePrompts);
495
 
496
  // Step 6: Generate speech
497
  updateProgress('Creating voiceover...');
498
+ const audioData = await generateSpeech(script, language, ttsEngine, ttsVoice);
499
 
500
+ // Step 7: Generate subtitles (simulated)
501
+ updateProgress('Creating subtitles...');
502
+ const subtitles = generateSimulatedSubtitles(script);
503
 
504
+ // Step 8: Display the results
505
+ updateProgress('Finalizing video...');
506
+ displayResults({
507
+ topic: topic,
508
+ script: script,
509
+ metadata: metadata,
510
+ imagePrompts: imagePrompts,
511
+ imageUrls: imageUrls,
512
+ audioUrl: audioData.url,
513
+ audioDuration: audioData.duration,
514
+ subtitles: subtitles,
515
+ subtitleSettings: {
516
+ font: subtitleFont,
517
+ color: subtitleColor,
518
+ highlightColor: highlightColor
519
+ }
520
+ });
521
 
522
  } catch (error) {
523
  console.error('Error:', error);
 
531
  }
532
 
533
  // Function to generate topic based on niche
534
+ async function generateTopic(niche, language) {
535
  try {
536
  const prompt = `Please generate a specific video idea that takes about the following topic: ${niche}. Make it exactly one sentence. Only return the topic, nothing else.`;
537
 
538
+ // For this demonstration, we'll use Claude
539
  const handlerId = 'topic-generation-handler';
540
  let topicResult = '';
541
 
 
550
  });
551
 
552
  // Send request to generate topic
553
+ await window.Poe.sendUserMessage(`@Claude-3.7-Sonnet ${prompt}`, {
554
  handler: handlerId,
555
  stream: false,
556
  openChat: false
 
569
  }
570
 
571
  // Function to generate script based on topic
572
+ async function generateScript(topic, language) {
573
  try {
574
  const prompt = `
575
  Generate a script for youtube shorts video, depending on the subject of the video.
 
603
  });
604
 
605
  // Send request to generate script
606
+ await window.Poe.sendUserMessage(`@Claude-3.7-Sonnet ${prompt}`, {
607
  handler: handlerId,
608
  stream: false,
609
  openChat: false
 
622
  }
623
 
624
  // Function to generate metadata (title and description)
625
+ async function generateMetadata(topic, script) {
626
  try {
627
  const titlePrompt = `Please generate a YouTube Video Title for the following subject, including hashtags: ${topic}. Only return the title, nothing else. Limit the title under 100 characters.`;
628
 
 
641
  });
642
 
643
  // Send request to generate title
644
+ await window.Poe.sendUserMessage(`@Claude-3.7-Sonnet ${titlePrompt}`, {
645
  handler: titleHandlerId,
646
  stream: false,
647
  openChat: false
 
670
  });
671
 
672
  // Send request to generate description
673
+ await window.Poe.sendUserMessage(`@Claude-3.7-Sonnet ${descPrompt}`, {
674
  handler: descHandlerId,
675
  stream: false,
676
  openChat: false
 
692
  }
693
 
694
  // Function to generate image prompts
695
+ async function generateImagePrompts(topic, script, count = 5) {
696
  try {
697
  const prompt = `
698
+ Generate ${count} Image Prompts for AI Image Generation,
699
  depending on the subject of a video.
700
  Subject: ${topic}
701
 
702
  The image prompts are to be returned as
703
  a JSON-Array of strings.
704
 
705
+ Each prompt should consist of a full sentence,
706
  always add the main subject of the video.
707
 
708
  Be emotional and use interesting adjectives to make the
 
730
  });
731
 
732
  // Send request to generate image prompts
733
+ await window.Poe.sendUserMessage(`@Claude-3.7-Sonnet ${prompt}`, {
734
  handler: handlerId,
735
  stream: false,
736
  openChat: false
 
764
  }
765
 
766
  // Function to generate images based on prompts
767
+ async function generateImages(imagePrompts) {
768
  try {
769
  const imageUrls = [];
770
 
 
785
  });
786
 
787
  // Send request to generate image
788
+ await window.Poe.sendUserMessage(`@FLUX-pro-1.1 ${imagePrompts[i]}`, {
789
  handler: handlerId,
790
  stream: false,
791
  openChat: false
 
808
  }
809
 
810
  // Function to generate speech from script
811
+ async function generateSpeech(script, language, ttsEngine = 'elevenlabs', ttsVoice = '') {
812
  try {
813
  // Use Poe API to send user message
814
  const handlerId = 'speech-generation-handler';
 
826
 
827
  // Prepare the prompt
828
  let prompt = script;
829
+ if (ttsVoice) {
830
+ prompt += ` --voice ${ttsVoice}`;
831
  }
832
 
833
  // Send request to generate speech
834
+ await window.Poe.sendUserMessage(`@ElevenLabs ${prompt}`, {
835
  handler: handlerId,
836
  stream: false,
837
  openChat: false
 
848
  throw new Error('Failed to generate speech audio');
849
  }
850
 
851
+ // Create an audio element to get the duration
852
+ const audio = new Audio();
853
+ audio.src = audioUrl;
854
+
855
+ // Wait for the audio to load to get its duration
856
+ const audioDuration = await new Promise((resolve) => {
857
+ audio.addEventListener('loadedmetadata', () => {
858
+ resolve(audio.duration);
859
+ });
860
+
861
+ // Fallback in case loadedmetadata doesn't fire
862
+ setTimeout(() => resolve(60), 5000); // Default to 60 seconds
863
+ });
864
+
865
+ return {
866
+ url: audioUrl,
867
+ duration: audioDuration
868
+ };
869
  } catch (error) {
870
  console.error('Error generating speech:', error);
871
  throw new Error('Failed to generate speech');
872
  }
873
  }
874
 
875
+ // Function to generate simulated subtitles (word-level timing)
876
+ function generateSimulatedSubtitles(script) {
877
+ // Split script into words
878
+ const words = script.split(/\s+/);
879
+ const subtitles = [];
880
 
881
+ // Simulate timing for each word (we'd normally get this from AssemblyAI)
882
+ let currentTime = 0;
883
+ for (let i = 0; i < words.length; i++) {
884
+ const word = words[i];
885
+ // Simulate word duration based on length
886
+ const duration = 0.2 + (word.length * 0.05);
887
 
888
+ subtitles.push({
889
+ word: word,
890
+ start: currentTime,
891
+ end: currentTime + duration
892
+ });
 
 
 
893
 
894
+ currentTime += duration;
 
 
 
 
 
 
 
895
  }
896
+
897
+ return subtitles;
898
  }
899
 
900
  // Function to display results
901
+ function displayResults(data) {
902
  // Hide loading container
903
  document.getElementById('loading-container').classList.add('hidden');
904
  document.getElementById('loading-container').classList.remove('flex');
 
907
  document.getElementById('results-container').classList.remove('hidden');
908
 
909
  // Set title and description
910
+ document.getElementById('video-title').textContent = data.metadata.title;
911
+ document.getElementById('video-description').textContent = data.metadata.description;
912
+
913
+ // Set script
914
+ document.getElementById('video-script').textContent = data.script;
915
+
916
+ // Set image prompts
917
+ const imagePromptsElement = document.getElementById('image-prompts');
918
+ imagePromptsElement.innerHTML = '';
919
+ data.imagePrompts.forEach((prompt, index) => {
920
+ const promptEl = document.createElement('div');
921
+ promptEl.className = 'mb-2';
922
+ promptEl.textContent = `${index + 1}. ${prompt}`;
923
+ imagePromptsElement.appendChild(promptEl);
924
+ });
925
 
926
+ // Set up image slideshow
927
+ const imageSlideshow = document.getElementById('image-slideshow');
928
+ imageSlideshow.innerHTML = '';
929
+ data.imageUrls.forEach((url, index) => {
930
+ const img = document.createElement('img');
931
+ img.src = url;
932
+ img.className = 'absolute top-0 left-0 w-full h-full object-cover transition-opacity duration-1000';
933
+ img.style.opacity = index === 0 ? '1' : '0';
934
+ img.dataset.index = index;
935
+ imageSlideshow.appendChild(img);
936
+ });
937
 
938
+ // Set up audio player
939
+ const audioPlayer = document.getElementById('audio-player');
940
+ audioPlayer.src = data.audioUrl;
941
+ audioPlayer.preload = 'auto';
942
+
943
+ // Set up subtitle container
944
+ const subtitleContainer = document.getElementById('subtitle-container');
945
+ subtitleContainer.innerHTML = '';
946
+
947
+ // Create elements for each word
948
+ data.subtitles.forEach(subtitle => {
949
+ const wordEl = document.createElement('span');
950
+ wordEl.className = 'subtitle-word';
951
+ wordEl.textContent = subtitle.word;
952
+ wordEl.dataset.start = subtitle.start;
953
+ wordEl.dataset.end = subtitle.end;
954
+ wordEl.style.color = data.subtitleSettings.color;
955
+ subtitleContainer.appendChild(wordEl);
956
+ });
957
+
958
+ // Set up video player controls
959
+ const playBtn = document.getElementById('play-btn');
960
+ const progressBar = document.getElementById('progress-bar');
961
+
962
+ let isPlaying = false;
963
+ let currentImageIndex = 0;
964
+ let slideInterval;
965
+
966
+ // Function to handle play/pause
967
+ function togglePlayPause() {
968
+ if (isPlaying) {
969
+ audioPlayer.pause();
970
+ clearInterval(slideInterval);
971
+ playBtn.innerHTML = `<svg xmlns="http://www.w3.org/2000/svg" class="h-6 w-6" fill="none" viewBox="0 0 24 24" stroke="currentColor">
972
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M14.752 11.168l-3.197-2.132A1 1 0 0010 9.87v4.263a1 1 0 001.555.832l3.197-2.132a1 1 0 000-1.664z" />
973
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M21 12a9 9 0 11-18 0 9 9 0 0118 0z" />
974
+ </svg>`;
975
+ } else {
976
+ audioPlayer.play();
977
+ playBtn.innerHTML = `<svg xmlns="http://www.w3.org/2000/svg" class="h-6 w-6" fill="none" viewBox="0 0 24 24" stroke="currentColor">
978
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M10 9v6m4-6v6m7-3a9 9 0 11-18 0 9 9 0 0118 0z" />
979
+ </svg>`;
980
+
981
+ // Set up image slideshow interval
982
+ const slideDuration = data.audioDuration / data.imageUrls.length;
983
+ slideInterval = setInterval(() => {
984
+ const images = imageSlideshow.querySelectorAll('img');
985
+ images[currentImageIndex].style.opacity = '0';
986
+ currentImageIndex = (currentImageIndex + 1) % images.length;
987
+ images[currentImageIndex].style.opacity = '1';
988
+ }, slideDuration * 1000);
989
+ }
990
+ isPlaying = !isPlaying;
991
  }
992
 
993
+ // Play button click handler
994
+ playBtn.addEventListener('click', togglePlayPause);
 
 
 
 
995
 
996
+ // Update progress bar
997
+ audioPlayer.addEventListener('timeupdate', () => {
998
+ const percent = (audioPlayer.currentTime / data.audioDuration) * 100;
999
+ progressBar.style.width = `${percent}%`;
1000
+
1001
+ // Update subtitle highlighting
1002
+ const currentTime = audioPlayer.currentTime;
1003
+ const subtitleWords = subtitleContainer.querySelectorAll('.subtitle-word');
1004
+
1005
+ subtitleWords.forEach(word => {
1006
+ const start = parseFloat(word.dataset.start);
1007
+ const end = parseFloat(word.dataset.end);
1008
+
1009
+ if (currentTime >= start && currentTime <= end) {
1010
+ word.classList.add('highlighted');
1011
+ word.style.backgroundColor = data.subtitleSettings.highlightColor;
1012
+ } else {
1013
+ word.classList.remove('highlighted');
1014
+ word.style.backgroundColor = 'transparent';
1015
+ }
1016
+ });
1017
+ });
1018
 
1019
+ // Reset when audio ends
1020
+ audioPlayer.addEventListener('ended', () => {
1021
+ isPlaying = false;
1022
+ clearInterval(slideInterval);
1023
+ playBtn.innerHTML = `<svg xmlns="http://www.w3.org/2000/svg" class="h-6 w-6" fill="none" viewBox="0 0 24 24" stroke="currentColor">
1024
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M14.752 11.168l-3.197-2.132A1 1 0 0010 9.87v4.263a1 1 0 001.555.832l3.197-2.132a1 1 0 000-1.664z" />
1025
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M21 12a9 9 0 11-18 0 9 9 0 0118 0z" />
1026
+ </svg>`;
1027
+
1028
+ // Reset images
1029
+ const images = imageSlideshow.querySelectorAll('img');
1030
+ images.forEach((img, i) => {
1031
+ img.style.opacity = i === 0 ? '1' : '0';
1032
+ });
1033
+ currentImageIndex = 0;
1034
+
1035
+ // Reset subtitles
1036
+ const subtitleWords = subtitleContainer.querySelectorAll('.subtitle-word');
1037
+ subtitleWords.forEach(word => {
1038
+ word.classList.remove('highlighted');
1039
+ word.style.backgroundColor = 'transparent';
1040
+ });
1041
+ });
1042
  }
1043
  </script>
1044
  </body>