Spaces:
Build error
Build error
<!DOCTYPE html> | |
<html lang="en"> | |
<head> | |
<meta charset="UTF-8"> | |
<meta name="viewport" content="width=device-width, initial-scale=1.0"> | |
<title>YouTube Shorts Generator</title> | |
<script src="https://cdn.tailwindcss.com"></script> | |
<script src="https://cdn.jsdelivr.net/npm/marked/marked.min.js"></script> | |
<script> | |
tailwind.config = { | |
darkMode: 'class', | |
theme: { | |
extend: { | |
colors: { | |
primary: '#5D5CDE', | |
} | |
} | |
} | |
}; | |
</script> | |
<style> | |
.loading-spinner { | |
border: 4px solid rgba(0, 0, 0, 0.1); | |
border-left-color: #5D5CDE; | |
border-radius: 50%; | |
width: 50px; | |
height: 50px; | |
animation: spin 1s linear infinite; | |
} | |
to { transform: rotate(360deg); } | |
} | |
.dark .loading-spinner { | |
border-color: rgba(255, 255, 255, 0.1); | |
border-left-color: #5D5CDE; | |
} | |
.subtitle-word { | |
display: inline-block; | |
margin-right: 5px; | |
padding: 2px 5px; | |
border-radius: 4px; | |
font-family: 'Helvetica', sans-serif; | |
font-weight: bold; | |
color: white; | |
} | |
.subtitle-word.highlighted { | |
background-color: blue; | |
transition: background-color 0.1s ease; | |
} | |
.subtitle-container { | |
position: absolute; | |
bottom: 15%; | |
left: 10%; | |
width: 80%; | |
text-align: center; | |
z-index: 10; | |
font-size: 24px; | |
line-height: 1.5; | |
} | |
.accordion-content { | |
max-height: 0; | |
overflow: hidden; | |
transition: max-height 0.3s ease; | |
} | |
.accordion-content.open { | |
max-height: 1000px; | |
} | |
</style> | |
</head> | |
<body class="bg-white dark:bg-gray-900 text-gray-800 dark:text-gray-200 min-h-screen"> | |
<div class="container mx-auto px-4 py-8 max-w-4xl"> | |
<h1 class="text-3xl font-bold mb-4 text-center text-primary">YouTube Shorts Generator</h1> | |
<div class="mb-8 bg-gray-100 dark:bg-gray-800 p-6 rounded-lg shadow-md"> | |
<!-- Main Required Inputs --> | |
<div class="mb-6"> | |
<div class="mb-4"> | |
<label for="niche" class="block text-sm font-medium mb-1">Niche/Topic <span class="text-red-500">*</span></label> | |
<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"> | |
</div> | |
<div class="mb-4"> | |
<label for="language" class="block text-sm font-medium mb-1">Language <span class="text-red-500">*</span></label> | |
<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"> | |
<option value="English">English</option> | |
<option value="Spanish">Spanish</option> | |
<option value="French">French</option> | |
<option value="German">German</option> | |
<option value="Italian">Italian</option> | |
<option value="Portuguese">Portuguese</option> | |
<option value="Russian">Russian</option> | |
<option value="Japanese">Japanese</option> | |
<option value="Chinese">Chinese</option> | |
<option value="Hindi">Hindi</option> | |
<option value="Arabic">Arabic</option> | |
<option value="Korean">Korean</option> | |
<option value="Dutch">Dutch</option> | |
<option value="Swedish">Swedish</option> | |
<option value="Turkish">Turkish</option> | |
</select> | |
</div> | |
</div> | |
<!-- Advanced Options Accordion --> | |
<div class="mb-6"> | |
<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"> | |
<span class="font-medium">Advanced Options</span> | |
<span class="transition-transform duration-300 transform">▼</span> | |
</button> | |
<div id="advanced-options" class="accordion-content bg-gray-50 dark:bg-gray-800 p-4 rounded-md"> | |
<div class="grid grid-cols-1 md:grid-cols-2 gap-4 mb-6"> | |
<!-- Text Generator Options --> | |
<div> | |
<label for="text-generator" class="block text-sm font-medium mb-1">Text Generator</label> | |
<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"> | |
<option value="gemini">Google Gemini</option> | |
<option value="gpt3">GPT-3.5-Turbo</option> | |
<option value="gpt4">GPT-4</option> | |
<option value="claude">Claude</option> | |
<option value="llama">Llama</option> | |
<option value="mistral">Mistral</option> | |
<option value="command">Cohere Command</option> | |
</select> | |
<label for="text-model" class="block text-sm font-medium mb-1">Text Model</label> | |
<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"> | |
<option value="gemini-2.0-flash">gemini-2.0-flash</option> | |
<option value="gemini-2.0-flash-lite">gemini-2.0-flash-lite</option> | |
<option value="gemini-1.5-flash">gemini-1.5-flash</option> | |
<option value="gemini-1.5-flash-8b">gemini-1.5-flash-8b</option> | |
<option value="gemini-1.5-pro">gemini-1.5-pro</option> | |
<!-- Other models will be populated based on text generator selection --> | |
</select> | |
</div> | |
<!-- Image Generator Options --> | |
<div> | |
<label for="image-generator" class="block text-sm font-medium mb-1">Image Generator</label> | |
<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"> | |
<option value="prodia">Prodia</option> | |
<option value="hercai">Hercai</option> | |
<option value="g4f">G4F</option> | |
<option value="segmind">Segmind</option> | |
<option value="pollinations">Pollinations</option> | |
</select> | |
<label for="image-model" class="block text-sm font-medium mb-1">Image Model</label> | |
<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"> | |
<option value="sdxl">SDXL</option> | |
<option value="realvisxl">RealVisXL</option> | |
<option value="juggernaut">Juggernaut</option> | |
<option value="dalle">DALL-E</option> | |
<option value="midjourney">Midjourney</option> | |
<!-- Other models will be populated based on image generator selection --> | |
</select> | |
</div> | |
</div> | |
<div class="grid grid-cols-1 md:grid-cols-2 gap-4 mb-6"> | |
<!-- Speech Generator Options --> | |
<div> | |
<label for="tts-engine" class="block text-sm font-medium mb-1">Speech Generator</label> | |
<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"> | |
<option value="elevenlabs">ElevenLabs</option> | |
<option value="bark">Bark</option> | |
<option value="gtts">Google TTS</option> | |
<option value="openai">OpenAI TTS</option> | |
<option value="edge">Edge TTS</option> | |
<option value="local_tts">Local TTS</option> | |
<option value="xtts">XTTS</option> | |
<option value="rvc">RVC</option> | |
</select> | |
<label for="tts-voice" class="block text-sm font-medium mb-1">Voice</label> | |
<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"> | |
</div> | |
<!-- Subtitle Options --> | |
<div> | |
<label for="subtitle-font" class="block text-sm font-medium mb-1">Subtitle Font</label> | |
<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"> | |
<option value="Helvetica-Bold">Helvetica Bold</option> | |
<option value="Arial-Bold">Arial Bold</option> | |
<option value="Roboto-Bold">Roboto Bold</option> | |
<option value="Verdana-Bold">Verdana Bold</option> | |
</select> | |
<div class="grid grid-cols-2 gap-2"> | |
<div> | |
<label for="subtitle-color" class="block text-sm font-medium mb-1">Text Color</label> | |
<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"> | |
</div> | |
<div> | |
<label for="highlight-color" class="block text-sm font-medium mb-1">Highlight Color</label> | |
<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"> | |
</div> | |
</div> | |
</div> | |
</div> | |
<!-- Image Prompt Options --> | |
<div class="mb-4"> | |
<label for="prompt-count" class="block text-sm font-medium mb-1">Number of Image Prompts</label> | |
<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"> | |
</div> | |
</div> | |
</div> | |
<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"> | |
Generate Video | |
</button> | |
</div> | |
<div id="loading-container" class="hidden flex-col items-center justify-center py-8"> | |
<div class="loading-spinner mb-4"></div> | |
<div id="status-message" class="text-lg font-medium">Generating content...</div> | |
<div id="progress-detail" class="text-sm text-gray-500 dark:text-gray-400 mt-2"></div> | |
</div> | |
<div id="results-container" class="hidden bg-gray-100 dark:bg-gray-800 p-6 rounded-lg shadow-md"> | |
<h2 class="text-xl font-bold mb-3">Generated Video</h2> | |
<div id="video-player-container" class="mb-6 relative pt-[56.25%] bg-black rounded-lg"> | |
<!-- Video Player --> | |
<div id="video-container" class="absolute top-0 left-0 w-full h-full rounded-lg overflow-hidden"> | |
<!-- Video or Images will be displayed here --> | |
<div id="image-slideshow" class="w-full h-full"></div> | |
<!-- Subtitles will be displayed here --> | |
<div id="subtitle-container" class="subtitle-container"></div> | |
<!-- Audio Player --> | |
<audio id="audio-player" class="hidden"></audio> | |
</div> | |
<!-- Video Controls --> | |
<div id="video-controls" class="absolute bottom-4 left-0 right-0 mx-auto flex justify-center items-center space-x-4"> | |
<button id="play-btn" class="bg-white bg-opacity-20 hover:bg-opacity-30 text-white p-2 rounded-full"> | |
<svg xmlns="http://www.w3.org/2000/svg" class="h-6 w-6" fill="none" viewBox="0 0 24 24" stroke="currentColor"> | |
<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" /> | |
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M21 12a9 9 0 11-18 0 9 9 0 0118 0z" /> | |
</svg> | |
</button> | |
<div class="w-48 h-1 bg-white bg-opacity-20 rounded-full"> | |
<div id="progress-bar" class="h-full bg-white rounded-full" style="width: 0%"></div> | |
</div> | |
</div> | |
</div> | |
<div class="grid grid-cols-1 md:grid-cols-2 gap-4"> | |
<div> | |
<h3 class="font-medium mb-2">Title</h3> | |
<p id="video-title" class="bg-white dark:bg-gray-700 p-3 rounded-md"></p> | |
</div> | |
<div> | |
<h3 class="font-medium mb-2">Description</h3> | |
<p id="video-description" class="bg-white dark:bg-gray-700 p-3 rounded-md h-24 overflow-y-auto"></p> | |
</div> | |
</div> | |
<div class="mt-6"> | |
<h3 class="font-medium mb-2">Script</h3> | |
<div id="video-script" class="bg-white dark:bg-gray-700 p-3 rounded-md"></div> | |
</div> | |
<div class="mt-6"> | |
<h3 class="font-medium mb-2">Image Prompts</h3> | |
<div id="image-prompts" class="bg-white dark:bg-gray-700 p-3 rounded-md"></div> | |
</div> | |
</div> | |
</div> | |
<script> | |
// Initialize dark mode based on user preference | |
if (window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches) { | |
document.documentElement.classList.add('dark'); | |
} | |
window.matchMedia('(prefers-color-scheme: dark)').addEventListener('change', event => { | |
if (event.matches) { | |
document.documentElement.classList.add('dark'); | |
} else { | |
document.documentElement.classList.remove('dark'); | |
} | |
}); | |
// Advanced options accordion | |
document.getElementById('advanced-options-toggle').addEventListener('click', function() { | |
const content = document.getElementById('advanced-options'); | |
content.classList.toggle('open'); | |
this.querySelector('span:last-child').classList.toggle('rotate-180'); | |
}); | |
// Populate models based on generator selection | |
document.getElementById('text-generator').addEventListener('change', function() { | |
const modelSelect = document.getElementById('text-model'); | |
modelSelect.innerHTML = ''; | |
switch(this.value) { | |
case 'gemini': | |
addOptions(modelSelect, [ | |
{value: 'gemini-2.0-flash', text: 'gemini-2.0-flash'}, | |
{value: 'gemini-2.0-flash-lite', text: 'gemini-2.0-flash-lite'}, | |
{value: 'gemini-1.5-flash', text: 'gemini-1.5-flash'}, | |
{value: 'gemini-1.5-flash-8b', text: 'gemini-1.5-flash-8b'}, | |
{value: 'gemini-1.5-pro', text: 'gemini-1.5-pro'} | |
]); | |
break; | |
case 'gpt3': | |
addOptions(modelSelect, [ | |
{value: 'gpt-3.5-turbo', text: 'gpt-3.5-turbo'}, | |
{value: 'gpt-3.5-turbo-16k', text: 'gpt-3.5-turbo-16k'} | |
]); | |
break; | |
case 'gpt4': | |
addOptions(modelSelect, [ | |
{value: 'gpt-4', text: 'gpt-4'}, | |
{value: 'gpt-4o', text: 'gpt-4o'}, | |
{value: 'gpt-4-turbo', text: 'gpt-4-turbo'} | |
]); | |
break; | |
case 'claude': | |
addOptions(modelSelect, [ | |
{value: 'claude-3-opus-20240229', text: 'Claude 3 Opus'}, | |
{value: 'claude-3-sonnet-20240229', text: 'Claude 3 Sonnet'}, | |
{value: 'claude-3-haiku-20240307', text: 'Claude 3 Haiku'} | |
]); | |
break; | |
case 'llama': | |
addOptions(modelSelect, [ | |
{value: 'llama-3-70b-chat', text: 'Llama 3 70B'}, | |
{value: 'llama-3-8b-chat', text: 'Llama 3 8B'}, | |
{value: 'llama-2-70b-chat', text: 'Llama 2 70B'} | |
]); | |
break; | |
case 'mistral': | |
addOptions(modelSelect, [ | |
{value: 'mistral-large-latest', text: 'Mistral Large'}, | |
{value: 'mistral-medium-latest', text: 'Mistral Medium'}, | |
{value: 'mistral-small-latest', text: 'Mistral Small'} | |
]); | |
break; | |
case 'command': | |
addOptions(modelSelect, [ | |
{value: 'command-r', text: 'Command R'}, | |
{value: 'command-r-plus', text: 'Command R+'}, | |
{value: 'command-light', text: 'Command Light'} | |
]); | |
break; | |
} | |
}); | |
document.getElementById('image-generator').addEventListener('change', function() { | |
const modelSelect = document.getElementById('image-model'); | |
modelSelect.innerHTML = ''; | |
switch(this.value) { | |
case 'prodia': | |
addOptions(modelSelect, [ | |
{value: 'sdxl', text: 'SDXL'}, | |
{value: 'realvisxl', text: 'RealVisXL V4.0'}, | |
{value: 'juggernaut', text: 'Juggernaut XL'}, | |
{value: 'dreamshaper', text: 'DreamShaper 8'}, | |
{value: 'portraitplus', text: 'Portrait+ V1'} | |
]); | |
break; | |
case 'hercai': | |
addOptions(modelSelect, [ | |
{value: 'v1', text: 'Stable Diffusion v1'}, | |
{value: 'v2', text: 'Stable Diffusion v2'}, | |
{value: 'v3', text: 'Stable Diffusion v3'}, | |
{value: 'lexica', text: 'Lexica Diffusion'} | |
]); | |
break; | |
case 'g4f': | |
addOptions(modelSelect, [ | |
{value: 'dall-e-3', text: 'DALL-E 3'}, | |
{value: 'dall-e-2', text: 'DALL-E 2'}, | |
{value: 'imageapi', text: 'ImageAPI v1'} | |
]); | |
break; | |
case 'segmind': | |
addOptions(modelSelect, [ | |
{value: 'sdxl-turbo', text: 'SDXL Turbo'}, | |
{value: 'realistic-vision', text: 'Realistic Vision'}, | |
{value: 'sd3', text: 'Stable Diffusion 3'} | |
]); | |
break; | |
case 'pollinations': | |
addOptions(modelSelect, [ | |
{value: 'default', text: 'Default Model'} | |
]); | |
break; | |
} | |
}); | |
document.getElementById('tts-engine').addEventListener('change', function() { | |
const voiceInput = document.getElementById('tts-voice'); | |
switch(this.value) { | |
case 'elevenlabs': | |
voiceInput.placeholder = 'E.g., Sarah, Brian, Lily, Monika Sogam'; | |
break; | |
case 'openai': | |
voiceInput.placeholder = 'E.g., alloy, echo, fable, onyx, nova, shimmer'; | |
break; | |
case 'edge': | |
voiceInput.placeholder = 'E.g., en-US-JennyNeural, en-US-GuyNeural'; | |
break; | |
case 'gtts': | |
voiceInput.placeholder = 'Language code (en, es, fr, etc.)'; | |
break; | |
case 'xtts': | |
voiceInput.placeholder = 'Speaker name or reference audio path'; | |
break; | |
default: | |
voiceInput.placeholder = 'Voice name or identifier'; | |
} | |
}); | |
function addOptions(selectElement, options) { | |
options.forEach(option => { | |
const optElement = document.createElement('option'); | |
optElement.value = option.value; | |
optElement.textContent = option.text; | |
selectElement.appendChild(optElement); | |
}); | |
} | |
// Initialize model selects | |
document.getElementById('text-generator').dispatchEvent(new Event('change')); | |
document.getElementById('image-generator').dispatchEvent(new Event('change')); | |
document.getElementById('tts-engine').dispatchEvent(new Event('change')); | |
// Handler for generating videos | |
document.getElementById('generate-btn').addEventListener('click', async function() { | |
// Get input values | |
const niche = document.getElementById('niche').value.trim(); | |
const language = document.getElementById('language').value; | |
// Get advanced options | |
const textGenerator = document.getElementById('text-generator').value; | |
const textModel = document.getElementById('text-model').value; | |
const imageGenerator = document.getElementById('image-generator').value; | |
const imageModel = document.getElementById('image-model').value; | |
const ttsEngine = document.getElementById('tts-engine').value; | |
const ttsVoice = document.getElementById('tts-voice').value.trim(); | |
const subtitleFont = document.getElementById('subtitle-font').value; | |
const subtitleColor = document.getElementById('subtitle-color').value; | |
const highlightColor = document.getElementById('highlight-color').value; | |
const promptCount = document.getElementById('prompt-count').value; | |
// Validation | |
if (!niche) { | |
alert('Please enter a niche/topic'); | |
return; | |
} | |
// Show loading state | |
document.getElementById('loading-container').classList.remove('hidden'); | |
document.getElementById('loading-container').classList.add('flex'); | |
document.getElementById('results-container').classList.add('hidden'); | |
try { | |
// For the Poe environment, we'll create a simulated process that generates a video | |
// using text generation, image generation, and speech generation | |
// Step 1: Generate topic | |
updateProgress('Generating topic...'); | |
const topic = await generateTopic(niche, language); | |
// Step 2: Generate script | |
updateProgress('Creating script...'); | |
const script = await generateScript(topic, language); | |
// Step 3: Generate metadata | |
updateProgress('Creating title and description...'); | |
const metadata = await generateMetadata(topic, script); | |
// Step 4: Generate image prompts | |
updateProgress('Creating image prompts...'); | |
const imagePrompts = await generateImagePrompts(topic, script, promptCount); | |
// Step 5: Generate images | |
updateProgress('Generating images...'); | |
const imageUrls = await generateImages(imagePrompts); | |
// Step 6: Generate speech | |
updateProgress('Creating voiceover...'); | |
const audioData = await generateSpeech(script, language, ttsEngine, ttsVoice); | |
// Step 7: Generate subtitles (simulated) | |
updateProgress('Creating subtitles...'); | |
const subtitles = generateSimulatedSubtitles(script); | |
// Step 8: Display the results | |
updateProgress('Finalizing video...'); | |
displayResults({ | |
topic: topic, | |
script: script, | |
metadata: metadata, | |
imagePrompts: imagePrompts, | |
imageUrls: imageUrls, | |
audioUrl: audioData.url, | |
audioDuration: audioData.duration, | |
subtitles: subtitles, | |
subtitleSettings: { | |
font: subtitleFont, | |
color: subtitleColor, | |
highlightColor: highlightColor | |
} | |
}); | |
} catch (error) { | |
console.error('Error:', error); | |
document.getElementById('status-message').textContent = 'Error generating video'; | |
document.getElementById('progress-detail').textContent = error.message || 'An unexpected error occurred'; | |
} | |
}); | |
function updateProgress(message) { | |
document.getElementById('progress-detail').textContent = message; | |
} | |
// Function to generate topic based on niche | |
async function generateTopic(niche, language) { | |
try { | |
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.`; | |
// For this demonstration, we'll use Claude | |
const handlerId = 'topic-generation-handler'; | |
let topicResult = ''; | |
// Register handler for response | |
window.Poe.registerHandler(handlerId, (result) => { | |
if (result.responses.length > 0) { | |
const response = result.responses[0]; | |
if (response.status === 'complete') { | |
topicResult = response.content.trim(); | |
} | |
} | |
}); | |
// Send request to generate topic | |
await window.Poe.sendUserMessage(`@Claude-3.7-Sonnet ${prompt}`, { | |
handler: handlerId, | |
stream: false, | |
openChat: false | |
}); | |
// Wait for response to be complete | |
while (!topicResult) { | |
await new Promise(resolve => setTimeout(resolve, 100)); | |
} | |
return topicResult; | |
} catch (error) { | |
console.error('Error generating topic:', error); | |
throw new Error('Failed to generate topic'); | |
} | |
} | |
// Function to generate script based on topic | |
async function generateScript(topic, language) { | |
try { | |
const prompt = ` | |
Generate a script for youtube shorts video, depending on the subject of the video. | |
The script is to be returned as a string with several paragraphs. | |
Get straight to the point, don't start with unnecessary things like, "welcome to this video". | |
Obviously, the script should be related to the subject of the video. | |
YOU MUST NOT INCLUDE ANY TYPE OF MARKDOWN OR FORMATTING IN THE SCRIPT, NEVER USE A TITLE. | |
YOU MUST WRITE THE SCRIPT IN THE LANGUAGE SPECIFIED IN [LANGUAGE]. | |
ONLY RETURN THE RAW CONTENT OF THE SCRIPT. DO NOT INCLUDE "VOICEOVER", "NARRATOR" OR SIMILAR INDICATORS. | |
Subject: ${topic} | |
Language: ${language} | |
`; | |
// Use Poe API to send user message | |
const handlerId = 'script-generation-handler'; | |
let scriptResult = ''; | |
// Register handler for response | |
window.Poe.registerHandler(handlerId, (result) => { | |
if (result.responses.length > 0) { | |
const response = result.responses[0]; | |
if (response.status === 'complete') { | |
scriptResult = response.content.trim(); | |
} | |
} | |
}); | |
// Send request to generate script | |
await window.Poe.sendUserMessage(`@Claude-3.7-Sonnet ${prompt}`, { | |
handler: handlerId, | |
stream: false, | |
openChat: false | |
}); | |
// Wait for response to be complete | |
while (!scriptResult) { | |
await new Promise(resolve => setTimeout(resolve, 100)); | |
} | |
return scriptResult; | |
} catch (error) { | |
console.error('Error generating script:', error); | |
throw new Error('Failed to generate script'); | |
} | |
} | |
// Function to generate metadata (title and description) | |
async function generateMetadata(topic, script) { | |
try { | |
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.`; | |
// Use Poe API to send user message for title | |
const titleHandlerId = 'title-generation-handler'; | |
let titleResult = ''; | |
// Register handler for title response | |
window.Poe.registerHandler(titleHandlerId, (result) => { | |
if (result.responses.length > 0) { | |
const response = result.responses[0]; | |
if (response.status === 'complete') { | |
titleResult = response.content.trim(); | |
} | |
} | |
}); | |
// Send request to generate title | |
await window.Poe.sendUserMessage(`@Claude-3.7-Sonnet ${titlePrompt}`, { | |
handler: titleHandlerId, | |
stream: false, | |
openChat: false | |
}); | |
// Wait for title response to be complete | |
while (!titleResult) { | |
await new Promise(resolve => setTimeout(resolve, 100)); | |
} | |
// Now generate description | |
const descPrompt = `Please generate a YouTube Video Description for the following script: ${script}. Only return the description, nothing else.`; | |
// Use Poe API to send user message for description | |
const descHandlerId = 'desc-generation-handler'; | |
let descResult = ''; | |
// Register handler for description response | |
window.Poe.registerHandler(descHandlerId, (result) => { | |
if (result.responses.length > 0) { | |
const response = result.responses[0]; | |
if (response.status === 'complete') { | |
descResult = response.content.trim(); | |
} | |
} | |
}); | |
// Send request to generate description | |
await window.Poe.sendUserMessage(`@Claude-3.7-Sonnet ${descPrompt}`, { | |
handler: descHandlerId, | |
stream: false, | |
openChat: false | |
}); | |
// Wait for description response to be complete | |
while (!descResult) { | |
await new Promise(resolve => setTimeout(resolve, 100)); | |
} | |
return { | |
title: titleResult, | |
description: descResult | |
}; | |
} catch (error) { | |
console.error('Error generating metadata:', error); | |
throw new Error('Failed to generate title and description'); | |
} | |
} | |
// Function to generate image prompts | |
async function generateImagePrompts(topic, script, count = 5) { | |
try { | |
const prompt = ` | |
Generate ${count} Image Prompts for AI Image Generation, | |
depending on the subject of a video. | |
Subject: ${topic} | |
The image prompts are to be returned as | |
a JSON-Array of strings. | |
Each prompt should consist of a full sentence, | |
always add the main subject of the video. | |
Be emotional and use interesting adjectives to make the | |
Image Prompt as detailed as possible. | |
YOU MUST ONLY RETURN THE JSON-ARRAY OF STRINGS. | |
YOU MUST NOT RETURN ANYTHING ELSE. | |
For context, here is the full text: | |
${script} | |
`; | |
// Use Poe API to send user message | |
const handlerId = 'image-prompts-handler'; | |
let promptsResult = ''; | |
// Register handler for response | |
window.Poe.registerHandler(handlerId, (result) => { | |
if (result.responses.length > 0) { | |
const response = result.responses[0]; | |
if (response.status === 'complete') { | |
promptsResult = response.content.trim(); | |
} | |
} | |
}); | |
// Send request to generate image prompts | |
await window.Poe.sendUserMessage(`@Claude-3.7-Sonnet ${prompt}`, { | |
handler: handlerId, | |
stream: false, | |
openChat: false | |
}); | |
// Wait for response to be complete | |
while (!promptsResult) { | |
await new Promise(resolve => setTimeout(resolve, 100)); | |
} | |
// Clean and parse the JSON response | |
const cleanedResponse = promptsResult | |
.replace(/```json/g, '') | |
.replace(/```/g, '') | |
.trim(); | |
try { | |
return JSON.parse(cleanedResponse); | |
} catch (parseError) { | |
// If parsing fails, try to extract the array from the text | |
const arrayMatch = cleanedResponse.match(/\[.*\]/s); | |
if (arrayMatch) { | |
return JSON.parse(arrayMatch[0]); | |
} | |
throw new Error('Failed to parse image prompts'); | |
} | |
} catch (error) { | |
console.error('Error generating image prompts:', error); | |
throw new Error('Failed to generate image prompts'); | |
} | |
} | |
// Function to generate images based on prompts | |
async function generateImages(imagePrompts) { | |
try { | |
const imageUrls = []; | |
for (let i = 0; i < imagePrompts.length; i++) { | |
updateProgress(`Generating image ${i+1}/${imagePrompts.length}...`); | |
// Use Poe API to send user message | |
const handlerId = `image-generation-handler-${i}`; | |
// Register handler for response | |
window.Poe.registerHandler(handlerId, (result) => { | |
if (result.responses.length > 0) { | |
const response = result.responses[0]; | |
if (response.status === 'complete' && response.attachments && response.attachments.length > 0) { | |
imageUrls.push(response.attachments[0].url); | |
} | |
} | |
}); | |
// Send request to generate image | |
await window.Poe.sendUserMessage(`@FLUX-pro-1.1 ${imagePrompts[i]}`, { | |
handler: handlerId, | |
stream: false, | |
openChat: false | |
}); | |
// Wait for a short time to ensure the handler has time to receive the response | |
await new Promise(resolve => setTimeout(resolve, 3000)); | |
} | |
// Ensure we have at least one image | |
if (imageUrls.length === 0) { | |
throw new Error('Failed to generate any images'); | |
} | |
return imageUrls; | |
} catch (error) { | |
console.error('Error generating images:', error); | |
throw new Error('Failed to generate images'); | |
} | |
} | |
// Function to generate speech from script | |
async function generateSpeech(script, language, ttsEngine = 'elevenlabs', ttsVoice = '') { | |
try { | |
// Use Poe API to send user message | |
const handlerId = 'speech-generation-handler'; | |
let audioUrl = null; | |
// Register handler for response | |
window.Poe.registerHandler(handlerId, (result) => { | |
if (result.responses.length > 0) { | |
const response = result.responses[0]; | |
if (response.status === 'complete' && response.attachments && response.attachments.length > 0) { | |
audioUrl = response.attachments[0].url; | |
} | |
} | |
}); | |
// Prepare the prompt | |
let prompt = script; | |
if (ttsVoice) { | |
prompt += ` --voice ${ttsVoice}`; | |
} | |
// Send request to generate speech | |
await window.Poe.sendUserMessage(`@ElevenLabs ${prompt}`, { | |
handler: handlerId, | |
stream: false, | |
openChat: false | |
}); | |
// Wait for audio URL to be available | |
let attempts = 0; | |
while (!audioUrl && attempts < 30) { | |
await new Promise(resolve => setTimeout(resolve, 1000)); | |
attempts++; | |
} | |
if (!audioUrl) { | |
throw new Error('Failed to generate speech audio'); | |
} | |
// Create an audio element to get the duration | |
const audio = new Audio(); | |
audio.src = audioUrl; | |
// Wait for the audio to load to get its duration | |
const audioDuration = await new Promise((resolve) => { | |
audio.addEventListener('loadedmetadata', () => { | |
resolve(audio.duration); | |
}); | |
// Fallback in case loadedmetadata doesn't fire | |
setTimeout(() => resolve(60), 5000); // Default to 60 seconds | |
}); | |
return { | |
url: audioUrl, | |
duration: audioDuration | |
}; | |
} catch (error) { | |
console.error('Error generating speech:', error); | |
throw new Error('Failed to generate speech'); | |
} | |
} | |
// Function to generate simulated subtitles (word-level timing) | |
function generateSimulatedSubtitles(script) { | |
// Split script into words | |
const words = script.split(/\s+/); | |
const subtitles = []; | |
// Simulate timing for each word (we'd normally get this from AssemblyAI) | |
let currentTime = 0; | |
for (let i = 0; i < words.length; i++) { | |
const word = words[i]; | |
// Simulate word duration based on length | |
const duration = 0.2 + (word.length * 0.05); | |
subtitles.push({ | |
word: word, | |
start: currentTime, | |
end: currentTime + duration | |
}); | |
currentTime += duration; | |
} | |
return subtitles; | |
} | |
// Function to display results | |
function displayResults(data) { | |
// Hide loading container | |
document.getElementById('loading-container').classList.add('hidden'); | |
document.getElementById('loading-container').classList.remove('flex'); | |
// Show results container | |
document.getElementById('results-container').classList.remove('hidden'); | |
// Set title and description | |
document.getElementById('video-title').textContent = data.metadata.title; | |
document.getElementById('video-description').textContent = data.metadata.description; | |
// Set script | |
document.getElementById('video-script').textContent = data.script; | |
// Set image prompts | |
const imagePromptsElement = document.getElementById('image-prompts'); | |
imagePromptsElement.innerHTML = ''; | |
data.imagePrompts.forEach((prompt, index) => { | |
const promptEl = document.createElement('div'); | |
promptEl.className = 'mb-2'; | |
promptEl.textContent = `${index + 1}. ${prompt}`; | |
imagePromptsElement.appendChild(promptEl); | |
}); | |
// Set up image slideshow | |
const imageSlideshow = document.getElementById('image-slideshow'); | |
imageSlideshow.innerHTML = ''; | |
data.imageUrls.forEach((url, index) => { | |
const img = document.createElement('img'); | |
img.src = url; | |
img.className = 'absolute top-0 left-0 w-full h-full object-cover transition-opacity duration-1000'; | |
img.style.opacity = index === 0 ? '1' : '0'; | |
img.dataset.index = index; | |
imageSlideshow.appendChild(img); | |
}); | |
// Set up audio player | |
const audioPlayer = document.getElementById('audio-player'); | |
audioPlayer.src = data.audioUrl; | |
audioPlayer.preload = 'auto'; | |
// Set up subtitle container | |
const subtitleContainer = document.getElementById('subtitle-container'); | |
subtitleContainer.innerHTML = ''; | |
// Create elements for each word | |
data.subtitles.forEach(subtitle => { | |
const wordEl = document.createElement('span'); | |
wordEl.className = 'subtitle-word'; | |
wordEl.textContent = subtitle.word; | |
wordEl.dataset.start = subtitle.start; | |
wordEl.dataset.end = subtitle.end; | |
wordEl.style.color = data.subtitleSettings.color; | |
subtitleContainer.appendChild(wordEl); | |
}); | |
// Set up video player controls | |
const playBtn = document.getElementById('play-btn'); | |
const progressBar = document.getElementById('progress-bar'); | |
let isPlaying = false; | |
let currentImageIndex = 0; | |
let slideInterval; | |
// Function to handle play/pause | |
function togglePlayPause() { | |
if (isPlaying) { | |
audioPlayer.pause(); | |
clearInterval(slideInterval); | |
playBtn.innerHTML = `<svg xmlns="http://www.w3.org/2000/svg" class="h-6 w-6" fill="none" viewBox="0 0 24 24" stroke="currentColor"> | |
<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" /> | |
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M21 12a9 9 0 11-18 0 9 9 0 0118 0z" /> | |
</svg>`; | |
} else { | |
audioPlayer.play(); | |
playBtn.innerHTML = `<svg xmlns="http://www.w3.org/2000/svg" class="h-6 w-6" fill="none" viewBox="0 0 24 24" stroke="currentColor"> | |
<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" /> | |
</svg>`; | |
// Set up image slideshow interval | |
const slideDuration = data.audioDuration / data.imageUrls.length; | |
slideInterval = setInterval(() => { | |
const images = imageSlideshow.querySelectorAll('img'); | |
images[currentImageIndex].style.opacity = '0'; | |
currentImageIndex = (currentImageIndex + 1) % images.length; | |
images[currentImageIndex].style.opacity = '1'; | |
}, slideDuration * 1000); | |
} | |
isPlaying = !isPlaying; | |
} | |
// Play button click handler | |
playBtn.addEventListener('click', togglePlayPause); | |
// Update progress bar | |
audioPlayer.addEventListener('timeupdate', () => { | |
const percent = (audioPlayer.currentTime / data.audioDuration) * 100; | |
progressBar.style.width = `${percent}%`; | |
// Update subtitle highlighting | |
const currentTime = audioPlayer.currentTime; | |
const subtitleWords = subtitleContainer.querySelectorAll('.subtitle-word'); | |
subtitleWords.forEach(word => { | |
const start = parseFloat(word.dataset.start); | |
const end = parseFloat(word.dataset.end); | |
if (currentTime >= start && currentTime <= end) { | |
word.classList.add('highlighted'); | |
word.style.backgroundColor = data.subtitleSettings.highlightColor; | |
} else { | |
word.classList.remove('highlighted'); | |
word.style.backgroundColor = 'transparent'; | |
} | |
}); | |
}); | |
// Reset when audio ends | |
audioPlayer.addEventListener('ended', () => { | |
isPlaying = false; | |
clearInterval(slideInterval); | |
playBtn.innerHTML = `<svg xmlns="http://www.w3.org/2000/svg" class="h-6 w-6" fill="none" viewBox="0 0 24 24" stroke="currentColor"> | |
<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" /> | |
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M21 12a9 9 0 11-18 0 9 9 0 0118 0z" /> | |
</svg>`; | |
// Reset images | |
const images = imageSlideshow.querySelectorAll('img'); | |
images.forEach((img, i) => { | |
img.style.opacity = i === 0 ? '1' : '0'; | |
}); | |
currentImageIndex = 0; | |
// Reset subtitles | |
const subtitleWords = subtitleContainer.querySelectorAll('.subtitle-word'); | |
subtitleWords.forEach(word => { | |
word.classList.remove('highlighted'); | |
word.style.backgroundColor = 'transparent'; | |
}); | |
}); | |
} | |
</script> | |
</body> | |
</html> |