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> | |
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; | |
} | |
</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"> | |
<div class="mb-4"> | |
<label for="niche" class="block text-sm font-medium mb-1">Niche/Topic</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</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> | |
</select> | |
</div> | |
<div class="grid grid-cols-1 md:grid-cols-3 gap-4 mb-6"> | |
<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"> | |
<option value="Claude-3.7-Sonnet">Claude-3.7-Sonnet</option> | |
<option value="GPT-4o">GPT-4o</option> | |
<option value="GPT-4o-mini">GPT-4o-mini</option> | |
<option value="Gemini-2.0-Flash">Gemini-2.0-Flash</option> | |
</select> | |
</div> | |
<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"> | |
<option value="FLUX-pro-1.1">FLUX-pro-1.1</option> | |
<option value="FLUX-schnell">FLUX-schnell</option> | |
<option value="Dall-E-3">Dall-E-3</option> | |
</select> | |
</div> | |
<div> | |
<label for="voice-generator" class="block text-sm font-medium mb-1">Voice Generator</label> | |
<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"> | |
<option value="ElevenLabs">ElevenLabs</option> | |
<option value="PlayAI-Dialog">PlayAI-Dialog</option> | |
</select> | |
</div> | |
</div> | |
<div> | |
<label for="voice-name" class="block text-sm font-medium mb-1">Voice Name (Optional)</label> | |
<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"> | |
</div> | |
<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"> | |
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%]"> | |
<video id="video-player" controls class="absolute top-0 left-0 w-full h-full rounded-lg"> | |
Your browser does not support the video tag. | |
</video> | |
</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> | |
</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'); | |
} | |
}); | |
// 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; | |
const textGenerator = document.getElementById('text-generator').value; | |
const imageGenerator = document.getElementById('image-generator').value; | |
const voiceGenerator = document.getElementById('voice-generator').value; | |
const voiceName = document.getElementById('voice-name').value.trim(); | |
// 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 { | |
// Updates for the loading message | |
updateProgress('Generating topic...'); | |
await new Promise(resolve => setTimeout(resolve, 1000)); | |
// Step 1: Generate topic | |
const topic = await generateTopic(niche, textGenerator); | |
// Step 2: Generate script | |
updateProgress('Creating script...'); | |
const script = await generateScript(topic, language, textGenerator); | |
// Step 3: Generate metadata | |
updateProgress('Creating title and description...'); | |
const metadata = await generateMetadata(topic, script, textGenerator); | |
// Step 4: Generate image prompts | |
updateProgress('Creating image prompts...'); | |
const imagePrompts = await generateImagePrompts(topic, script, textGenerator); | |
// Step 5: Generate images | |
updateProgress('Generating images...'); | |
const imageUrls = await generateImages(imagePrompts, imageGenerator); | |
// Step 6: Generate speech | |
updateProgress('Creating voiceover...'); | |
const audioUrl = await generateSpeech(script, language, voiceGenerator, voiceName); | |
// Step 7: Generate video | |
updateProgress('Creating final video...'); | |
const videoUrl = await generateVideo(imageUrls, audioUrl, script); | |
// Display results | |
displayResults(videoUrl, metadata.title, metadata.description); | |
} 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, textGeneratorModel) { | |
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.`; | |
// Use Poe API to send user message | |
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(`@${textGeneratorModel} ${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, textGeneratorModel) { | |
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(`@${textGeneratorModel} ${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, textGeneratorModel) { | |
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(`@${textGeneratorModel} ${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(`@${textGeneratorModel} ${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, textGeneratorModel) { | |
try { | |
const prompt = ` | |
Generate 5 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 search term 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(`@${textGeneratorModel} ${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, imageGeneratorModel) { | |
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(`@${imageGeneratorModel} ${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, voiceGeneratorModel, voiceName) { | |
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 (voiceName) { | |
prompt += ` --voice ${voiceName}`; | |
} | |
// Send request to generate speech | |
await window.Poe.sendUserMessage(`@${voiceGeneratorModel} ${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'); | |
} | |
return audioUrl; | |
} catch (error) { | |
console.error('Error generating speech:', error); | |
throw new Error('Failed to generate speech'); | |
} | |
} | |
// Function to generate video by combining images and audio | |
async function generateVideo(imageUrls, audioUrl, script) { | |
// Here we would normally combine everything into a video | |
// Since we can't actually do video processing in the browser easily, | |
// we'll simulate it with image slideshow and audio | |
try { | |
// Create a simulated video player that shows images as a slideshow with audio | |
// This is a simple mockup - in a real application, you would use a video processing service | |
// For this demo, we'll just return the audio URL and use the first image | |
// as a placeholder in the video player | |
// In a real implementation, this is where you would call an external video | |
// processing service or use a server-side component | |
// Simulate processing time | |
await new Promise(resolve => setTimeout(resolve, 3000)); | |
// Return a mock video URL (which is just the audio URL for this demo) | |
return { | |
audioUrl: audioUrl, | |
imageUrls: imageUrls | |
}; | |
} catch (error) { | |
console.error('Error generating video:', error); | |
throw new Error('Failed to generate video'); | |
} | |
} | |
// Function to display results | |
function displayResults(videoData, title, description) { | |
// 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 = title; | |
document.getElementById('video-description').textContent = description; | |
// Set up video player with images slideshow and audio | |
const videoPlayer = document.getElementById('video-player'); | |
// Create a simple slideshow with the first image and audio | |
if (videoData.imageUrls && videoData.imageUrls.length > 0) { | |
// Set poster to first image | |
videoPlayer.setAttribute('poster', videoData.imageUrls[0]); | |
} | |
// Set audio source | |
videoPlayer.innerHTML = ''; | |
const audioSource = document.createElement('source'); | |
audioSource.src = videoData.audioUrl; | |
audioSource.type = 'audio/mpeg'; | |
videoPlayer.appendChild(audioSource); | |
// Add text explaining this is a simulation | |
const textOverlay = document.createElement('div'); | |
textOverlay.innerHTML = ` | |
<div class="absolute inset-0 flex items-center justify-center bg-black bg-opacity-50 text-white p-4 text-center"> | |
<div> | |
<p class="font-bold mb-2">Audio Preview</p> | |
<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> | |
</div> | |
</div> | |
`; | |
videoPlayer.parentNode.appendChild(textOverlay); | |
// Load and play | |
videoPlayer.load(); | |
} | |
</script> | |
</body> | |
</html> |