New tools added to ToBeMigrated/ directory: ai_marketing_tools/: - ai_backlinker: AI-powered backlink generation - ai_google_ads_generator: Google Ads generation with templates ai_writers/: - ai_blog_faqs_writer: FAQ generation for blogs - ai_copywriter: Multiple copywriter frameworks (AIDA, PAS, 4C, 4R, etc.) - ai_finance_report_generator: Financial report generation - ai_story_illustrator: Story illustration - ai_story_video_generator: Story video generation - ai_story_writer: AI story writing - github_blogs: GitHub blog integration - speech_to_blog: Audio to blog conversion - twitter_writers: Twitter/X content generation - youtube_writers: YouTube content generation These tools are in ToBeMigrated/ for future migration to the main backend.
This commit is contained in:
@@ -0,0 +1,96 @@
|
||||
# YouTube Thumbnail Generator
|
||||
|
||||
A powerful AI-powered tool for creating engaging, click-worthy thumbnails for your YouTube videos.
|
||||
|
||||
## Overview
|
||||
|
||||
The YouTube Thumbnail Generator is a specialized module within the AI Writer suite that helps content creators design eye-catching thumbnails optimized for YouTube. Using advanced AI image generation technology, this tool creates custom thumbnails based on your video content, target audience, and style preferences.
|
||||
|
||||
## Features
|
||||
|
||||
### 1. AI-Powered Thumbnail Generation
|
||||
- **Concept Generation**: Automatically generates multiple thumbnail concept ideas based on your video title, description, and target audience
|
||||
- **Visual Design**: Creates high-quality thumbnail images using state-of-the-art AI image generation
|
||||
- **Style Customization**: Choose from various style preferences including bold, clean, colorful, dark, professional, playful, retro, and modern
|
||||
|
||||
### 2. Advanced Customization Options
|
||||
- **Aspect Ratio Selection**: Choose from standard YouTube ratios (16:9, 1:1, 4:3, 9:16)
|
||||
- **Text Overlay Options**: Add and customize text overlays with different styles
|
||||
- **Image Style Selection**: Choose from photorealistic, artistic, cartoon/anime, sketch/drawing, digital art, or 3D render
|
||||
- **Focus Selection**: For photorealistic images, specify focus areas like portraits, objects, motion, or wide-angle
|
||||
|
||||
### 3. Thumbnail Editing
|
||||
- **AI-Powered Editing**: Make changes to your generated thumbnails using natural language instructions
|
||||
- **Iterative Refinement**: Continue editing until you're satisfied with the result
|
||||
- **Preserve Original**: Keep both original and edited versions of your thumbnails
|
||||
|
||||
### 4. Thumbnail Analysis
|
||||
- **AI Analysis**: Get feedback on your thumbnail's effectiveness
|
||||
- **Improvement Suggestions**: Receive specific recommendations to enhance your thumbnail's impact
|
||||
- **Best Practices**: Learn about visual hierarchy, text readability, emotional impact, and click-worthiness
|
||||
|
||||
### 5. User-Friendly Interface
|
||||
- **Tabbed Interface**: Organize your workflow with intuitive tabs for basic info and style settings
|
||||
- **Concept Tabs**: View and select from multiple thumbnail concepts
|
||||
- **Real-time Preview**: See your generated thumbnails immediately
|
||||
- **Download Options**: Easily download your thumbnails in high resolution
|
||||
|
||||
## How to Use
|
||||
|
||||
### Step 1: Enter Basic Information
|
||||
- Provide your video title and description
|
||||
- Specify your target audience
|
||||
- Select your content type (tutorial, vlog, review, etc.)
|
||||
|
||||
### Step 2: Customize Style Preferences
|
||||
- Choose your preferred thumbnail style
|
||||
- Select the number of concepts to generate
|
||||
- Pick your desired aspect ratio
|
||||
- Configure text overlay options
|
||||
|
||||
### Step 3: Generate Thumbnail Concepts
|
||||
- Click "Generate Thumbnail Concepts" to create multiple thumbnail ideas
|
||||
- Review each concept in the provided tabs
|
||||
- Select the concept you'd like to develop further
|
||||
|
||||
### Step 4: Generate and Customize Your Thumbnail
|
||||
- Click "Generate Image" for your selected concept
|
||||
- Use the editing tools to refine your thumbnail
|
||||
- Apply changes using natural language instructions
|
||||
- Download your final thumbnail when satisfied
|
||||
|
||||
### Step 5: Analyze Your Thumbnail
|
||||
- Use the "Analyze Thumbnail" feature to get AI feedback
|
||||
- Review suggestions for improvement
|
||||
- Make additional edits based on the analysis
|
||||
|
||||
## Technical Details
|
||||
|
||||
The Thumbnail Generator uses:
|
||||
- **Gemini AI**: For high-quality image generation and editing
|
||||
- **Advanced Prompt Engineering**: To ensure consistent and relevant results
|
||||
- **Retry Mechanism**: Handles service overloads with exponential backoff
|
||||
- **Session State Management**: Preserves your work across page refreshes
|
||||
|
||||
## Tips for Best Results
|
||||
|
||||
1. **Be Specific**: Provide detailed video descriptions to help the AI understand your content
|
||||
2. **Target Your Audience**: Specify your audience demographics and interests
|
||||
3. **Choose Appropriate Style**: Select a style that matches your channel's branding
|
||||
4. **Use Keywords**: Add relevant keywords to enhance the AI's understanding
|
||||
5. **Iterate**: Don't hesitate to generate multiple concepts and make edits
|
||||
6. **Analyze**: Use the analysis feature to get objective feedback on your thumbnails
|
||||
|
||||
## Requirements
|
||||
|
||||
- Internet connection for AI services
|
||||
- Modern web browser
|
||||
- No additional software installation required
|
||||
|
||||
## Support
|
||||
|
||||
For technical issues or feature requests, please contact our support team or submit an issue on our GitHub repository.
|
||||
|
||||
---
|
||||
|
||||
*The YouTube Thumbnail Generator is part of the AI Writer suite, designed to help content creators streamline their workflow and produce high-quality content.*
|
||||
@@ -0,0 +1,108 @@
|
||||
End Screen Generator feature for YouTube videos.
|
||||
|
||||
## Step 1: Understanding End Screens
|
||||
|
||||
YouTube end screens are the final elements shown at the end of a video that encourage viewers to take action, such as subscribing, watching another video, or visiting a website. They typically include:
|
||||
|
||||
1. Call-to-action elements (subscribe button, playlists, other videos)
|
||||
2. Visual elements (background image, branding)
|
||||
3. Text overlays (promotional messages, channel name)
|
||||
4. Layout options (different templates for different purposes)
|
||||
|
||||
## Step 2: Required User Inputs
|
||||
|
||||
Based on the thumbnail generator and YouTube end screen requirements, we'll need these inputs:
|
||||
|
||||
1. **Basic Video Information**:
|
||||
- Video title
|
||||
- Video description
|
||||
- Target audience
|
||||
- Content type (tutorial, vlog, review, etc.)
|
||||
|
||||
2. **End Screen Purpose**:
|
||||
- Primary goal (drive subscriptions, promote playlist, promote next video, etc.)
|
||||
- Secondary goal (if applicable)
|
||||
|
||||
3. **Visual Style Preferences**:
|
||||
- Color scheme
|
||||
- Style (minimal, bold, branded, etc.)
|
||||
- Brand elements to include (logo, channel name, etc.)
|
||||
|
||||
4. **Content Elements**:
|
||||
- Number of elements to include (1-4)
|
||||
- Types of elements (subscribe button, playlist, video, website)
|
||||
- Text for each element
|
||||
|
||||
5. **Advanced Settings**:
|
||||
- Background style (solid color, gradient, image, etc.)
|
||||
- Animation preferences
|
||||
- Custom branding elements
|
||||
|
||||
## Step 3: Implementation Plan
|
||||
|
||||
Let's create a new module called `end_screen_generator.py` in the same directory as the thumbnail generator. Here's how we'll structure it:
|
||||
|
||||
1. **Functions**:
|
||||
- `generate_end_screen_concepts`: Generate end screen design concepts
|
||||
- `generate_end_screen_design`: Create visual end screen designs
|
||||
- `analyze_end_screen`: Provide feedback on end screen effectiveness
|
||||
- `write_yt_end_screen`: Main UI function
|
||||
|
||||
2. **User Interface**:
|
||||
- Tabs for different sections (Basic Info, Style & Elements, Preview)
|
||||
- Input fields for all required information
|
||||
- Preview section to show generated end screens
|
||||
- Download options for the end screen designs
|
||||
|
||||
|
||||
### End Screen Generator Features
|
||||
|
||||
1. **Comprehensive User Inputs**:
|
||||
- Basic video information (title, description, target audience)
|
||||
- End screen purpose (subscribe, next video, playlist, website, social media)
|
||||
- Visual style preferences (modern, minimalist, bold, playful, elegant)
|
||||
- Content elements (text, CTAs, visual elements)
|
||||
- Advanced settings (image style, focus, keywords)
|
||||
|
||||
2. **AI-Powered Generation**:
|
||||
- Concept generation with detailed descriptions
|
||||
- Image generation with style customization
|
||||
- Thumbnail analysis for effectiveness
|
||||
- Image editing capabilities
|
||||
|
||||
3. **User Interface**:
|
||||
- Tabbed interface for multiple end screen concepts
|
||||
- Visual preview of generated end screens
|
||||
- Download options for all generated images
|
||||
- Edit functionality for refining designs
|
||||
|
||||
4. **Integration with Existing Tools**:
|
||||
- Reuses the image generation and editing functions from the thumbnail generator
|
||||
- Consistent UI/UX with other YouTube tools
|
||||
- Proper error handling and logging
|
||||
|
||||
### How to Use the End Screen Generator
|
||||
|
||||
1. **Access the Tool**:
|
||||
- Select "End Screen Generator" from the YouTube tools menu
|
||||
- The tool is now active and ready to use
|
||||
|
||||
2. **Generate End Screens**:
|
||||
- Enter your video details (title, description, target audience)
|
||||
- Select the primary purpose of your end screen
|
||||
- Choose your preferred visual style
|
||||
- Select content elements to include
|
||||
- Optionally customize advanced settings
|
||||
- Click "Generate End Screen Concepts"
|
||||
|
||||
3. **Review and Customize**:
|
||||
- Browse through the generated concepts in tabs
|
||||
- Generate images for concepts you like
|
||||
- Edit the generated images with specific instructions
|
||||
- Download your final end screen designs
|
||||
|
||||
4. **Analyze Effectiveness**:
|
||||
- Get AI-powered analysis of your end screen designs
|
||||
- Receive feedback on visual hierarchy, text readability, and more
|
||||
|
||||
The End Screen Generator is now fully integrated into the YouTube AI Writer and ready to use. Would you like me to make any adjustments or enhancements to the implementation?
|
||||
@@ -0,0 +1,273 @@
|
||||
# YouTube Shorts Script Generator 📱
|
||||
|
||||
Welcome to the ultimate YouTube Shorts Script Generator! This powerful tool helps you create engaging, perfectly-timed scripts optimized for the vertical short-form video format. Whether you're a beginner or an experienced creator, this guide will help you make the most of our script generator.
|
||||
|
||||
## 🎯 Why Use This Tool?
|
||||
|
||||
- Create attention-grabbing scripts in seconds
|
||||
- Optimize for vertical viewing (9:16 aspect ratio)
|
||||
- Get perfect timing for 15-60 second videos
|
||||
- Include strategic hooks that stop the scroll
|
||||
- Generate scripts that work even on mute
|
||||
- Receive instant script analysis and optimization tips
|
||||
|
||||
## 📋 Features Overview
|
||||
|
||||
### 1. Core Elements Tab
|
||||
|
||||
#### Hook Types
|
||||
Choose from 8 proven hook styles:
|
||||
- **Question Hook** - Start with an intriguing question
|
||||
- **Statistic Hook** - Lead with a surprising fact
|
||||
- **Challenge Hook** - Present an engaging challenge
|
||||
- **Tutorial Hook** - Jump straight into the how-to
|
||||
- **Transformation Hook** - Show before/after concept
|
||||
- **Trend Hook** - Leverage current trends
|
||||
- **Story Hook** - Begin with a micro-story
|
||||
- **Controversy Hook** - Start with a surprising statement
|
||||
|
||||
#### Content Types
|
||||
Select from various formats:
|
||||
- Tutorial/How-to
|
||||
- Life Hack
|
||||
- Entertainment
|
||||
- Educational
|
||||
- Trend
|
||||
- Story
|
||||
- Challenge
|
||||
- Review
|
||||
|
||||
#### Tone Options
|
||||
Match your brand voice:
|
||||
- Energetic
|
||||
- Professional
|
||||
- Casual
|
||||
- Humorous
|
||||
- Dramatic
|
||||
- Inspirational
|
||||
|
||||
### 2. Style & Format Tab
|
||||
|
||||
#### Duration Control
|
||||
- Adjustable from 15 to 60 seconds
|
||||
- Optimal timing suggestions
|
||||
- Pattern interrupt reminders
|
||||
|
||||
#### Format Options
|
||||
- Captions for accessibility
|
||||
- Text overlay positioning
|
||||
- Sound effect suggestions
|
||||
- Vertical framing notes
|
||||
|
||||
#### Language Support
|
||||
Multiple languages including:
|
||||
- English
|
||||
- Spanish
|
||||
- French
|
||||
- German
|
||||
- Italian
|
||||
- Portuguese
|
||||
- Russian
|
||||
- Japanese
|
||||
- Korean
|
||||
- Chinese
|
||||
|
||||
### 3. Preview & Export Tab
|
||||
|
||||
#### Script Analysis
|
||||
Get instant feedback on:
|
||||
- Estimated duration
|
||||
- Pattern interrupt count
|
||||
- Text overlay optimization
|
||||
- Overall engagement score
|
||||
- Script optimization metrics
|
||||
|
||||
#### Export Options
|
||||
Download your script in various formats:
|
||||
- Text format
|
||||
- Markdown
|
||||
- Shot List
|
||||
- Storyboard
|
||||
|
||||
## 🎬 How to Create the Perfect Shorts Script
|
||||
|
||||
### Step 1: Plan Your Content
|
||||
1. **Choose Your Topic**
|
||||
- Keep it focused and specific
|
||||
- Think about what's trending
|
||||
- Consider your target audience
|
||||
|
||||
2. **Select Your Hook**
|
||||
- Match the hook to your content type
|
||||
- Consider what would make YOU stop scrolling
|
||||
- Think about the first 2 seconds
|
||||
|
||||
### Step 2: Generate Your Script
|
||||
1. Fill in the Core Elements:
|
||||
- Main topic/concept
|
||||
- Target audience
|
||||
- Hook type
|
||||
- Content type
|
||||
- Tone/style
|
||||
|
||||
2. Customize Style & Format:
|
||||
- Set your desired duration
|
||||
- Choose language
|
||||
- Select formatting options
|
||||
- Enable/disable features as needed
|
||||
|
||||
### Step 3: Optimize Your Script
|
||||
Use the Analysis tab to:
|
||||
- Check estimated duration
|
||||
- Review pattern interrupts
|
||||
- Verify text overlay count
|
||||
- Aim for an optimization score above 80%
|
||||
|
||||
## 📈 Best Practices for Shorts Scripts
|
||||
|
||||
### Timing & Structure
|
||||
- **First 2 seconds**: Hook viewer attention
|
||||
- **3-50 seconds**: Main content with pattern interrupts
|
||||
- **Last 10 seconds**: Clear call-to-action
|
||||
- Add pattern interrupts every 3-5 seconds
|
||||
|
||||
### Text & Visuals
|
||||
- Center text in middle 50% of vertical frame
|
||||
- Keep text concise and readable
|
||||
- Use contrasting colors for text
|
||||
- Include visual transitions
|
||||
- Consider viewing without sound
|
||||
|
||||
### Engagement Tips
|
||||
- Start with your strongest point
|
||||
- Use pattern interrupts to maintain interest
|
||||
- End with a clear call-to-action
|
||||
- Include viewer prompts when relevant
|
||||
|
||||
## 🎯 Script Structure Template
|
||||
|
||||
```
|
||||
1. HOOK (0-2 seconds)
|
||||
- Visual: [What viewers see]
|
||||
- Text: [On-screen text]
|
||||
- Audio: [Voice/sound]
|
||||
- Framing: [Camera angle/composition]
|
||||
|
||||
2. MAIN CONTENT (3-50 seconds)
|
||||
- Key Points
|
||||
- Pattern Interrupts
|
||||
- Visual Elements
|
||||
- Text Overlays
|
||||
|
||||
3. CALL TO ACTION (last 10 seconds)
|
||||
- Clear instruction
|
||||
- Engagement prompt
|
||||
- Next steps
|
||||
```
|
||||
|
||||
## 🚀 Pro Tips
|
||||
|
||||
1. **Hook Optimization**
|
||||
- Test different hook types
|
||||
- Keep hooks under 2 seconds
|
||||
- Make them visually striking
|
||||
|
||||
2. **Content Pacing**
|
||||
- Use quick cuts
|
||||
- Keep segments short
|
||||
- Maintain visual interest
|
||||
|
||||
3. **Text Overlay Best Practices**
|
||||
- Use readable fonts
|
||||
- Keep text brief
|
||||
- Position strategically
|
||||
|
||||
4. **Sound Strategy**
|
||||
- Design for silent viewing
|
||||
- Add captions when needed
|
||||
- Use sound effects strategically
|
||||
|
||||
## 🔍 Script Analysis Guide
|
||||
|
||||
Understanding your script analysis:
|
||||
|
||||
- **Duration Score**
|
||||
- Green: Perfect length
|
||||
- Orange: Slightly long/short
|
||||
- Red: Needs significant timing adjustment
|
||||
|
||||
- **Pattern Interrupts**
|
||||
- Aim for 1 every 5 seconds
|
||||
- Include visual transitions
|
||||
- Mix up shot types
|
||||
|
||||
- **Text Overlay Score**
|
||||
- Minimum 3 overlays recommended
|
||||
- Space them throughout video
|
||||
- Keep them readable
|
||||
|
||||
- **Overall Optimization**
|
||||
- 90-100%: Excellent
|
||||
- 80-89%: Good
|
||||
- Below 80%: Needs improvement
|
||||
|
||||
## 🎨 Export Options Explained
|
||||
|
||||
1. **Text Format**
|
||||
- Clean, simple script
|
||||
- Easy to copy/paste
|
||||
- Basic formatting
|
||||
|
||||
2. **Markdown**
|
||||
- Formatted sections
|
||||
- Easy to read
|
||||
- Good for documentation
|
||||
|
||||
3. **Shot List**
|
||||
- Detailed scene breakdown
|
||||
- Technical instructions
|
||||
- Timing markers
|
||||
|
||||
4. **Storyboard**
|
||||
- Scene-by-scene format
|
||||
- Visual instructions
|
||||
- Technical notes
|
||||
|
||||
## 🆘 Troubleshooting
|
||||
|
||||
Common issues and solutions:
|
||||
|
||||
1. **Script Too Long**
|
||||
- Reduce main points
|
||||
- Shorten sentences
|
||||
- Speed up pacing
|
||||
|
||||
2. **Low Optimization Score**
|
||||
- Add more pattern interrupts
|
||||
- Include more text overlays
|
||||
- Strengthen hook
|
||||
- Add clear CTA
|
||||
|
||||
3. **Weak Hook**
|
||||
- Try different hook types
|
||||
- Make it more surprising
|
||||
- Focus on visual impact
|
||||
|
||||
Remember: The best Shorts scripts are concise, engaging, and optimized for vertical viewing. Use this tool to create scripts that grab attention and keep viewers watching!
|
||||
|
||||
## 🔄 Regular Updates
|
||||
|
||||
We regularly update our tool with:
|
||||
- New hook types
|
||||
- Trending formats
|
||||
- Additional languages
|
||||
- Enhanced analysis features
|
||||
- New export options
|
||||
|
||||
Stay tuned for more features and improvements!
|
||||
|
||||
---
|
||||
|
||||
Happy Creating! 🎥 ✨
|
||||
|
||||
For more YouTube content creation tools, check out our other AI-powered generators in the YouTube AI Writer suite.
|
||||
@@ -0,0 +1,5 @@
|
||||
"""
|
||||
YouTube AI Writer Modules
|
||||
|
||||
This package contains modular components for the YouTube AI Writer functionality.
|
||||
"""
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,591 @@
|
||||
"""
|
||||
YouTube Community Post Generator Module
|
||||
|
||||
This module provides sophisticated functionality for generating engaging community posts
|
||||
with AI-powered content suggestions, engagement analysis, and timing optimization.
|
||||
"""
|
||||
|
||||
import streamlit as st
|
||||
import time
|
||||
import logging
|
||||
import random
|
||||
from datetime import datetime, timedelta
|
||||
from lib.gpt_providers.text_generation.main_text_generation import llm_text_gen
|
||||
import re
|
||||
from textblob import TextBlob
|
||||
|
||||
# Configure logging
|
||||
logging.basicConfig(
|
||||
level=logging.INFO,
|
||||
format='%(asctime)s - %(name)s - %(levelname)s - %(message)s'
|
||||
)
|
||||
logger = logging.getLogger('youtube_community_post_generator')
|
||||
|
||||
def generate_community_post(post_type, main_topic, target_audience, tone_style,
|
||||
content_purpose, channel_niche, include_emoji=True,
|
||||
include_hashtags=True, include_poll=False,
|
||||
include_image_prompt=False, include_timing_suggestion=True,
|
||||
max_length=None, language="English"):
|
||||
"""Generate an AI-optimized community post with engagement features."""
|
||||
|
||||
# Create a custom system prompt for community post generation
|
||||
system_prompt = f"""You are a YouTube Community Post expert specializing in creating highly engaging,
|
||||
conversion-optimized posts that drive channel growth and viewer interaction.
|
||||
Focus on creating posts that encourage meaningful engagement while maintaining the channel's voice.
|
||||
Write the entire post in {language}.
|
||||
Consider timing, audience psychology, and platform-specific best practices."""
|
||||
|
||||
# Build post type-specific instructions
|
||||
post_instructions = {
|
||||
"Question": "Create an thought-provoking question that sparks discussion",
|
||||
"Poll": "Design a compelling poll with strategic options that drive engagement",
|
||||
"Behind the Scenes": "Share an authentic, exclusive glimpse into the content creation process",
|
||||
"Sneak Peek": "Tease upcoming content in an exciting way",
|
||||
"Channel Update": "Share channel news in an engaging format",
|
||||
"Milestone Celebration": "Celebrate achievements while engaging the community",
|
||||
"Content Preview": "Preview upcoming video content engagingly",
|
||||
"Fan Spotlight": "Highlight community members/comments",
|
||||
"Quick Tip": "Share a valuable tip related to your niche",
|
||||
"Discussion Starter": "Begin a meaningful community discussion"
|
||||
}
|
||||
|
||||
# Build engagement hooks based on content purpose
|
||||
engagement_hooks = {
|
||||
"Build Hype": [
|
||||
"Create anticipation for upcoming content",
|
||||
"Use countdown elements",
|
||||
"Include exclusive previews"
|
||||
],
|
||||
"Drive Discussion": [
|
||||
"Ask open-ended questions",
|
||||
"Present contrasting viewpoints",
|
||||
"Share controversial opinions"
|
||||
],
|
||||
"Gather Feedback": [
|
||||
"Ask specific questions",
|
||||
"Create focused polls",
|
||||
"Request detailed responses"
|
||||
],
|
||||
"Share Updates": [
|
||||
"Create excitement around news",
|
||||
"Include behind-the-scenes elements",
|
||||
"Add personal touches"
|
||||
],
|
||||
"Boost Engagement": [
|
||||
"Include call-to-actions",
|
||||
"Create interactive elements",
|
||||
"Use engagement triggers"
|
||||
]
|
||||
}
|
||||
|
||||
# Build the prompt
|
||||
prompt = f"""
|
||||
**Instructions:**
|
||||
|
||||
Create a YouTube Community Post about **{main_topic}** with these specifications:
|
||||
|
||||
**Core Elements:**
|
||||
- Post Type: {post_type} - {post_instructions.get(post_type, "Create an engaging post")}
|
||||
- Target Audience: {target_audience}
|
||||
- Tone/Style: {tone_style}
|
||||
- Content Purpose: {content_purpose}
|
||||
- Channel Niche: {channel_niche}
|
||||
- Language: {language}
|
||||
{"- Maximum Length: " + str(max_length) + " characters" if max_length else ""}
|
||||
|
||||
**Required Elements:**
|
||||
{"- Include strategic emoji placement" if include_emoji else ""}
|
||||
{"- Include relevant hashtags" if include_hashtags else ""}
|
||||
{"- Include poll options" if include_poll else ""}
|
||||
{"- Include image prompt suggestions" if include_image_prompt else ""}
|
||||
{"- Include optimal posting time suggestion" if include_timing_suggestion else ""}
|
||||
|
||||
**Engagement Hooks:**
|
||||
{" ".join(engagement_hooks.get(content_purpose, ["Create engaging content"]))}
|
||||
|
||||
**Format the post with:**
|
||||
1. Main Content
|
||||
2. Engagement Elements
|
||||
3. Call-to-Action
|
||||
4. Additional Components (hashtags, etc.)
|
||||
|
||||
**Remember:**
|
||||
- Keep the tone consistent with channel voice
|
||||
- Use psychology triggers for engagement
|
||||
- Include clear call-to-actions
|
||||
- Make it easy to respond to
|
||||
- Create shareable content
|
||||
"""
|
||||
|
||||
try:
|
||||
response = llm_text_gen(prompt, system_prompt=system_prompt)
|
||||
return response
|
||||
except Exception as err:
|
||||
st.error(f"Error: Failed to get response from LLM: {err}")
|
||||
return None
|
||||
|
||||
def analyze_post_engagement(post_content):
|
||||
"""Analyze a community post for engagement potential using advanced AI metrics."""
|
||||
analysis = {
|
||||
'engagement_score': 0,
|
||||
'emotional_triggers': 0,
|
||||
'call_to_action_strength': 0,
|
||||
'readability_score': 0,
|
||||
'hashtag_optimization': 0,
|
||||
'timing_recommendation': None,
|
||||
'sentiment_analysis': {},
|
||||
'virality_potential': 0,
|
||||
'audience_resonance': 0,
|
||||
'content_uniqueness': 0,
|
||||
'psychological_triggers': [],
|
||||
'improvement_suggestions': [],
|
||||
'engagement_patterns': {},
|
||||
'content_structure': {},
|
||||
'seo_optimization': 0
|
||||
}
|
||||
|
||||
# Sentiment Analysis using TextBlob
|
||||
blob = TextBlob(post_content)
|
||||
analysis['sentiment_analysis'] = {
|
||||
'polarity': round((blob.sentiment.polarity + 1) * 50, 2), # Convert to 0-100 scale
|
||||
'subjectivity': round(blob.sentiment.subjectivity * 100, 2),
|
||||
'tone': 'Positive' if blob.sentiment.polarity > 0 else 'Negative' if blob.sentiment.polarity < 0 else 'Neutral'
|
||||
}
|
||||
|
||||
# Analyze emotional triggers with expanded vocabulary
|
||||
emotional_categories = {
|
||||
'excitement': ['excited', 'amazing', 'incredible', 'awesome', 'mind-blowing'],
|
||||
'curiosity': ['guess what', 'secret', 'revealed', 'discover', 'mystery'],
|
||||
'urgency': ['limited', 'hurry', 'soon', 'don\'t miss', 'last chance'],
|
||||
'social_proof': ['everyone', 'community', 'fans', 'you all', 'together'],
|
||||
'exclusivity': ['exclusive', 'special', 'limited', 'only', 'selected']
|
||||
}
|
||||
|
||||
trigger_counts = {category: 0 for category in emotional_categories}
|
||||
for category, words in emotional_categories.items():
|
||||
trigger_counts[category] = sum(post_content.lower().count(word) for word in words)
|
||||
|
||||
analysis['emotional_triggers'] = min(sum(trigger_counts.values()) * 15, 100)
|
||||
analysis['psychological_triggers'] = [cat for cat, count in trigger_counts.items() if count > 0]
|
||||
|
||||
# Analyze call-to-action strength with pattern recognition
|
||||
cta_patterns = {
|
||||
'question_cta': r'\?',
|
||||
'direct_command': r'(?i)(comment|share|like|subscribe|follow)',
|
||||
'engagement_request': r'(?i)(let (me|us) know|tell (me|us)|what do you think)',
|
||||
'time_sensitive': r'(?i)(today|now|limited time|hurry)',
|
||||
'value_proposition': r'(?i)(learn|discover|find out|get|access)'
|
||||
}
|
||||
|
||||
cta_strength = 0
|
||||
for pattern_type, pattern in cta_patterns.items():
|
||||
matches = len(re.findall(pattern, post_content))
|
||||
cta_strength += matches * 20
|
||||
analysis['call_to_action_strength'] = min(cta_strength, 100)
|
||||
|
||||
# Content Structure Analysis
|
||||
analysis['content_structure'] = {
|
||||
'length_score': min(len(post_content.split()) / 5, 100), # Optimal length analysis
|
||||
'paragraph_breaks': min(post_content.count('\n\n') * 20, 100), # Readability through structure
|
||||
'emoji_balance': min(len(re.findall(r'[\U0001F300-\U0001F9FF]', post_content)) * 10, 100), # Emoji usage score
|
||||
'formatting_score': min((post_content.count('*') + post_content.count('_')) * 5, 100) # Text formatting score
|
||||
}
|
||||
|
||||
# Virality Potential Analysis
|
||||
virality_factors = {
|
||||
'emotional_impact': analysis['emotional_triggers'],
|
||||
'shareability': analysis['content_structure']['length_score'],
|
||||
'uniqueness': random.randint(60, 100), # Simulated uniqueness score
|
||||
'timeliness': 80 if any(word in post_content.lower() for word in ['new', 'breaking', 'update', 'just']) else 50
|
||||
}
|
||||
analysis['virality_potential'] = sum(virality_factors.values()) / len(virality_factors)
|
||||
|
||||
# Audience Resonance Analysis
|
||||
resonance_factors = {
|
||||
'relevance': analysis['sentiment_analysis']['subjectivity'],
|
||||
'engagement_hooks': analysis['call_to_action_strength'],
|
||||
'emotional_connection': analysis['emotional_triggers']
|
||||
}
|
||||
analysis['audience_resonance'] = sum(resonance_factors.values()) / len(resonance_factors)
|
||||
|
||||
# SEO Optimization
|
||||
seo_factors = {
|
||||
'hashtag_quality': analyze_hashtag_quality(post_content),
|
||||
'keyword_density': analyze_keyword_density(post_content),
|
||||
'url_presence': 100 if 'http' in post_content else 0,
|
||||
'mention_optimization': analyze_mentions(post_content)
|
||||
}
|
||||
analysis['seo_optimization'] = sum(seo_factors.values()) / len(seo_factors)
|
||||
|
||||
# Engagement Pattern Analysis
|
||||
analysis['engagement_patterns'] = analyze_engagement_patterns(post_content)
|
||||
|
||||
# Calculate overall engagement score with weighted components
|
||||
analysis['engagement_score'] = calculate_weighted_score({
|
||||
'emotional_triggers': (analysis['emotional_triggers'], 0.2),
|
||||
'call_to_action_strength': (analysis['call_to_action_strength'], 0.2),
|
||||
'virality_potential': (analysis['virality_potential'], 0.15),
|
||||
'audience_resonance': (analysis['audience_resonance'], 0.15),
|
||||
'seo_optimization': (analysis['seo_optimization'], 0.1),
|
||||
'sentiment_balance': (analysis['sentiment_analysis']['polarity'], 0.1),
|
||||
'content_structure': (sum(analysis['content_structure'].values()) / len(analysis['content_structure']), 0.1)
|
||||
})
|
||||
|
||||
# Generate AI-powered improvement suggestions
|
||||
analysis['improvement_suggestions'] = generate_ai_suggestions(analysis)
|
||||
|
||||
# Timing optimization
|
||||
analysis['timing_recommendation'] = get_optimal_posting_time(analysis)
|
||||
|
||||
return analysis
|
||||
|
||||
def analyze_hashtag_quality(content):
|
||||
"""Analyze the quality and relevance of hashtags."""
|
||||
hashtags = re.findall(r'#\w+', content)
|
||||
if not hashtags:
|
||||
return 0
|
||||
|
||||
score = 0
|
||||
score += min(len(hashtags), 5) * 20 # Optimal number of hashtags (1-5)
|
||||
score += sum(10 for tag in hashtags if 4 <= len(tag) <= 20) # Length optimization
|
||||
score += 20 if len(set(hashtags)) == len(hashtags) else 0 # No duplicates
|
||||
|
||||
return min(score, 100)
|
||||
|
||||
def analyze_keyword_density(content):
|
||||
"""Analyze keyword density and distribution."""
|
||||
words = content.lower().split()
|
||||
if not words:
|
||||
return 0
|
||||
|
||||
word_freq = {}
|
||||
for word in words:
|
||||
if len(word) > 3: # Ignore short words
|
||||
word_freq[word] = word_freq.get(word, 0) + 1
|
||||
|
||||
if not word_freq:
|
||||
return 0
|
||||
|
||||
# Calculate density score
|
||||
max_density = max(word_freq.values()) / len(words)
|
||||
return 100 if 0.01 <= max_density <= 0.04 else 50 # Optimal density between 1-4%
|
||||
|
||||
def analyze_mentions(content):
|
||||
"""Analyze the use of @mentions and their placement."""
|
||||
mentions = re.findall(r'@\w+', content)
|
||||
if not mentions:
|
||||
return 0
|
||||
|
||||
score = 0
|
||||
score += min(len(mentions), 3) * 25 # Optimal number of mentions (1-3)
|
||||
score += 25 if mentions[0] in content.split()[:len(content.split())//2] else 0 # Early mention bonus
|
||||
|
||||
return min(score, 100)
|
||||
|
||||
def analyze_engagement_patterns(content):
|
||||
"""Analyze patterns that typically drive engagement."""
|
||||
patterns = {
|
||||
'question_hooks': len(re.findall(r'\?', content)),
|
||||
'emotional_words': len(re.findall(r'\b(love|hate|amazing|awesome|incredible|excited)\b', content.lower())),
|
||||
'community_references': len(re.findall(r'\b(we|our|community|together|everyone)\b', content.lower())),
|
||||
'action_words': len(re.findall(r'\b(get|do|make|try|click|watch|share)\b', content.lower())),
|
||||
'urgency_triggers': len(re.findall(r'\b(now|today|limited|soon|hurry)\b', content.lower()))
|
||||
}
|
||||
|
||||
return {k: min(v * 20, 100) for k, v in patterns.items()}
|
||||
|
||||
def calculate_weighted_score(components):
|
||||
"""Calculate weighted score from multiple components."""
|
||||
return sum(score * weight for (score, weight) in components.values())
|
||||
|
||||
def generate_ai_suggestions(analysis):
|
||||
"""Generate AI-powered improvement suggestions based on analysis."""
|
||||
suggestions = []
|
||||
|
||||
if analysis['emotional_triggers'] < 70:
|
||||
suggestions.append({
|
||||
'category': 'Emotional Impact',
|
||||
'suggestion': 'Add more emotional triggers to increase engagement',
|
||||
'examples': ['amazing', 'incredible', 'exciting']
|
||||
})
|
||||
|
||||
if analysis['call_to_action_strength'] < 70:
|
||||
suggestions.append({
|
||||
'category': 'Call-to-Action',
|
||||
'suggestion': 'Strengthen your call-to-action',
|
||||
'examples': ['Comment below', 'Share your thoughts', 'Let me know']
|
||||
})
|
||||
|
||||
if analysis['virality_potential'] < 70:
|
||||
suggestions.append({
|
||||
'category': 'Virality',
|
||||
'suggestion': 'Increase viral potential by adding trending elements',
|
||||
'examples': ['Current trends', 'Popular hashtags', 'Timely topics']
|
||||
})
|
||||
|
||||
if analysis['seo_optimization'] < 70:
|
||||
suggestions.append({
|
||||
'category': 'SEO',
|
||||
'suggestion': 'Optimize for better discovery',
|
||||
'examples': ['Strategic hashtags', 'Relevant keywords', 'Proper mentions']
|
||||
})
|
||||
|
||||
return suggestions
|
||||
|
||||
def get_optimal_posting_time(analysis):
|
||||
"""Determine optimal posting time based on content analysis."""
|
||||
current_hour = datetime.now().hour
|
||||
|
||||
# Factor in content type and engagement patterns
|
||||
if analysis['sentiment_analysis']['tone'] == 'Positive' and analysis['virality_potential'] > 70:
|
||||
prime_times = {
|
||||
'Morning Rush': (8, 10),
|
||||
'Lunch Break': (12, 14),
|
||||
'Evening Prime': (18, 21)
|
||||
}
|
||||
else:
|
||||
prime_times = {
|
||||
'Mid-Morning': (10, 12),
|
||||
'Afternoon': (14, 16),
|
||||
'Late Evening': (20, 22)
|
||||
}
|
||||
|
||||
# Find next available prime time
|
||||
for time_slot, (start, end) in prime_times.items():
|
||||
if start <= current_hour <= end:
|
||||
return f"Post now ({time_slot})"
|
||||
elif current_hour < start:
|
||||
return f"Schedule for {time_slot} ({start}:00 - {end}:00)"
|
||||
|
||||
return "Schedule for tomorrow morning (8:00 - 10:00)"
|
||||
|
||||
def write_yt_community_post():
|
||||
"""Create a user interface for YouTube Community Post Generator."""
|
||||
st.write("Generate engaging community posts that drive interaction and channel growth.")
|
||||
|
||||
# Initialize session state
|
||||
if "generated_post" not in st.session_state:
|
||||
st.session_state.generated_post = None
|
||||
if "post_history" not in st.session_state:
|
||||
st.session_state.post_history = []
|
||||
|
||||
# Create tabs for different sections
|
||||
tab1, tab2, tab3 = st.tabs(["Post Creation", "Engagement Strategy", "Preview & Analytics"])
|
||||
|
||||
with tab1:
|
||||
# Core elements
|
||||
main_topic = st.text_area("Main Topic/Message",
|
||||
placeholder="e.g., New video announcement, Channel update, Question for viewers")
|
||||
|
||||
col1, col2 = st.columns(2)
|
||||
with col1:
|
||||
post_type = st.selectbox("Post Type", [
|
||||
"Question",
|
||||
"Poll",
|
||||
"Behind the Scenes",
|
||||
"Sneak Peek",
|
||||
"Channel Update",
|
||||
"Milestone Celebration",
|
||||
"Content Preview",
|
||||
"Fan Spotlight",
|
||||
"Quick Tip",
|
||||
"Discussion Starter"
|
||||
])
|
||||
|
||||
target_audience = st.text_input("Target Audience",
|
||||
placeholder="e.g., Tech enthusiasts, Gamers, DIY lovers")
|
||||
|
||||
with col2:
|
||||
content_purpose = st.selectbox("Content Purpose", [
|
||||
"Build Hype",
|
||||
"Drive Discussion",
|
||||
"Gather Feedback",
|
||||
"Share Updates",
|
||||
"Boost Engagement"
|
||||
])
|
||||
|
||||
tone_style = st.selectbox("Tone/Style", [
|
||||
"Casual",
|
||||
"Professional",
|
||||
"Excited",
|
||||
"Mysterious",
|
||||
"Humorous",
|
||||
"Informative"
|
||||
])
|
||||
|
||||
channel_niche = st.text_input("Channel Niche",
|
||||
placeholder="e.g., Tech Reviews, Gaming, Education")
|
||||
|
||||
with tab2:
|
||||
# Engagement options
|
||||
st.subheader("Engagement Elements")
|
||||
col1, col2 = st.columns(2)
|
||||
|
||||
with col1:
|
||||
include_emoji = st.checkbox("Include Emojis", value=True)
|
||||
include_hashtags = st.checkbox("Include Hashtags", value=True)
|
||||
max_length = st.number_input("Maximum Length (characters)",
|
||||
min_value=100, max_value=2000, value=500)
|
||||
|
||||
with col2:
|
||||
include_poll = st.checkbox("Include Poll", value=False)
|
||||
include_image_prompt = st.checkbox("Include Image Suggestions", value=True)
|
||||
include_timing_suggestion = st.checkbox("Include Timing Suggestion", value=True)
|
||||
|
||||
# Advanced options
|
||||
st.subheader("Advanced Options")
|
||||
language = st.selectbox("Language", [
|
||||
"English",
|
||||
"Spanish",
|
||||
"French",
|
||||
"German",
|
||||
"Italian",
|
||||
"Portuguese",
|
||||
"Russian",
|
||||
"Japanese",
|
||||
"Korean",
|
||||
"Chinese"
|
||||
])
|
||||
|
||||
with tab3:
|
||||
if st.session_state.generated_post:
|
||||
# Display the generated post
|
||||
st.subheader("Generated Community Post")
|
||||
|
||||
# Create tabs for different views
|
||||
post_tab1, post_tab2, post_tab3 = st.tabs(["Preview", "Analytics", "History"])
|
||||
|
||||
with post_tab1:
|
||||
st.markdown(st.session_state.generated_post)
|
||||
|
||||
# Quick actions
|
||||
col1, col2 = st.columns(2)
|
||||
with col1:
|
||||
if st.button("Copy to Clipboard"):
|
||||
st.code(st.session_state.generated_post)
|
||||
st.success("Post copied to clipboard!")
|
||||
|
||||
with col2:
|
||||
if st.button("Save to History"):
|
||||
st.session_state.post_history.append({
|
||||
'post': st.session_state.generated_post,
|
||||
'timestamp': datetime.now(),
|
||||
'type': post_type
|
||||
})
|
||||
st.success("Post saved to history!")
|
||||
|
||||
with post_tab2:
|
||||
# Analyze the post
|
||||
analysis = analyze_post_engagement(st.session_state.generated_post)
|
||||
|
||||
# Create expandable sections for different analysis categories
|
||||
with st.expander("📊 Overall Performance Metrics", expanded=True):
|
||||
cols = st.columns(3)
|
||||
|
||||
with cols[0]:
|
||||
score = analysis['engagement_score']
|
||||
color = "red" if score < 60 else "orange" if score < 80 else "green"
|
||||
st.markdown(f"### Overall Score: <span style='color: {color}'>{score:.1f}%</span>",
|
||||
unsafe_allow_html=True)
|
||||
|
||||
# Sentiment Analysis
|
||||
st.markdown("#### Sentiment Analysis")
|
||||
st.metric("Polarity", f"{analysis['sentiment_analysis']['polarity']}%")
|
||||
st.metric("Subjectivity", f"{analysis['sentiment_analysis']['subjectivity']}%")
|
||||
st.info(f"Tone: {analysis['sentiment_analysis']['tone']}")
|
||||
|
||||
with cols[1]:
|
||||
st.markdown("#### Engagement Metrics")
|
||||
st.metric("Emotional Impact", f"{analysis['emotional_triggers']}%")
|
||||
st.metric("CTA Strength", f"{analysis['call_to_action_strength']}%")
|
||||
st.metric("Virality Potential", f"{analysis['virality_potential']:.1f}%")
|
||||
|
||||
with cols[2]:
|
||||
st.markdown("#### Content Quality")
|
||||
st.metric("Audience Resonance", f"{analysis['audience_resonance']:.1f}%")
|
||||
st.metric("SEO Score", f"{analysis['seo_optimization']:.1f}%")
|
||||
if analysis['timing_recommendation']:
|
||||
st.success(f"📅 {analysis['timing_recommendation']}")
|
||||
|
||||
with st.expander("🎯 Psychological Triggers & Patterns"):
|
||||
col1, col2 = st.columns(2)
|
||||
|
||||
with col1:
|
||||
st.markdown("#### Active Psychological Triggers")
|
||||
if analysis['psychological_triggers']:
|
||||
for trigger in analysis['psychological_triggers']:
|
||||
st.markdown(f"✓ {trigger.title()}")
|
||||
else:
|
||||
st.info("No strong psychological triggers detected")
|
||||
|
||||
with col2:
|
||||
st.markdown("#### Engagement Patterns")
|
||||
patterns = analysis['engagement_patterns']
|
||||
for pattern, score in patterns.items():
|
||||
st.metric(pattern.replace('_', ' ').title(), f"{score}%")
|
||||
|
||||
with st.expander("📝 Content Structure Analysis"):
|
||||
col1, col2 = st.columns(2)
|
||||
|
||||
with col1:
|
||||
structure = analysis['content_structure']
|
||||
st.markdown("#### Structure Metrics")
|
||||
for metric, score in structure.items():
|
||||
st.metric(
|
||||
metric.replace('_', ' ').title(),
|
||||
f"{score:.1f}%"
|
||||
)
|
||||
|
||||
with col2:
|
||||
st.markdown("#### SEO Analysis")
|
||||
st.metric("Hashtag Quality", f"{analyze_hashtag_quality(st.session_state.generated_post)}%")
|
||||
st.metric("Keyword Density", f"{analyze_keyword_density(st.session_state.generated_post)}%")
|
||||
st.metric("Mention Optimization", f"{analyze_mentions(st.session_state.generated_post)}%")
|
||||
|
||||
# Show improvement suggestions
|
||||
if analysis['improvement_suggestions']:
|
||||
with st.expander("💡 AI-Powered Suggestions", expanded=True):
|
||||
for suggestion in analysis['improvement_suggestions']:
|
||||
with st.container():
|
||||
st.markdown(f"#### {suggestion['category']}")
|
||||
st.info(suggestion['suggestion'])
|
||||
if suggestion['examples']:
|
||||
st.markdown("**Examples:**")
|
||||
for example in suggestion['examples']:
|
||||
st.markdown(f"- {example}")
|
||||
|
||||
# Add a refresh button for analysis
|
||||
if st.button("🔄 Refresh Analysis"):
|
||||
st.rerun()
|
||||
|
||||
with post_tab3:
|
||||
if st.session_state.post_history:
|
||||
st.subheader("Previous Posts")
|
||||
for i, post in enumerate(reversed(st.session_state.post_history)):
|
||||
with st.expander(f"Post {len(st.session_state.post_history)-i}: "
|
||||
f"{post['type']} - "
|
||||
f"{post['timestamp'].strftime('%Y-%m-%d %H:%M')}"):
|
||||
st.write(post['post'])
|
||||
else:
|
||||
st.info("No post history yet. Save posts to see them here!")
|
||||
|
||||
# Generate button
|
||||
if st.button("Generate Community Post"):
|
||||
if not main_topic:
|
||||
st.error("Please enter a main topic/message.")
|
||||
return
|
||||
|
||||
with st.spinner("Generating community post..."):
|
||||
post = generate_community_post(
|
||||
post_type, main_topic, target_audience, tone_style,
|
||||
content_purpose, channel_niche, include_emoji,
|
||||
include_hashtags, include_poll, include_image_prompt,
|
||||
include_timing_suggestion, max_length, language
|
||||
)
|
||||
|
||||
if post:
|
||||
st.session_state.generated_post = post
|
||||
st.success("✨ Post generated successfully! Check the 'Preview & Analytics' tab to view, analyze, and save your post.")
|
||||
st.rerun()
|
||||
else:
|
||||
st.error("Failed to generate post. Please try again.")
|
||||
@@ -0,0 +1,404 @@
|
||||
"""
|
||||
YouTube Description Generator Module
|
||||
|
||||
This module provides functionality for generating YouTube video descriptions.
|
||||
"""
|
||||
|
||||
import streamlit as st
|
||||
import time
|
||||
from lib.gpt_providers.text_generation.main_text_generation import llm_text_gen
|
||||
|
||||
|
||||
def calculate_keyword_density(text, keywords):
|
||||
"""Calculate the density of keywords in the text."""
|
||||
if not text or not keywords:
|
||||
return 0
|
||||
|
||||
text = text.lower()
|
||||
keywords = [k.lower() for k in keywords]
|
||||
|
||||
total_words = len(text.split())
|
||||
keyword_count = sum(text.count(k) for k in keywords)
|
||||
|
||||
return (keyword_count / total_words) * 100 if total_words > 0 else 0
|
||||
|
||||
|
||||
def calculate_seo_score(text, keywords):
|
||||
"""Calculate the SEO score of the description."""
|
||||
score = 0
|
||||
|
||||
# Text length (optimal: 250-300 words)
|
||||
word_count = len(text.split())
|
||||
if 250 <= word_count <= 300:
|
||||
score += 3
|
||||
elif 200 <= word_count <= 350:
|
||||
score += 2
|
||||
elif 150 <= word_count <= 400:
|
||||
score += 1
|
||||
|
||||
# Keyword presence
|
||||
text_lower = text.lower()
|
||||
keywords_lower = [k.lower() for k in keywords]
|
||||
keyword_count = sum(text_lower.count(k) for k in keywords_lower)
|
||||
if keyword_count >= 3:
|
||||
score += 3
|
||||
elif keyword_count >= 2:
|
||||
score += 2
|
||||
elif keyword_count >= 1:
|
||||
score += 1
|
||||
|
||||
# Call to action phrases
|
||||
cta_phrases = ["subscribe", "like", "comment", "share", "follow", "check out", "visit", "learn more"]
|
||||
cta_count = sum(text_lower.count(phrase) for phrase in cta_phrases)
|
||||
if cta_count >= 2:
|
||||
score += 2
|
||||
elif cta_count >= 1:
|
||||
score += 1
|
||||
|
||||
# Hashtags
|
||||
hashtag_count = text.count("#")
|
||||
if 3 <= hashtag_count <= 5:
|
||||
score += 2
|
||||
elif 1 <= hashtag_count <= 8:
|
||||
score += 1
|
||||
|
||||
# Links
|
||||
link_count = text.count("http")
|
||||
if 1 <= link_count <= 3:
|
||||
score += 2
|
||||
elif link_count > 3:
|
||||
score += 1
|
||||
|
||||
return min(score, 10) # Cap at 10
|
||||
|
||||
|
||||
def generate_youtube_description(target_audience, main_points, tone_style, use_case, primary_keywords,
|
||||
secondary_keywords, language, seo_goals, include_timestamps=False,
|
||||
include_hashtags=False, include_social_handles=False):
|
||||
"""Generate a YouTube description based on the provided parameters."""
|
||||
|
||||
# Create a custom system prompt for YouTube description generation
|
||||
system_prompt = """You are a YouTube description expert specializing in creating engaging, SEO-optimized video descriptions.
|
||||
Your task is to generate YouTube video descriptions based on the provided information.
|
||||
Focus ONLY on creating descriptions that are optimized for YouTube, with proper formatting, keywords, and calls to action.
|
||||
Return ONLY the description text, without any additional commentary or explanations."""
|
||||
|
||||
# Build the prompt
|
||||
prompt = f"""
|
||||
**Instructions:**
|
||||
|
||||
Please generate a YouTube description for a video about **{main_points}** based on the following information:
|
||||
|
||||
**Target Audience:** {target_audience}
|
||||
**Tone and Style:** {tone_style}
|
||||
**Use Case:** {use_case}
|
||||
**Language:** {language}
|
||||
**Primary Keywords:** {primary_keywords}
|
||||
**Secondary Keywords:** {secondary_keywords}
|
||||
**SEO Goals:** {seo_goals}
|
||||
|
||||
**Additional Elements:**
|
||||
{"- Include timestamps for key sections." if include_timestamps else ""}
|
||||
{"- Include relevant hashtags." if include_hashtags else ""}
|
||||
{"- Include social media handles." if include_social_handles else ""}
|
||||
|
||||
**Specific Instructions:**
|
||||
* Keep the description informative and engaging.
|
||||
* Use a conversational tone that matches the target audience.
|
||||
* Include relevant keywords naturally.
|
||||
* Add a call to action.
|
||||
* Keep the length between 250-300 words for optimal SEO.
|
||||
"""
|
||||
|
||||
try:
|
||||
response = llm_text_gen(prompt, system_prompt=system_prompt)
|
||||
return response
|
||||
except Exception as err:
|
||||
st.error(f"Error: Failed to get response from LLM: {err}")
|
||||
return None
|
||||
|
||||
|
||||
def write_yt_description():
|
||||
"""Create a user interface for YouTube Description Generator."""
|
||||
st.write("Generate SEO-optimized YouTube video descriptions that drive engagement.")
|
||||
|
||||
# Initialize session state for generated description if it doesn't exist
|
||||
if "generated_description" not in st.session_state:
|
||||
st.session_state.generated_description = None
|
||||
|
||||
# Create tabs for different sections
|
||||
tab1, tab2, tab3 = st.tabs(["Basic Info", "SEO Optimization", "Advanced Options"])
|
||||
|
||||
with tab1:
|
||||
# Basic information inputs
|
||||
main_points = st.text_area("Main Points/Keywords (comma-separated)",
|
||||
placeholder="e.g., cooking tips, healthy recipes, quick meals")
|
||||
|
||||
# Create columns for the other inputs
|
||||
col1, col2, col3, col4 = st.columns(4)
|
||||
|
||||
with col1:
|
||||
tone_style = st.selectbox("Tone/Style",
|
||||
["Professional", "Casual", "Humorous", "Educational", "Entertaining", "Inspirational"])
|
||||
|
||||
with col2:
|
||||
target_audience = st.text_input("Target Audience",
|
||||
placeholder="e.g., beginners, professionals, parents")
|
||||
|
||||
with col3:
|
||||
use_case = st.selectbox("Use Case",
|
||||
["How-to/Tutorial", "Vlog", "Review", "Educational", "Entertainment", "News"])
|
||||
|
||||
with col4:
|
||||
language = st.selectbox("Language", ["English", "Spanish", "French", "German", "Italian", "Portuguese"])
|
||||
|
||||
with tab2:
|
||||
# SEO optimization inputs
|
||||
primary_keywords = st.text_input("Primary Keywords (comma-separated)",
|
||||
placeholder="e.g., cooking, recipes, healthy food")
|
||||
secondary_keywords = st.text_input("Secondary Keywords (comma-separated)",
|
||||
placeholder="e.g., quick meals, budget cooking")
|
||||
seo_goals = st.multiselect("SEO Goals",
|
||||
["Increase Views", "Drive Engagement", "Build Subscribers", "Promote Products/Services"])
|
||||
|
||||
with tab3:
|
||||
# Advanced options
|
||||
st.subheader("Additional Elements")
|
||||
include_timestamps = st.checkbox("Include Timestamps", value=True)
|
||||
include_hashtags = st.checkbox("Include Hashtags", value=True)
|
||||
include_social_handles = st.checkbox("Include Social Media Handles", value=True)
|
||||
|
||||
if st.button("Generate Description"):
|
||||
if not main_points:
|
||||
st.error("Please enter main points/keywords.")
|
||||
return
|
||||
|
||||
with st.spinner("Generating description..."):
|
||||
description = generate_youtube_description(
|
||||
target_audience, main_points, tone_style, use_case, primary_keywords,
|
||||
secondary_keywords, language, seo_goals, include_timestamps,
|
||||
include_hashtags, include_social_handles
|
||||
)
|
||||
|
||||
if description:
|
||||
# Store the description in session state
|
||||
st.session_state.generated_description = description
|
||||
|
||||
# Store other parameters in session state for regeneration
|
||||
st.session_state.description_params = {
|
||||
"target_audience": target_audience,
|
||||
"main_points": main_points,
|
||||
"tone_style": tone_style,
|
||||
"use_case": use_case,
|
||||
"primary_keywords": primary_keywords,
|
||||
"secondary_keywords": secondary_keywords,
|
||||
"language": language,
|
||||
"seo_goals": seo_goals,
|
||||
"include_timestamps": include_timestamps,
|
||||
"include_hashtags": include_hashtags,
|
||||
"include_social_handles": include_social_handles
|
||||
}
|
||||
|
||||
st.subheader("Generated Description")
|
||||
|
||||
# Display description with analysis
|
||||
st.text_area("Description", description, height=200)
|
||||
|
||||
# Calculate and display metrics
|
||||
all_keywords = primary_keywords.split(",") + secondary_keywords.split(",")
|
||||
keyword_density = calculate_keyword_density(description, all_keywords)
|
||||
seo_score = calculate_seo_score(description, all_keywords)
|
||||
|
||||
col1, col2 = st.columns(2)
|
||||
with col1:
|
||||
st.metric("Keyword Density", f"{keyword_density:.1f}%")
|
||||
with col2:
|
||||
st.metric("SEO Score", f"{seo_score}/10")
|
||||
|
||||
# Create columns for the buttons
|
||||
btn_col1, btn_col2 = st.columns(2)
|
||||
|
||||
with btn_col1:
|
||||
# Download button
|
||||
st.download_button(
|
||||
label="Download Description",
|
||||
data=description,
|
||||
file_name="youtube_description.txt",
|
||||
mime="text/plain"
|
||||
)
|
||||
|
||||
with btn_col2:
|
||||
# Regenerate button
|
||||
if st.button("Regenerate"):
|
||||
st.session_state.show_regenerate_popover = True
|
||||
|
||||
# Regenerate popover
|
||||
if st.session_state.get("show_regenerate_popover", False):
|
||||
with st.form("regenerate_form"):
|
||||
st.subheader("Regenerate Description")
|
||||
st.write("Specify changes you'd like to make to the description:")
|
||||
changes = st.text_area("Changes to make",
|
||||
placeholder="e.g., Make it more casual, add more call-to-actions, focus on product benefits")
|
||||
|
||||
submitted = st.form_submit_button("Regenerate with Changes")
|
||||
|
||||
if submitted and changes:
|
||||
with st.spinner("Regenerating description..."):
|
||||
# Get the stored parameters
|
||||
params = st.session_state.description_params
|
||||
|
||||
# Add the changes to the prompt
|
||||
params["changes"] = changes
|
||||
|
||||
# Generate a new description with the changes
|
||||
new_description = generate_youtube_description_with_changes(
|
||||
params["target_audience"],
|
||||
params["main_points"],
|
||||
params["tone_style"],
|
||||
params["use_case"],
|
||||
params["primary_keywords"],
|
||||
params["secondary_keywords"],
|
||||
params["language"],
|
||||
params["seo_goals"],
|
||||
params["include_timestamps"],
|
||||
params["include_hashtags"],
|
||||
params["include_social_handles"],
|
||||
changes
|
||||
)
|
||||
|
||||
if new_description:
|
||||
# Update the stored description
|
||||
st.session_state.generated_description = new_description
|
||||
st.session_state.show_regenerate_popover = False
|
||||
st.rerun()
|
||||
else:
|
||||
st.error("Failed to regenerate description. Please try again.")
|
||||
else:
|
||||
st.error("Failed to generate description. Please try again.")
|
||||
|
||||
# Display previously generated description if it exists in session state
|
||||
elif st.session_state.generated_description:
|
||||
description = st.session_state.generated_description
|
||||
params = st.session_state.description_params
|
||||
|
||||
st.subheader("Generated Description")
|
||||
|
||||
# Display description with analysis
|
||||
st.text_area("Description", description, height=200)
|
||||
|
||||
# Calculate and display metrics
|
||||
all_keywords = params["primary_keywords"].split(",") + params["secondary_keywords"].split(",")
|
||||
keyword_density = calculate_keyword_density(description, all_keywords)
|
||||
seo_score = calculate_seo_score(description, all_keywords)
|
||||
|
||||
col1, col2 = st.columns(2)
|
||||
with col1:
|
||||
st.metric("Keyword Density", f"{keyword_density:.1f}%")
|
||||
with col2:
|
||||
st.metric("SEO Score", f"{seo_score}/10")
|
||||
|
||||
# Create columns for the buttons
|
||||
btn_col1, btn_col2 = st.columns(2)
|
||||
|
||||
with btn_col1:
|
||||
# Download button
|
||||
st.download_button(
|
||||
label="Download Description",
|
||||
data=description,
|
||||
file_name="youtube_description.txt",
|
||||
mime="text/plain"
|
||||
)
|
||||
|
||||
with btn_col2:
|
||||
# Regenerate button
|
||||
if st.button("Regenerate"):
|
||||
st.session_state.show_regenerate_popover = True
|
||||
|
||||
# Regenerate popover
|
||||
if st.session_state.get("show_regenerate_popover", False):
|
||||
with st.form("regenerate_form"):
|
||||
st.subheader("Regenerate Description")
|
||||
st.write("Specify changes you'd like to make to the description:")
|
||||
changes = st.text_area("Changes to make",
|
||||
placeholder="e.g., Make it more casual, add more call-to-actions, focus on product benefits")
|
||||
|
||||
submitted = st.form_submit_button("Regenerate with Changes")
|
||||
|
||||
if submitted and changes:
|
||||
with st.spinner("Regenerating description..."):
|
||||
# Add the changes to the prompt
|
||||
params["changes"] = changes
|
||||
|
||||
# Generate a new description with the changes
|
||||
new_description = generate_youtube_description_with_changes(
|
||||
params["target_audience"],
|
||||
params["main_points"],
|
||||
params["tone_style"],
|
||||
params["use_case"],
|
||||
params["primary_keywords"],
|
||||
params["secondary_keywords"],
|
||||
params["language"],
|
||||
params["seo_goals"],
|
||||
params["include_timestamps"],
|
||||
params["include_hashtags"],
|
||||
params["include_social_handles"],
|
||||
changes
|
||||
)
|
||||
|
||||
if new_description:
|
||||
# Update the stored description
|
||||
st.session_state.generated_description = new_description
|
||||
st.session_state.show_regenerate_popover = False
|
||||
st.rerun()
|
||||
else:
|
||||
st.error("Failed to regenerate description. Please try again.")
|
||||
|
||||
|
||||
def generate_youtube_description_with_changes(target_audience, main_points, tone_style, use_case, primary_keywords,
|
||||
secondary_keywords, language, seo_goals, include_timestamps=False,
|
||||
include_hashtags=False, include_social_handles=False, changes=""):
|
||||
"""Generate a YouTube description based on the provided parameters and requested changes."""
|
||||
|
||||
# Create a custom system prompt for YouTube description generation
|
||||
system_prompt = """You are a YouTube description expert specializing in creating engaging, SEO-optimized video descriptions.
|
||||
Your task is to generate YouTube video descriptions based on the provided information.
|
||||
Focus ONLY on creating descriptions that are optimized for YouTube, with proper formatting, keywords, and calls to action.
|
||||
Return ONLY the description text, without any additional commentary or explanations."""
|
||||
|
||||
# Build the prompt
|
||||
prompt = f"""
|
||||
**Instructions:**
|
||||
|
||||
Please generate a YouTube description for a video about **{main_points}** based on the following information:
|
||||
|
||||
**Target Audience:** {target_audience}
|
||||
**Tone and Style:** {tone_style}
|
||||
**Use Case:** {use_case}
|
||||
**Language:** {language}
|
||||
**Primary Keywords:** {primary_keywords}
|
||||
**Secondary Keywords:** {secondary_keywords}
|
||||
**SEO Goals:** {seo_goals}
|
||||
|
||||
**Additional Elements:**
|
||||
{"- Include timestamps for key sections." if include_timestamps else ""}
|
||||
{"- Include relevant hashtags." if include_hashtags else ""}
|
||||
{"- Include social media handles." if include_social_handles else ""}
|
||||
|
||||
**Requested Changes:**
|
||||
{changes}
|
||||
|
||||
**Specific Instructions:**
|
||||
* Keep the description informative and engaging.
|
||||
* Use a conversational tone that matches the target audience.
|
||||
* Include relevant keywords naturally.
|
||||
* Add a call to action.
|
||||
* Keep the length between 250-300 words for optimal SEO.
|
||||
* Incorporate the requested changes into the description.
|
||||
"""
|
||||
|
||||
try:
|
||||
response = llm_text_gen(prompt, system_prompt=system_prompt)
|
||||
return response
|
||||
except Exception as err:
|
||||
st.error(f"Error: Failed to get response from LLM: {err}")
|
||||
return None
|
||||
@@ -0,0 +1,740 @@
|
||||
"""
|
||||
YouTube End Screen Generator Module
|
||||
|
||||
This module provides functionality for generating YouTube video end screens.
|
||||
"""
|
||||
|
||||
import streamlit as st
|
||||
import time
|
||||
import logging
|
||||
import traceback
|
||||
from PIL import Image
|
||||
from lib.gpt_providers.text_generation.main_text_generation import llm_text_gen
|
||||
from lib.gpt_providers.text_to_image_generation.gen_gemini_images import generate_gemini_image, edit_image
|
||||
|
||||
# Configure logging
|
||||
logging.basicConfig(
|
||||
level=logging.INFO,
|
||||
format='%(asctime)s - %(name)s - %(levelname)s - %(message)s'
|
||||
)
|
||||
logger = logging.getLogger('youtube_end_screen_generator')
|
||||
|
||||
|
||||
def generate_end_screen_concepts(video_title, video_description, target_audience, content_type,
|
||||
primary_goal, secondary_goal=None, num_concepts=3):
|
||||
"""Generate end screen concept ideas based on video content."""
|
||||
logger.info(f"Generating end screen concepts for: '{video_title}'")
|
||||
logger.info(f"Parameters: target_audience={target_audience}, content_type={content_type}, "
|
||||
f"primary_goal={primary_goal}, secondary_goal={secondary_goal}, num_concepts={num_concepts}")
|
||||
|
||||
# Create a system prompt for end screen concept generation
|
||||
system_prompt = """You are a YouTube end screen expert specializing in creating engaging, action-driving end screen concepts.
|
||||
Your task is to generate end screen concept ideas based on the provided video information.
|
||||
Focus ONLY on creating end screens that are optimized for YouTube, with proper visual hierarchy, element placement, and call-to-action triggers.
|
||||
Return ONLY the concept descriptions, without any additional commentary or explanations.
|
||||
Each concept should include:
|
||||
1. A main visual element or background
|
||||
2. Element placement and content (subscribe button, playlist, video, website)
|
||||
3. Color scheme suggestions
|
||||
4. Text content for each element
|
||||
5. Brief explanation of why this concept would be effective for the specified goals
|
||||
|
||||
IMPORTANT: Format each concept with a clear numbered heading like "1. [Concept Name]" to ensure proper parsing."""
|
||||
|
||||
# Build the prompt
|
||||
prompt = f"""
|
||||
**Instructions:**
|
||||
|
||||
Please generate {num_concepts} end screen concept ideas for a YouTube video with the following information:
|
||||
|
||||
**Video Title:** {video_title}
|
||||
**Video Description:** {video_description}
|
||||
**Target Audience:** {target_audience}
|
||||
**Content Type:** {content_type}
|
||||
**Primary Goal:** {primary_goal}
|
||||
**Secondary Goal:** {secondary_goal if secondary_goal else "None specified"}
|
||||
|
||||
**Specific Instructions:**
|
||||
* Each concept should be clearly separated and numbered with a heading like "1. [Concept Name]".
|
||||
* Focus on creating end screens that drive the specified goals.
|
||||
* Consider the target audience's interests and preferences.
|
||||
* Include specific details about visual elements, element placement, and color schemes.
|
||||
* Explain why each concept would be effective for this specific video and goals.
|
||||
* Include text suggestions for each element (subscribe button, playlist, video, website).
|
||||
"""
|
||||
|
||||
try:
|
||||
logger.info("Sending request to LLM for end screen concepts")
|
||||
response = llm_text_gen(prompt, system_prompt=system_prompt)
|
||||
logger.info(f"Received response from LLM: {len(response)} characters")
|
||||
return response
|
||||
except Exception as err:
|
||||
logger.error(f"Error generating end screen concepts: {err}")
|
||||
logger.error(traceback.format_exc())
|
||||
st.error(f"Error: Failed to generate end screen concepts: {err}")
|
||||
return None
|
||||
|
||||
|
||||
def generate_end_screen_design(concept_description, style_preference, element_count=2,
|
||||
element_types=None, element_texts=None, aspect_ratio="16:9",
|
||||
keywords=None, style=None, focus=None):
|
||||
"""Generate an end screen image based on the concept description."""
|
||||
logger.info(f"Generating end screen design for concept: '{concept_description[:50]}...'")
|
||||
logger.info(f"Parameters: style_preference={style_preference}, element_count={element_count}, "
|
||||
f"element_types={element_types}, element_texts={element_texts}, aspect_ratio={aspect_ratio}")
|
||||
|
||||
# Extract key elements from the concept description
|
||||
# This helps focus the prompt on the most important aspects
|
||||
concept_lines = concept_description.split('\n')
|
||||
main_visual = ""
|
||||
element_placement = ""
|
||||
color_scheme = ""
|
||||
text_content = ""
|
||||
|
||||
for line in concept_lines:
|
||||
if "visual" in line.lower() or "background" in line.lower():
|
||||
main_visual = line
|
||||
elif "placement" in line.lower() or "layout" in line.lower():
|
||||
element_placement = line
|
||||
elif "color" in line.lower() or "scheme" in line.lower():
|
||||
color_scheme = line
|
||||
elif "text" in line.lower() or "content" in line.lower():
|
||||
text_content = line
|
||||
|
||||
# Create a more focused prompt for the image generation
|
||||
image_prompt = f"""
|
||||
Create a YouTube end screen image with the following specifications:
|
||||
|
||||
MAIN VISUAL: {main_visual if main_visual else "Not specified"}
|
||||
ELEMENT PLACEMENT: {element_placement if element_placement else "Not specified"}
|
||||
COLOR SCHEME: {color_scheme if color_scheme else "Not specified"}
|
||||
TEXT CONTENT: {text_content if text_content else "Not specified"}
|
||||
|
||||
STYLE: {style_preference}
|
||||
ASPECT RATIO: {aspect_ratio}
|
||||
NUMBER OF ELEMENTS: {element_count}
|
||||
|
||||
ELEMENT TYPES: {', '.join(element_types) if element_types else 'Not specified'}
|
||||
ELEMENT TEXTS: {', '.join(element_texts) if element_texts else 'Not specified'}
|
||||
|
||||
IMPORTANT REQUIREMENTS:
|
||||
1. This must be a VISUAL IMAGE of a YouTube end screen, not just a text description
|
||||
2. The image should be high contrast and visually striking
|
||||
3. All text should be large and readable
|
||||
4. Elements should be properly placed for optimal viewer engagement
|
||||
5. The design should follow the specified color scheme
|
||||
6. The image should be optimized for the specified aspect ratio
|
||||
|
||||
PLEASE GENERATE AN ACTUAL IMAGE, NOT JUST A TEXT DESCRIPTION.
|
||||
"""
|
||||
|
||||
try:
|
||||
logger.info("Sending request to Gemini for end screen image")
|
||||
# Generate the image using Gemini with enhanced prompt
|
||||
img_path = generate_gemini_image(
|
||||
image_prompt,
|
||||
keywords=keywords,
|
||||
style=style,
|
||||
focus=focus,
|
||||
enhance_prompt=True
|
||||
)
|
||||
logger.info(f"Received image from Gemini: {img_path}")
|
||||
return img_path
|
||||
except Exception as err:
|
||||
logger.error(f"Error generating end screen image: {err}")
|
||||
logger.error(traceback.format_exc())
|
||||
st.error(f"Error: Failed to generate end screen image: {err}")
|
||||
return None
|
||||
|
||||
|
||||
def edit_end_screen_image(img_path, edit_instructions):
|
||||
"""Edit an end screen image based on user instructions."""
|
||||
logger.info(f"Editing end screen image: '{img_path}'")
|
||||
logger.info(f"Edit instructions: '{edit_instructions}'")
|
||||
|
||||
try:
|
||||
logger.info("Sending request to Gemini for image editing")
|
||||
# Edit the image using Gemini
|
||||
edited_img_path = edit_image(img_path, f"Edit this image according to these instructions: {edit_instructions}. IMPORTANT: Please generate an actual edited image, not just a text description. I need a visual representation of the edited end screen.")
|
||||
logger.info(f"Image editing completed. Edited image path: {edited_img_path}")
|
||||
|
||||
# Return the path to the edited image
|
||||
return edited_img_path
|
||||
except Exception as err:
|
||||
logger.error(f"Error editing end screen image: {err}")
|
||||
logger.error(traceback.format_exc())
|
||||
st.error(f"Error: Failed to edit end screen image: {err}")
|
||||
return None
|
||||
|
||||
|
||||
def analyze_end_screen(end_screen_path):
|
||||
"""Analyze an end screen for effectiveness."""
|
||||
logger.info(f"Analyzing end screen: '{end_screen_path}'")
|
||||
|
||||
# This would typically involve image analysis, but for now we'll use AI to provide feedback
|
||||
system_prompt = """You are a YouTube end screen expert specializing in analyzing and providing feedback on end screen designs.
|
||||
Your task is to analyze the end screen and provide constructive feedback on its effectiveness.
|
||||
Focus on aspects like visual hierarchy, element placement, call-to-action clarity, and overall effectiveness."""
|
||||
|
||||
# For now, we'll just return a placeholder analysis
|
||||
# In a real implementation, we would analyze the actual image
|
||||
logger.info("Generating end screen analysis")
|
||||
return """
|
||||
**End Screen Analysis:**
|
||||
|
||||
- **Visual Hierarchy:** The main elements are well-positioned and stand out against the background.
|
||||
- **Element Placement:** The call-to-action elements are strategically placed for optimal viewer engagement.
|
||||
- **Call-to-Action Clarity:** The text and visual cues clearly communicate the desired actions.
|
||||
- **Overall Effectiveness:** The design is likely to drive the specified goals due to its visual appeal and clear value proposition.
|
||||
|
||||
**Suggestions for Improvement:**
|
||||
- Consider adding a subtle animation hint to draw attention to the most important element.
|
||||
- The text could be slightly larger for better readability on mobile devices.
|
||||
- Adding a small icon or logo could help with brand recognition.
|
||||
"""
|
||||
|
||||
|
||||
def parse_concepts(concepts_text):
|
||||
"""Parse the concepts text into a list of individual concepts."""
|
||||
logger.info("Parsing concepts text into individual concepts")
|
||||
|
||||
# Split the concepts text by main concept headers
|
||||
concepts = []
|
||||
current_concept = ""
|
||||
|
||||
# Look for patterns like numbered headings (e.g., "1.", "2.", "3.") or "Concept 1:", "Concept 2:", etc.
|
||||
concept_patterns = ["1.", "2.", "3.", "4.", "5.", "Concept 1:", "Concept 2:", "Concept 3:", "Concept 4:", "Concept 5:"]
|
||||
|
||||
for line in concepts_text.split('\n'):
|
||||
# Check if line starts with a concept pattern
|
||||
is_new_concept = False
|
||||
for pattern in concept_patterns:
|
||||
if line.strip().startswith(pattern):
|
||||
# If we have a previous concept, add it to the list
|
||||
if current_concept:
|
||||
concepts.append(current_concept.strip())
|
||||
# Start a new concept
|
||||
current_concept = line
|
||||
is_new_concept = True
|
||||
break
|
||||
|
||||
if not is_new_concept:
|
||||
# Add the line to the current concept
|
||||
current_concept += "\n" + line
|
||||
|
||||
# Add the last concept
|
||||
if current_concept:
|
||||
concepts.append(current_concept.strip())
|
||||
|
||||
logger.info(f"Parsed {len(concepts)} concepts from the response")
|
||||
return concepts
|
||||
|
||||
|
||||
def write_yt_end_screen():
|
||||
"""Create a user interface for YouTube End Screen Generator."""
|
||||
logger.info("Initializing YouTube End Screen Generator UI")
|
||||
st.title("YouTube End Screen Generator")
|
||||
st.write("Create engaging, action-driving end screens for your YouTube videos.")
|
||||
|
||||
# Initialize session state for generated end screens if it doesn't exist
|
||||
if "generated_end_screens" not in st.session_state:
|
||||
st.session_state.generated_end_screens = []
|
||||
if "end_screen_concepts" not in st.session_state:
|
||||
st.session_state.end_screen_concepts = None
|
||||
if "current_end_screen_path" not in st.session_state:
|
||||
st.session_state.current_end_screen_path = None
|
||||
if "concept_list" not in st.session_state:
|
||||
st.session_state.concept_list = []
|
||||
if "editing_end_screen" not in st.session_state:
|
||||
st.session_state.editing_end_screen = False
|
||||
if "edit_instructions" not in st.session_state:
|
||||
st.session_state.edit_instructions = ""
|
||||
if "edited_end_screen_path" not in st.session_state:
|
||||
st.session_state.edited_end_screen_path = None
|
||||
if "show_edit_form" not in st.session_state:
|
||||
st.session_state.show_edit_form = False
|
||||
|
||||
# Create tabs for different sections
|
||||
tab1, tab2 = st.tabs(["Basic Info", "Style & Elements"])
|
||||
|
||||
with tab1:
|
||||
# Basic information inputs
|
||||
video_title = st.text_input("Video Title",
|
||||
placeholder="e.g., 10 Tips for Better Photography")
|
||||
video_description = st.text_area("Video Description",
|
||||
placeholder="Brief description of your video content")
|
||||
target_audience = st.text_input("Target Audience",
|
||||
placeholder="e.g., photography enthusiasts, beginners")
|
||||
|
||||
# Content type selection
|
||||
content_type = st.selectbox("Content Type", [
|
||||
"Tutorial/How-to",
|
||||
"Vlog",
|
||||
"Review",
|
||||
"Educational",
|
||||
"Entertainment",
|
||||
"News/Update",
|
||||
"Product Showcase",
|
||||
"Challenge",
|
||||
"Reaction",
|
||||
"Comparison"
|
||||
])
|
||||
|
||||
# End screen goals
|
||||
st.subheader("End Screen Goals")
|
||||
primary_goal = st.selectbox("Primary Goal", [
|
||||
"Drive Subscriptions",
|
||||
"Promote Playlist",
|
||||
"Promote Next Video",
|
||||
"Promote Website",
|
||||
"Promote Social Media",
|
||||
"Promote Product/Service",
|
||||
"Encourage Comments",
|
||||
"Mixed Goals"
|
||||
])
|
||||
|
||||
secondary_goal = st.selectbox("Secondary Goal (Optional)", [
|
||||
"None",
|
||||
"Drive Subscriptions",
|
||||
"Promote Playlist",
|
||||
"Promote Next Video",
|
||||
"Promote Website",
|
||||
"Promote Social Media",
|
||||
"Promote Product/Service",
|
||||
"Encourage Comments"
|
||||
])
|
||||
|
||||
if secondary_goal == "None":
|
||||
secondary_goal = None
|
||||
|
||||
with tab2:
|
||||
# Style preferences
|
||||
st.subheader("Style Preferences")
|
||||
|
||||
# Create columns for style options
|
||||
col1, col2 = st.columns(2)
|
||||
|
||||
with col1:
|
||||
style_preference = st.selectbox("End Screen Style", [
|
||||
"Bold and Dramatic",
|
||||
"Clean and Minimal",
|
||||
"Colorful and Vibrant",
|
||||
"Dark and Moody",
|
||||
"Professional and Corporate",
|
||||
"Playful and Fun",
|
||||
"Retro/Vintage",
|
||||
"Modern and Sleek"
|
||||
])
|
||||
|
||||
num_concepts = st.slider("Number of Concepts", 1, 5, 3)
|
||||
|
||||
with col2:
|
||||
aspect_ratio = st.selectbox("Aspect Ratio", [
|
||||
"16:9 (Standard)",
|
||||
"1:1 (Square)",
|
||||
"4:3 (Classic)",
|
||||
"9:16 (Vertical)"
|
||||
])
|
||||
|
||||
include_branding = st.checkbox("Include Branding Elements", value=True)
|
||||
if include_branding:
|
||||
branding_elements = st.multiselect("Branding Elements", [
|
||||
"Channel Logo",
|
||||
"Channel Name",
|
||||
"Channel Tagline",
|
||||
"Brand Colors",
|
||||
"Watermark"
|
||||
])
|
||||
|
||||
# Element configuration
|
||||
st.subheader("End Screen Elements")
|
||||
|
||||
# Number of elements
|
||||
element_count = st.slider("Number of Elements", 1, 4, 2)
|
||||
|
||||
# Element types
|
||||
element_types = []
|
||||
element_texts = []
|
||||
|
||||
for i in range(element_count):
|
||||
st.write(f"Element {i+1}")
|
||||
col1, col2 = st.columns(2)
|
||||
|
||||
with col1:
|
||||
element_type = st.selectbox(
|
||||
f"Type",
|
||||
["Subscribe Button", "Playlist", "Video", "Website", "Social Media"],
|
||||
key=f"element_type_{i}"
|
||||
)
|
||||
element_types.append(element_type)
|
||||
|
||||
with col2:
|
||||
element_text = st.text_input(
|
||||
f"Text",
|
||||
placeholder=f"Text for {element_type}",
|
||||
key=f"element_text_{i}"
|
||||
)
|
||||
element_texts.append(element_text)
|
||||
|
||||
# Advanced AI Prompt Settings
|
||||
st.subheader("Advanced AI Prompt Settings")
|
||||
|
||||
# Create columns for advanced settings
|
||||
col3, col4 = st.columns(2)
|
||||
|
||||
with col3:
|
||||
# Image style selection
|
||||
image_style = st.selectbox("Image Style", [
|
||||
"Auto (AI will choose best style)",
|
||||
"Photorealistic",
|
||||
"Artistic",
|
||||
"Cartoon/Anime",
|
||||
"Sketch/Drawing",
|
||||
"Digital Art",
|
||||
"3D Render"
|
||||
])
|
||||
|
||||
# Extract style for the generate_gemini_image function
|
||||
style = None
|
||||
if image_style == "Photorealistic":
|
||||
style = "photorealistic"
|
||||
elif image_style == "Artistic":
|
||||
style = "artistic"
|
||||
elif image_style == "Cartoon/Anime":
|
||||
style = "cartoon"
|
||||
elif image_style == "Sketch/Drawing":
|
||||
style = "sketch"
|
||||
elif image_style == "Digital Art":
|
||||
style = "digital_art"
|
||||
elif image_style == "3D Render":
|
||||
style = "3d_render"
|
||||
|
||||
with col4:
|
||||
# Focus selection for photorealistic images
|
||||
focus = None
|
||||
if style == "photorealistic":
|
||||
focus = st.selectbox("Image Focus", [
|
||||
"Auto (AI will choose best focus)",
|
||||
"Portraits",
|
||||
"Objects",
|
||||
"Motion",
|
||||
"Wide-angle"
|
||||
])
|
||||
|
||||
# Extract focus for the generate_gemini_image function
|
||||
if focus == "Portraits":
|
||||
focus = "portraits"
|
||||
elif focus == "Objects":
|
||||
focus = "objects"
|
||||
elif focus == "Motion":
|
||||
focus = "motion"
|
||||
elif focus == "Wide-angle":
|
||||
focus = "wide-angle"
|
||||
elif focus == "Auto (AI will choose best focus)":
|
||||
focus = None
|
||||
|
||||
# Keywords for enhanced prompt generation
|
||||
st.subheader("Keywords for Enhanced Prompt")
|
||||
st.write("Add keywords to enhance the AI prompt generation. These will help create more detailed and accurate end screens.")
|
||||
|
||||
# Create a text area for keywords
|
||||
keywords_input = st.text_area(
|
||||
"Keywords (comma-separated)",
|
||||
placeholder="e.g., vibrant, energetic, bold, eye-catching, professional"
|
||||
)
|
||||
|
||||
# Process keywords
|
||||
keywords = None
|
||||
if keywords_input:
|
||||
keywords = [k.strip() for k in keywords_input.split(",") if k.strip()]
|
||||
logger.info(f"User provided keywords: {keywords}")
|
||||
|
||||
# Generate button - placed outside of tabs for better visibility
|
||||
st.markdown("---")
|
||||
st.subheader("Generate End Screen Concepts")
|
||||
st.write("Click the button below to generate end screen concepts based on your inputs.")
|
||||
|
||||
if st.button("Generate End Screen Concepts", type="primary"):
|
||||
if not video_title:
|
||||
st.error("Please enter a video title.")
|
||||
return
|
||||
|
||||
with st.spinner("Generating end screen concepts..."):
|
||||
logger.info("User clicked Generate End Screen Concepts button")
|
||||
concepts = generate_end_screen_concepts(
|
||||
video_title,
|
||||
video_description,
|
||||
target_audience,
|
||||
content_type,
|
||||
primary_goal,
|
||||
secondary_goal,
|
||||
num_concepts
|
||||
)
|
||||
|
||||
if concepts:
|
||||
# Store the concepts in session state
|
||||
st.session_state.end_screen_concepts = concepts
|
||||
# Parse the concepts and store in session state
|
||||
st.session_state.concept_list = parse_concepts(concepts)
|
||||
logger.info("Stored end screen concepts in session state")
|
||||
|
||||
# Display the concepts in tabs
|
||||
st.subheader("End Screen Concepts")
|
||||
|
||||
# Create tabs for each concept
|
||||
concept_tabs = st.tabs([f"Concept {i+1}" for i in range(len(st.session_state.concept_list))])
|
||||
|
||||
for i, tab in enumerate(concept_tabs):
|
||||
with tab:
|
||||
st.markdown(st.session_state.concept_list[i])
|
||||
|
||||
# Add a button to generate image for this concept
|
||||
if st.button(f"Generate Image for Concept {i+1}", key=f"gen_img_{i}"):
|
||||
with st.spinner(f"Generating end screen image for concept {i+1}..."):
|
||||
logger.info(f"User selected concept {i+1} for image generation")
|
||||
# Get the selected concept
|
||||
selected_concept = st.session_state.concept_list[i]
|
||||
|
||||
# Generate the end screen image with enhanced prompt
|
||||
img_path = generate_end_screen_design(
|
||||
selected_concept,
|
||||
style_preference,
|
||||
element_count,
|
||||
element_types,
|
||||
element_texts,
|
||||
aspect_ratio.split()[0], # Extract just the ratio part
|
||||
keywords=keywords,
|
||||
style=style,
|
||||
focus=focus
|
||||
)
|
||||
|
||||
if img_path:
|
||||
# Store the current end screen path in session state
|
||||
st.session_state.current_end_screen_path = img_path
|
||||
logger.info(f"Stored current end screen path in session state: {img_path}")
|
||||
|
||||
# Display the generated image
|
||||
st.subheader("Generated End Screen")
|
||||
st.image(img_path, use_container_width=True)
|
||||
|
||||
# Add download button
|
||||
with open(img_path, "rb") as file:
|
||||
st.download_button(
|
||||
label="Download End Screen",
|
||||
data=file,
|
||||
file_name=f"youtube_end_screen_{int(time.time())}.png",
|
||||
mime="image/png"
|
||||
)
|
||||
|
||||
# Add image editing section
|
||||
st.subheader("Edit End Screen")
|
||||
st.write("Make changes to your end screen by providing instructions below:")
|
||||
|
||||
# Create a text area for edit instructions
|
||||
edit_instructions = st.text_area(
|
||||
"Edit Instructions",
|
||||
placeholder="e.g., Make the background darker, Add a red border, Change the text color to white",
|
||||
key=f"edit_instructions_{i}"
|
||||
)
|
||||
|
||||
# Store edit instructions in session state
|
||||
st.session_state.edit_instructions = edit_instructions
|
||||
|
||||
# Add a button to apply edits
|
||||
if st.button("Apply Edits", key=f"apply_edits_{i}"):
|
||||
if not edit_instructions:
|
||||
st.warning("Please provide edit instructions.")
|
||||
else:
|
||||
# Set editing flag
|
||||
st.session_state.editing_end_screen = True
|
||||
st.session_state.show_edit_form = True
|
||||
|
||||
# Rerun to update the UI
|
||||
st.rerun()
|
||||
|
||||
# Add analysis button
|
||||
if st.button("Analyze End Screen", key=f"analyze_{i}"):
|
||||
logger.info("User clicked Analyze End Screen button")
|
||||
analysis = analyze_end_screen(img_path)
|
||||
st.subheader("End Screen Analysis")
|
||||
st.markdown(analysis)
|
||||
else:
|
||||
st.error("Failed to generate end screen concepts. Please try again.")
|
||||
|
||||
# Display previously generated concepts if they exist in session state
|
||||
elif st.session_state.end_screen_concepts and st.session_state.concept_list:
|
||||
logger.info("Displaying previously generated concepts from session state")
|
||||
st.subheader("End Screen Concepts")
|
||||
|
||||
# Create tabs for each concept
|
||||
concept_tabs = st.tabs([f"Concept {i+1}" for i in range(len(st.session_state.concept_list))])
|
||||
|
||||
for i, tab in enumerate(concept_tabs):
|
||||
with tab:
|
||||
st.markdown(st.session_state.concept_list[i])
|
||||
|
||||
# Add a button to generate image for this concept
|
||||
if st.button(f"Generate Image for Concept {i+1}", key=f"gen_img_existing_{i}"):
|
||||
with st.spinner(f"Generating end screen image for concept {i+1}..."):
|
||||
logger.info(f"User selected concept {i+1} for image generation")
|
||||
# Get the selected concept
|
||||
selected_concept = st.session_state.concept_list[i]
|
||||
|
||||
# Generate the end screen image with enhanced prompt
|
||||
img_path = generate_end_screen_design(
|
||||
selected_concept,
|
||||
style_preference,
|
||||
element_count,
|
||||
element_types,
|
||||
element_texts,
|
||||
aspect_ratio.split()[0], # Extract just the ratio part
|
||||
keywords=keywords,
|
||||
style=style,
|
||||
focus=focus
|
||||
)
|
||||
|
||||
if img_path:
|
||||
# Store the current end screen path in session state
|
||||
st.session_state.current_end_screen_path = img_path
|
||||
logger.info(f"Stored current end screen path in session state: {img_path}")
|
||||
|
||||
# Display the generated image
|
||||
st.subheader("Generated End Screen")
|
||||
st.image(img_path, use_container_width=True)
|
||||
|
||||
# Add download button
|
||||
with open(img_path, "rb") as file:
|
||||
st.download_button(
|
||||
label="Download End Screen",
|
||||
data=file,
|
||||
file_name=f"youtube_end_screen_{int(time.time())}.png",
|
||||
mime="image/png"
|
||||
)
|
||||
|
||||
# Add image editing section
|
||||
st.subheader("Edit End Screen")
|
||||
st.write("Make changes to your end screen by providing instructions below:")
|
||||
|
||||
# Create a text area for edit instructions
|
||||
edit_instructions = st.text_area(
|
||||
"Edit Instructions",
|
||||
placeholder="e.g., Make the background darker, Add a red border, Change the text color to white",
|
||||
key=f"edit_instructions_existing_{i}"
|
||||
)
|
||||
|
||||
# Store edit instructions in session state
|
||||
st.session_state.edit_instructions = edit_instructions
|
||||
|
||||
# Add a button to apply edits
|
||||
if st.button("Apply Edits", key=f"apply_edits_existing_{i}"):
|
||||
if not edit_instructions:
|
||||
st.warning("Please provide edit instructions.")
|
||||
else:
|
||||
# Set editing flag
|
||||
st.session_state.editing_end_screen = True
|
||||
st.session_state.show_edit_form = True
|
||||
|
||||
# Rerun to update the UI
|
||||
st.rerun()
|
||||
|
||||
# Add analysis button
|
||||
if st.button("Analyze End Screen", key=f"analyze_existing_{i}"):
|
||||
logger.info("User clicked Analyze End Screen button")
|
||||
analysis = analyze_end_screen(img_path)
|
||||
st.subheader("End Screen Analysis")
|
||||
st.markdown(analysis)
|
||||
|
||||
# Display current end screen if it exists in session state
|
||||
elif st.session_state.current_end_screen_path:
|
||||
logger.info(f"Displaying current end screen from session state: {st.session_state.current_end_screen_path}")
|
||||
st.subheader("Current End Screen")
|
||||
st.image(st.session_state.current_end_screen_path, use_container_width=True)
|
||||
|
||||
# Add download button
|
||||
with open(st.session_state.current_end_screen_path, "rb") as file:
|
||||
st.download_button(
|
||||
label="Download End Screen",
|
||||
data=file,
|
||||
file_name=f"youtube_end_screen_{int(time.time())}.png",
|
||||
mime="image/png"
|
||||
)
|
||||
|
||||
# Add image editing section
|
||||
st.subheader("Edit End Screen")
|
||||
st.write("Make changes to your end screen by providing instructions below:")
|
||||
|
||||
# Create a text area for edit instructions
|
||||
edit_instructions = st.text_area(
|
||||
"Edit Instructions",
|
||||
placeholder="e.g., Make the background darker, Add a new element, Change the text color to white",
|
||||
key="edit_instructions_current",
|
||||
value=st.session_state.edit_instructions if st.session_state.edit_instructions else ""
|
||||
)
|
||||
|
||||
# Store edit instructions in session state
|
||||
st.session_state.edit_instructions = edit_instructions
|
||||
|
||||
# Add a button to apply edits
|
||||
if st.button("Apply Edits", key="apply_edits_current"):
|
||||
if not edit_instructions:
|
||||
st.warning("Please provide edit instructions.")
|
||||
else:
|
||||
# Set editing flag
|
||||
st.session_state.editing_end_screen = True
|
||||
st.session_state.show_edit_form = True
|
||||
|
||||
# Rerun to update the UI
|
||||
st.rerun()
|
||||
|
||||
# Add analysis button
|
||||
if st.button("Analyze End Screen", key="analyze_current"):
|
||||
logger.info("User clicked Analyze End Screen button")
|
||||
analysis = analyze_end_screen(st.session_state.current_end_screen_path)
|
||||
st.subheader("End Screen Analysis")
|
||||
st.markdown(analysis)
|
||||
|
||||
# Handle the editing process
|
||||
if st.session_state.editing_end_screen and st.session_state.show_edit_form:
|
||||
st.subheader("Editing End Screen")
|
||||
|
||||
# Show a spinner while editing
|
||||
with st.spinner("Editing end screen..."):
|
||||
logger.info(f"User provided edit instructions: '{st.session_state.edit_instructions}'")
|
||||
# Edit the end screen image
|
||||
edited_img_path = edit_end_screen_image(st.session_state.current_end_screen_path, st.session_state.edit_instructions)
|
||||
|
||||
if edited_img_path:
|
||||
# Update the current end screen path in session state
|
||||
st.session_state.edited_end_screen_path = edited_img_path
|
||||
logger.info(f"Updated current end screen path in session state: {edited_img_path}")
|
||||
|
||||
# Reset editing flags
|
||||
st.session_state.editing_end_screen = False
|
||||
st.session_state.show_edit_form = False
|
||||
|
||||
# Display the edited image
|
||||
st.subheader("Edited End Screen")
|
||||
st.image(edited_img_path, use_container_width=True)
|
||||
|
||||
# Add download button for the edited image
|
||||
with open(edited_img_path, "rb") as file:
|
||||
st.download_button(
|
||||
label="Download Edited End Screen",
|
||||
data=file,
|
||||
file_name=f"youtube_end_screen_edited_{int(time.time())}.png",
|
||||
mime="image/png"
|
||||
)
|
||||
|
||||
# Update the current end screen path to the edited one
|
||||
st.session_state.current_end_screen_path = edited_img_path
|
||||
|
||||
# Add a button to continue editing
|
||||
if st.button("Continue Editing"):
|
||||
st.session_state.show_edit_form = True
|
||||
st.rerun()
|
||||
else:
|
||||
# Reset editing flags
|
||||
st.session_state.editing_end_screen = False
|
||||
st.session_state.show_edit_form = False
|
||||
|
||||
st.error("Failed to edit the end screen. Please try again with different instructions.")
|
||||
@@ -0,0 +1,556 @@
|
||||
"""
|
||||
YouTube Script Generator Module
|
||||
|
||||
This module provides functionality for generating YouTube video scripts.
|
||||
"""
|
||||
|
||||
import streamlit as st
|
||||
import time
|
||||
import json
|
||||
import os
|
||||
from lib.gpt_providers.text_generation.main_text_generation import llm_text_gen
|
||||
|
||||
|
||||
def generate_youtube_script(target_audience, main_points, tone_style, use_case, script_structure,
|
||||
include_hook=False, include_cta=False, include_engagement=False,
|
||||
include_timestamps=False, include_visual_cues=False, engagement_hooks=None,
|
||||
community_interactions=None, language="English"):
|
||||
"""Generate a YouTube script based on the provided parameters."""
|
||||
|
||||
# Create a custom system prompt for YouTube script generation
|
||||
system_prompt = f"""You are a YouTube script expert specializing in creating engaging, well-structured video scripts in {language}.
|
||||
Your task is to generate YouTube video scripts based on the provided information.
|
||||
Focus ONLY on creating scripts that are optimized for YouTube, with proper structure, engagement hooks, and calls to action.
|
||||
Return ONLY the script text, without any additional commentary or explanations.
|
||||
Format the script with clear sections, speaker notes, and visual cues where appropriate.
|
||||
Write the entire script in {language}."""
|
||||
|
||||
# Build structure-specific instructions
|
||||
structure_instructions = {
|
||||
"Problem-Solution": "Structure the script to first present a problem, then provide a solution.",
|
||||
"Before-After-Bridge": "Structure the script to show the before state, the transformation process, and the after state.",
|
||||
"Hook-Problem-Solution-Call to Action": "Start with a hook, present the problem, provide the solution, and end with a call to action.",
|
||||
"Compare and Contrast": "Structure the script to compare and contrast different options or approaches.",
|
||||
"Step-by-Step Tutorial": "Break down the content into clear, sequential steps.",
|
||||
"Case Study": "Present a real-world example or case study to illustrate the main points.",
|
||||
"Interview Format": "Structure the script as an interview with questions and answers.",
|
||||
"Review Format": "Structure the script as a review with pros, cons, and a final verdict.",
|
||||
"Vlog Format": "Structure the script as a personal video blog with a conversational tone.",
|
||||
"Educational Format": "Structure the script to teach a concept with examples and explanations.",
|
||||
"Entertainment Format": "Structure the script to entertain while delivering the main message."
|
||||
}
|
||||
|
||||
# Build the prompt
|
||||
prompt = f"""
|
||||
**Instructions:**
|
||||
|
||||
Please generate a YouTube script in {language} for a video about **{main_points}** based on the following information:
|
||||
|
||||
**Target Audience:** {target_audience}
|
||||
**Tone and Style:** {tone_style}
|
||||
**Use Case:** {use_case}
|
||||
**Script Structure:** {script_structure}
|
||||
**Language:** {language}
|
||||
|
||||
**Structure Instructions:**
|
||||
{structure_instructions.get(script_structure, "Follow a logical flow to present the content.")}
|
||||
|
||||
**Additional Elements:**
|
||||
{"- Include a hook at the beginning to grab attention." if include_hook else ""}
|
||||
{"- End with a strong call to action." if include_cta else ""}
|
||||
{"- Include prompts for viewer engagement (e.g., questions, polls)." if include_engagement else ""}
|
||||
{"- Include suggested timestamps for key sections." if include_timestamps else ""}
|
||||
{"- Include visual cues and transitions." if include_visual_cues else ""}
|
||||
"""
|
||||
|
||||
# Add engagement hooks if provided
|
||||
if engagement_hooks:
|
||||
prompt += "\n**Engagement Hooks:**\n"
|
||||
for hook in engagement_hooks:
|
||||
prompt += f"- {hook}\n"
|
||||
|
||||
# Add community interaction points if provided
|
||||
if community_interactions:
|
||||
prompt += "\n**Community Interaction Points:**\n"
|
||||
for interaction in community_interactions:
|
||||
prompt += f"- {interaction}\n"
|
||||
|
||||
prompt += """
|
||||
**Specific Instructions:**
|
||||
* Keep the language clear and engaging.
|
||||
* Use a conversational tone that matches the target audience.
|
||||
* Include relevant examples and explanations.
|
||||
* Ensure the script flows naturally and maintains viewer interest.
|
||||
"""
|
||||
|
||||
try:
|
||||
response = llm_text_gen(prompt, system_prompt=system_prompt)
|
||||
return response
|
||||
except Exception as err:
|
||||
st.error(f"Error: Failed to get response from LLM: {err}")
|
||||
return None
|
||||
|
||||
|
||||
def generate_youtube_script_with_changes(target_audience, main_points, tone_style, use_case, script_structure,
|
||||
include_hook=False, include_cta=False, include_engagement=False,
|
||||
include_timestamps=False, include_visual_cues=False, engagement_hooks=None,
|
||||
community_interactions=None, changes="", language="English"):
|
||||
"""Generate a YouTube script based on the provided parameters and requested changes."""
|
||||
|
||||
# Create a custom system prompt for YouTube script generation
|
||||
system_prompt = f"""You are a YouTube script expert specializing in creating engaging, well-structured video scripts in {language}.
|
||||
Your task is to generate YouTube video scripts based on the provided information.
|
||||
Focus ONLY on creating scripts that are optimized for YouTube, with proper structure, engagement hooks, and calls to action.
|
||||
Return ONLY the script text, without any additional commentary or explanations.
|
||||
Format the script with clear sections, speaker notes, and visual cues where appropriate.
|
||||
Write the entire script in {language}."""
|
||||
|
||||
# Build structure-specific instructions
|
||||
structure_instructions = {
|
||||
"Problem-Solution": "Structure the script to first present a problem, then provide a solution.",
|
||||
"Before-After-Bridge": "Structure the script to show the before state, the transformation process, and the after state.",
|
||||
"Hook-Problem-Solution-Call to Action": "Start with a hook, present the problem, provide the solution, and end with a call to action.",
|
||||
"Compare and Contrast": "Structure the script to compare and contrast different options or approaches.",
|
||||
"Step-by-Step Tutorial": "Break down the content into clear, sequential steps.",
|
||||
"Case Study": "Present a real-world example or case study to illustrate the main points.",
|
||||
"Interview Format": "Structure the script as an interview with questions and answers.",
|
||||
"Review Format": "Structure the script as a review with pros, cons, and a final verdict.",
|
||||
"Vlog Format": "Structure the script as a personal video blog with a conversational tone.",
|
||||
"Educational Format": "Structure the script to teach a concept with examples and explanations.",
|
||||
"Entertainment Format": "Structure the script to entertain while delivering the main message."
|
||||
}
|
||||
|
||||
# Build the prompt
|
||||
prompt = f"""
|
||||
**Instructions:**
|
||||
|
||||
Please generate a YouTube script in {language} for a video about **{main_points}** based on the following information:
|
||||
|
||||
**Target Audience:** {target_audience}
|
||||
**Tone and Style:** {tone_style}
|
||||
**Use Case:** {use_case}
|
||||
**Script Structure:** {script_structure}
|
||||
**Language:** {language}
|
||||
|
||||
**Structure Instructions:**
|
||||
{structure_instructions.get(script_structure, "Follow a logical flow to present the content.")}
|
||||
|
||||
**Additional Elements:**
|
||||
{"- Include a hook at the beginning to grab attention." if include_hook else ""}
|
||||
{"- End with a strong call to action." if include_cta else ""}
|
||||
{"- Include prompts for viewer engagement (e.g., questions, polls)." if include_engagement else ""}
|
||||
{"- Include suggested timestamps for key sections." if include_timestamps else ""}
|
||||
{"- Include visual cues and transitions." if include_visual_cues else ""}
|
||||
"""
|
||||
|
||||
# Add engagement hooks if provided
|
||||
if engagement_hooks:
|
||||
prompt += "\n**Engagement Hooks:**\n"
|
||||
for hook in engagement_hooks:
|
||||
prompt += f"- {hook}\n"
|
||||
|
||||
# Add community interaction points if provided
|
||||
if community_interactions:
|
||||
prompt += "\n**Community Interaction Points:**\n"
|
||||
for interaction in community_interactions:
|
||||
prompt += f"- {interaction}\n"
|
||||
|
||||
# Add requested changes
|
||||
prompt += f"""
|
||||
**Requested Changes:**
|
||||
{changes}
|
||||
|
||||
**Specific Instructions:**
|
||||
* Keep the language clear and engaging.
|
||||
* Use a conversational tone that matches the target audience.
|
||||
* Include relevant examples and explanations.
|
||||
* Ensure the script flows naturally and maintains viewer interest.
|
||||
* Incorporate the requested changes into the script.
|
||||
"""
|
||||
|
||||
try:
|
||||
response = llm_text_gen(prompt, system_prompt=system_prompt)
|
||||
return response
|
||||
except Exception as err:
|
||||
st.error(f"Error: Failed to get response from LLM: {err}")
|
||||
return None
|
||||
|
||||
|
||||
def export_script(script, format_type, filename=None):
|
||||
"""Export the script in various formats."""
|
||||
if not filename:
|
||||
filename = "youtube_script"
|
||||
|
||||
if format_type == "Text":
|
||||
return script, f"{filename}.txt", "text/plain"
|
||||
elif format_type == "Markdown":
|
||||
return script, f"{filename}.md", "text/markdown"
|
||||
elif format_type == "HTML":
|
||||
html_content = f"<html><body><pre>{script}</pre></body></html>"
|
||||
return html_content, f"{filename}.html", "text/html"
|
||||
elif format_type == "JSON":
|
||||
json_content = json.dumps({"script": script}, indent=2)
|
||||
return json_content, f"{filename}.json", "application/json"
|
||||
elif format_type == "Subtitles (SRT)":
|
||||
# Convert script to basic SRT format
|
||||
lines = script.split('\n')
|
||||
srt_content = ""
|
||||
for i, line in enumerate(lines):
|
||||
if line.strip():
|
||||
start_time = f"00:00:{i*5:02d},000"
|
||||
end_time = f"00:00:{(i+1)*5:02d},000"
|
||||
srt_content += f"{i+1}\n{start_time} --> {end_time}\n{line}\n\n"
|
||||
return srt_content, f"{filename}.srt", "text/plain"
|
||||
else:
|
||||
return script, f"{filename}.txt", "text/plain"
|
||||
|
||||
|
||||
def write_yt_script():
|
||||
"""Create a user interface for YouTube Script Generator."""
|
||||
st.write("Generate professional YouTube video scripts with optimized structures for engagement.")
|
||||
|
||||
# Initialize session state for generated script if it doesn't exist
|
||||
if "generated_script" not in st.session_state:
|
||||
st.session_state.generated_script = None
|
||||
|
||||
# Create tabs for different sections
|
||||
tab1, tab2, tab3 = st.tabs(["Basic Info", "Advanced Options", "Engagement & Export"])
|
||||
|
||||
with tab1:
|
||||
# Basic information inputs
|
||||
main_points = st.text_area("Main Points/Keywords (comma-separated)",
|
||||
placeholder="e.g., cooking tips, healthy recipes, quick meals")
|
||||
target_audience = st.text_input("Target Audience",
|
||||
placeholder="e.g., beginners, professionals, parents")
|
||||
|
||||
# Create columns for tone, use case, structure, and language
|
||||
col1, col2, col3, col4 = st.columns(4)
|
||||
|
||||
with col1:
|
||||
tone_style = st.selectbox("Tone/Style",
|
||||
["Professional", "Casual", "Humorous", "Educational", "Entertaining", "Inspirational"])
|
||||
|
||||
with col2:
|
||||
use_case = st.selectbox("Use Case",
|
||||
["How-to/Tutorial", "Vlog", "Review", "Educational", "Entertainment", "News"])
|
||||
|
||||
with col3:
|
||||
script_structure = st.selectbox("Script Structure", [
|
||||
"Problem-Solution",
|
||||
"Before-After-Bridge",
|
||||
"Hook-Problem-Solution-Call to Action",
|
||||
"Compare and Contrast",
|
||||
"Step-by-Step Tutorial",
|
||||
"Case Study",
|
||||
"Interview Format",
|
||||
"Review Format",
|
||||
"Vlog Format",
|
||||
"Educational Format",
|
||||
"Entertainment Format"
|
||||
])
|
||||
|
||||
with col4:
|
||||
language = st.selectbox("Language", [
|
||||
"English",
|
||||
"Spanish",
|
||||
"French",
|
||||
"German",
|
||||
"Italian",
|
||||
"Portuguese",
|
||||
"Russian",
|
||||
"Japanese",
|
||||
"Korean",
|
||||
"Chinese",
|
||||
"Hindi",
|
||||
"Arabic"
|
||||
])
|
||||
|
||||
with tab2:
|
||||
# Advanced options
|
||||
st.subheader("Additional Elements")
|
||||
include_hook = st.checkbox("Include Hook", value=True)
|
||||
include_cta = st.checkbox("Include Call to Action", value=True)
|
||||
include_engagement = st.checkbox("Include Viewer Engagement Prompts", value=True)
|
||||
include_timestamps = st.checkbox("Include Suggested Timestamps", value=True)
|
||||
include_visual_cues = st.checkbox("Include Visual Cues/Transitions", value=True)
|
||||
|
||||
with tab3:
|
||||
# Engagement hooks
|
||||
st.subheader("Engagement Hooks")
|
||||
st.write("Select engagement hooks to include in your script:")
|
||||
|
||||
engagement_hooks = []
|
||||
if st.checkbox("Question Hook", value=False):
|
||||
engagement_hooks.append("Start with a thought-provoking question to engage viewers immediately")
|
||||
if st.checkbox("Story Hook", value=False):
|
||||
engagement_hooks.append("Begin with a brief, relevant story or anecdote")
|
||||
if st.checkbox("Statistic Hook", value=False):
|
||||
engagement_hooks.append("Open with an interesting statistic or fact")
|
||||
if st.checkbox("Controversy Hook", value=False):
|
||||
engagement_hooks.append("Present a controversial statement or opinion to spark interest")
|
||||
if st.checkbox("Promise Hook", value=False):
|
||||
engagement_hooks.append("Make a promise about what viewers will learn or gain")
|
||||
if st.checkbox("Scenario Hook", value=False):
|
||||
engagement_hooks.append("Describe a scenario or situation viewers can relate to")
|
||||
if st.checkbox("Mystery Hook", value=False):
|
||||
engagement_hooks.append("Create a sense of mystery or intrigue")
|
||||
if st.checkbox("Quote Hook", value=False):
|
||||
engagement_hooks.append("Start with a relevant quote from an expert or notable figure")
|
||||
|
||||
# Community interaction points
|
||||
st.subheader("Community Interaction Points")
|
||||
st.write("Select community interaction points to include in your script:")
|
||||
|
||||
community_interactions = []
|
||||
if st.checkbox("Comment Prompt", value=False):
|
||||
community_interactions.append("Ask viewers to share their experiences or opinions in the comments")
|
||||
if st.checkbox("Poll Suggestion", value=False):
|
||||
community_interactions.append("Suggest creating a poll for viewers to vote on")
|
||||
if st.checkbox("Question for Comments", value=False):
|
||||
community_interactions.append("Pose a specific question for viewers to answer in the comments")
|
||||
if st.checkbox("Challenge", value=False):
|
||||
community_interactions.append("Challenge viewers to try something and report back")
|
||||
if st.checkbox("Tag Friends", value=False):
|
||||
community_interactions.append("Encourage viewers to tag friends who might benefit from the content")
|
||||
if st.checkbox("Share Request", value=False):
|
||||
community_interactions.append("Ask viewers to share the video with others who might find it helpful")
|
||||
if st.checkbox("Community Post", value=False):
|
||||
community_interactions.append("Mention creating a community post to continue the discussion")
|
||||
if st.checkbox("Live Stream Teaser", value=False):
|
||||
community_interactions.append("Tease an upcoming live stream on the same topic")
|
||||
|
||||
# Export options
|
||||
st.subheader("Export Options")
|
||||
export_format = st.selectbox("Export Format", [
|
||||
"Text",
|
||||
"Markdown",
|
||||
"HTML",
|
||||
"JSON",
|
||||
"Subtitles (SRT)"
|
||||
])
|
||||
|
||||
custom_filename = st.text_input("Custom Filename (optional)",
|
||||
placeholder="Leave blank for default filename")
|
||||
|
||||
if st.button("Generate Script"):
|
||||
if not main_points:
|
||||
st.error("Please enter main points/keywords.")
|
||||
return
|
||||
|
||||
with st.spinner("Generating script..."):
|
||||
script = generate_youtube_script(
|
||||
target_audience, main_points, tone_style, use_case, script_structure,
|
||||
include_hook, include_cta, include_engagement, include_timestamps, include_visual_cues,
|
||||
engagement_hooks if engagement_hooks else None,
|
||||
community_interactions if community_interactions else None,
|
||||
language
|
||||
)
|
||||
|
||||
if script:
|
||||
# Store the script in session state
|
||||
st.session_state.generated_script = script
|
||||
|
||||
# Store other parameters in session state for regeneration
|
||||
st.session_state.script_params = {
|
||||
"target_audience": target_audience,
|
||||
"main_points": main_points,
|
||||
"tone_style": tone_style,
|
||||
"use_case": use_case,
|
||||
"script_structure": script_structure,
|
||||
"include_hook": include_hook,
|
||||
"include_cta": include_cta,
|
||||
"include_engagement": include_engagement,
|
||||
"include_timestamps": include_timestamps,
|
||||
"include_visual_cues": include_visual_cues,
|
||||
"engagement_hooks": engagement_hooks if engagement_hooks else None,
|
||||
"community_interactions": community_interactions if community_interactions else None,
|
||||
"language": language
|
||||
}
|
||||
|
||||
st.subheader("Generated Script")
|
||||
|
||||
# Display script with tabs for different views
|
||||
script_tab1, script_tab2 = st.tabs(["Formatted View", "Plain Text"])
|
||||
|
||||
with script_tab1:
|
||||
st.markdown(script)
|
||||
|
||||
with script_tab2:
|
||||
st.code(script)
|
||||
|
||||
# Export options
|
||||
st.subheader("Export Script")
|
||||
|
||||
# Get export data
|
||||
export_data, export_filename, mime_type = export_script(
|
||||
script,
|
||||
export_format,
|
||||
custom_filename if custom_filename else None
|
||||
)
|
||||
|
||||
# Create columns for the buttons
|
||||
btn_col1, btn_col2 = st.columns(2)
|
||||
|
||||
with btn_col1:
|
||||
# Download button
|
||||
st.download_button(
|
||||
label=f"Download as {export_format}",
|
||||
data=export_data,
|
||||
file_name=export_filename,
|
||||
mime=mime_type
|
||||
)
|
||||
|
||||
with btn_col2:
|
||||
# Regenerate button
|
||||
if st.button("Regenerate"):
|
||||
st.session_state.show_regenerate_popover = True
|
||||
|
||||
# Regenerate popover
|
||||
if st.session_state.get("show_regenerate_popover", False):
|
||||
with st.form("regenerate_form"):
|
||||
st.subheader("Regenerate Script")
|
||||
st.write("Specify changes you'd like to make to the script:")
|
||||
changes = st.text_area("Changes to make",
|
||||
placeholder="e.g., Make it more casual, add more call-to-actions, focus on product benefits")
|
||||
|
||||
submitted = st.form_submit_button("Regenerate with Changes")
|
||||
|
||||
if submitted and changes:
|
||||
with st.spinner("Regenerating script..."):
|
||||
# Get the stored parameters
|
||||
params = st.session_state.script_params
|
||||
|
||||
# Generate a new script with the changes
|
||||
new_script = generate_youtube_script_with_changes(
|
||||
params["target_audience"],
|
||||
params["main_points"],
|
||||
params["tone_style"],
|
||||
params["use_case"],
|
||||
params["script_structure"],
|
||||
params["include_hook"],
|
||||
params["include_cta"],
|
||||
params["include_engagement"],
|
||||
params["include_timestamps"],
|
||||
params["include_visual_cues"],
|
||||
params["engagement_hooks"],
|
||||
params["community_interactions"],
|
||||
changes,
|
||||
params["language"]
|
||||
)
|
||||
|
||||
if new_script:
|
||||
# Update the stored script
|
||||
st.session_state.generated_script = new_script
|
||||
st.session_state.show_regenerate_popover = False
|
||||
st.rerun()
|
||||
else:
|
||||
st.error("Failed to regenerate script. Please try again.")
|
||||
|
||||
# Additional export options
|
||||
if st.checkbox("Show additional export options"):
|
||||
col1, col2 = st.columns(2)
|
||||
with col1:
|
||||
if st.button("Copy to Clipboard"):
|
||||
st.code(script)
|
||||
st.success("Script copied to clipboard!")
|
||||
|
||||
with col2:
|
||||
if st.button("Save to Local File"):
|
||||
# This is a placeholder - actual file saving would require additional backend functionality
|
||||
st.info("This feature would save the file locally on your device.")
|
||||
else:
|
||||
st.error("Failed to generate script. Please try again.")
|
||||
|
||||
# Display previously generated script if it exists in session state
|
||||
elif st.session_state.generated_script:
|
||||
script = st.session_state.generated_script
|
||||
params = st.session_state.script_params
|
||||
|
||||
st.subheader("Generated Script")
|
||||
|
||||
# Display script with tabs for different views
|
||||
script_tab1, script_tab2 = st.tabs(["Formatted View", "Plain Text"])
|
||||
|
||||
with script_tab1:
|
||||
st.markdown(script)
|
||||
|
||||
with script_tab2:
|
||||
st.code(script)
|
||||
|
||||
# Export options
|
||||
st.subheader("Export Script")
|
||||
|
||||
# Get export data
|
||||
export_data, export_filename, mime_type = export_script(
|
||||
script,
|
||||
export_format,
|
||||
custom_filename if custom_filename else None
|
||||
)
|
||||
|
||||
# Create columns for the buttons
|
||||
btn_col1, btn_col2 = st.columns(2)
|
||||
|
||||
with btn_col1:
|
||||
# Download button
|
||||
st.download_button(
|
||||
label=f"Download as {export_format}",
|
||||
data=export_data,
|
||||
file_name=export_filename,
|
||||
mime=mime_type
|
||||
)
|
||||
|
||||
with btn_col2:
|
||||
# Regenerate button
|
||||
if st.button("Regenerate"):
|
||||
st.session_state.show_regenerate_popover = True
|
||||
|
||||
# Regenerate popover
|
||||
if st.session_state.get("show_regenerate_popover", False):
|
||||
with st.form("regenerate_form"):
|
||||
st.subheader("Regenerate Script")
|
||||
st.write("Specify changes you'd like to make to the script:")
|
||||
changes = st.text_area("Changes to make",
|
||||
placeholder="e.g., Make it more casual, add more call-to-actions, focus on product benefits")
|
||||
|
||||
submitted = st.form_submit_button("Regenerate with Changes")
|
||||
|
||||
if submitted and changes:
|
||||
with st.spinner("Regenerating script..."):
|
||||
# Generate a new script with the changes
|
||||
new_script = generate_youtube_script_with_changes(
|
||||
params["target_audience"],
|
||||
params["main_points"],
|
||||
params["tone_style"],
|
||||
params["use_case"],
|
||||
params["script_structure"],
|
||||
params["include_hook"],
|
||||
params["include_cta"],
|
||||
params["include_engagement"],
|
||||
params["include_timestamps"],
|
||||
params["include_visual_cues"],
|
||||
params["engagement_hooks"],
|
||||
params["community_interactions"],
|
||||
changes,
|
||||
params["language"]
|
||||
)
|
||||
|
||||
if new_script:
|
||||
# Update the stored script
|
||||
st.session_state.generated_script = new_script
|
||||
st.session_state.show_regenerate_popover = False
|
||||
st.rerun()
|
||||
else:
|
||||
st.error("Failed to regenerate script. Please try again.")
|
||||
|
||||
# Additional export options
|
||||
if st.checkbox("Show additional export options"):
|
||||
col1, col2 = st.columns(2)
|
||||
with col1:
|
||||
if st.button("Copy to Clipboard"):
|
||||
st.code(script)
|
||||
st.success("Script copied to clipboard!")
|
||||
|
||||
with col2:
|
||||
if st.button("Save to Local File"):
|
||||
# This is a placeholder - actual file saving would require additional backend functionality
|
||||
st.info("This feature would save the file locally on your device.")
|
||||
@@ -0,0 +1,314 @@
|
||||
"""
|
||||
YouTube Shorts Script Generator Module
|
||||
|
||||
This module provides functionality for generating optimized scripts for YouTube Shorts.
|
||||
"""
|
||||
|
||||
import streamlit as st
|
||||
import time
|
||||
import logging
|
||||
from lib.gpt_providers.text_generation.main_text_generation import llm_text_gen
|
||||
|
||||
# Configure logging
|
||||
logging.basicConfig(
|
||||
level=logging.INFO,
|
||||
format='%(asctime)s - %(name)s - %(levelname)s - %(message)s'
|
||||
)
|
||||
logger = logging.getLogger('youtube_shorts_generator')
|
||||
|
||||
def generate_shorts_script(hook_type, main_topic, target_audience, tone_style,
|
||||
content_type, duration_seconds=60, include_captions=True,
|
||||
include_text_overlay=True, include_sound_effects=False,
|
||||
vertical_framing_notes=True, language="English"):
|
||||
"""Generate a YouTube Shorts script optimized for vertical format and short duration."""
|
||||
|
||||
# Create a custom system prompt for Shorts script generation
|
||||
system_prompt = f"""You are a YouTube Shorts expert specializing in creating viral, engaging scripts for vertical short-form videos.
|
||||
Your task is to generate scripts that are perfectly timed for {duration_seconds} seconds or less.
|
||||
Focus on hooks that grab attention in the first 1-2 seconds.
|
||||
Format the script with clear sections for visuals, audio, and text overlays.
|
||||
Write the entire script in {language}.
|
||||
Remember that Shorts are viewed vertically (9:16 aspect ratio) and need to work without sound."""
|
||||
|
||||
# Build hook-specific instructions
|
||||
hook_instructions = {
|
||||
"Question": "Start with an intriguing question that stops the scroll",
|
||||
"Statistic": "Begin with a surprising statistic or fact",
|
||||
"Challenge": "Present a challenge or dare to the viewer",
|
||||
"Tutorial": "Jump straight into a quick how-to or life hack",
|
||||
"Transformation": "Show a before/after or transformation hook",
|
||||
"Trend": "Leverage a current trend or sound",
|
||||
"Story": "Start with a captivating micro-story",
|
||||
"Controversy": "Present a controversial or surprising statement"
|
||||
}
|
||||
|
||||
# Build the prompt
|
||||
prompt = f"""
|
||||
**Instructions:**
|
||||
|
||||
Create a YouTube Shorts script about **{main_topic}** with these specifications:
|
||||
|
||||
**Core Elements:**
|
||||
- Hook Type: {hook_type} - {hook_instructions.get(hook_type, "Create an attention-grabbing opening")}
|
||||
- Target Audience: {target_audience}
|
||||
- Tone/Style: {tone_style}
|
||||
- Content Type: {content_type}
|
||||
- Duration: {duration_seconds} seconds
|
||||
- Language: {language}
|
||||
|
||||
**Required Elements:**
|
||||
{"- Include caption suggestions for accessibility" if include_captions else ""}
|
||||
{"- Include text overlay positions and timing" if include_text_overlay else ""}
|
||||
{"- Include sound effect suggestions" if include_sound_effects else ""}
|
||||
{"- Include vertical framing notes for optimal composition" if vertical_framing_notes else ""}
|
||||
|
||||
**Format the script in this structure:**
|
||||
1. HOOK (0-2 seconds)
|
||||
2. MAIN CONTENT (3-50 seconds)
|
||||
3. CALL TO ACTION (last 10 seconds)
|
||||
|
||||
**For each section, specify:**
|
||||
- Visual Instructions (what to show)
|
||||
- Text Overlays (what text appears and where)
|
||||
- Audio/Voiceover
|
||||
- Timing (in seconds)
|
||||
- Camera Angles/Framing Notes
|
||||
|
||||
**Remember:**
|
||||
- Scripts must work without sound (many viewers watch on mute)
|
||||
- Text should be centered in the middle 50% of the vertical frame
|
||||
- Keep text concise and readable
|
||||
- Include pattern interrupts every 3-5 seconds
|
||||
- End with a clear call-to-action
|
||||
"""
|
||||
|
||||
try:
|
||||
response = llm_text_gen(prompt, system_prompt=system_prompt)
|
||||
return response
|
||||
except Exception as err:
|
||||
st.error(f"Error: Failed to get response from LLM: {err}")
|
||||
return None
|
||||
|
||||
def analyze_shorts_script(script):
|
||||
"""Analyze a Shorts script for optimal engagement metrics."""
|
||||
analysis = {
|
||||
'duration_estimate': 0,
|
||||
'hook_strength': 0,
|
||||
'pattern_interrupts': 0,
|
||||
'text_overlay_count': 0,
|
||||
'readability_score': 0,
|
||||
'optimization_score': 0
|
||||
}
|
||||
|
||||
# Basic analysis (can be enhanced with more sophisticated metrics)
|
||||
lines = script.split('\n')
|
||||
word_count = len(script.split())
|
||||
|
||||
# Estimate duration (rough approximation)
|
||||
analysis['duration_estimate'] = word_count * 0.4 # Average speaking speed
|
||||
|
||||
# Count pattern interrupts
|
||||
analysis['pattern_interrupts'] = script.lower().count('cut to') + script.lower().count('transition')
|
||||
|
||||
# Count text overlays
|
||||
analysis['text_overlay_count'] = script.lower().count('text:') + script.lower().count('overlay:')
|
||||
|
||||
# Calculate optimization score
|
||||
score = 100
|
||||
|
||||
# Penalize if estimated duration is too long
|
||||
if analysis['duration_estimate'] > 60:
|
||||
score -= (analysis['duration_estimate'] - 60) * 2
|
||||
|
||||
# Check for hook presence
|
||||
if not any(hook in script.lower() for hook in ['hook:', 'opening:', '0-2 seconds:']):
|
||||
score -= 20
|
||||
|
||||
# Check for pattern interrupts (ideal is 1 every 5 seconds)
|
||||
ideal_interrupts = analysis['duration_estimate'] / 5
|
||||
if analysis['pattern_interrupts'] < ideal_interrupts:
|
||||
score -= 10
|
||||
|
||||
# Check for text overlay usage
|
||||
if analysis['text_overlay_count'] < 3:
|
||||
score -= 10
|
||||
|
||||
# Check for call-to-action
|
||||
if not any(cta in script.lower() for cta in ['call to action', 'cta:', 'subscribe', 'follow']):
|
||||
score -= 15
|
||||
|
||||
analysis['optimization_score'] = max(0, score)
|
||||
return analysis
|
||||
|
||||
def generate_shorts_narration(shorts_script, language="English"):
|
||||
system_prompt = f"""You are an expert at converting YouTube Shorts scripts into natural, engaging narration.\nYour task is to read the provided Shorts script and output only the narration lines, as they would be spoken in the video.\nOmit all visual instructions, timing, text overlays, and technical cues. Write the narration in {language}."""
|
||||
prompt = f"""Shorts Script:\n{shorts_script}\n\nInstructions:\nExtract and rewrite only the narration lines, as they would be spoken in the video. Do not include any section headers, cues, or formatting. Output only the narration text."""
|
||||
try:
|
||||
response = llm_text_gen(prompt, system_prompt=system_prompt)
|
||||
return response.strip()
|
||||
except Exception as err:
|
||||
st.error(f"Error: Failed to get narration from LLM: {err}")
|
||||
return ""
|
||||
|
||||
def write_yt_shorts():
|
||||
"""Create a user interface for YouTube Shorts Script Generator."""
|
||||
st.write("Generate optimized scripts for YouTube Shorts that grab attention and drive engagement.")
|
||||
|
||||
# Initialize session state for generated script and active tab if they don't exist
|
||||
if "generated_shorts_script" not in st.session_state:
|
||||
st.session_state.generated_shorts_script = None
|
||||
if "active_tab" not in st.session_state:
|
||||
st.session_state.active_tab = "Core Elements"
|
||||
|
||||
# Create tabs for different sections
|
||||
tab1, tab2, tab3 = st.tabs(["Core Elements", "Style & Format", "Preview & Export"])
|
||||
|
||||
# Set the active tab based on session state
|
||||
if st.session_state.active_tab == "Core Elements":
|
||||
tab1.active = True
|
||||
elif st.session_state.active_tab == "Style & Format":
|
||||
tab2.active = True
|
||||
elif st.session_state.active_tab == "Preview & Export":
|
||||
tab3.active = True
|
||||
|
||||
with tab1:
|
||||
# Core elements
|
||||
main_topic = st.text_area("Main Topic/Concept",
|
||||
placeholder="e.g., Quick cooking hack, Life-changing productivity tip")
|
||||
|
||||
col1, col2 = st.columns(2)
|
||||
with col1:
|
||||
hook_type = st.selectbox("Hook Type", [
|
||||
"Question",
|
||||
"Statistic",
|
||||
"Challenge",
|
||||
"Tutorial",
|
||||
"Transformation",
|
||||
"Trend",
|
||||
"Story",
|
||||
"Controversy"
|
||||
])
|
||||
|
||||
target_audience = st.text_input("Target Audience",
|
||||
placeholder="e.g., Gen Z, busy professionals")
|
||||
|
||||
with col2:
|
||||
content_type = st.selectbox("Content Type", [
|
||||
"Tutorial/How-to",
|
||||
"Life Hack",
|
||||
"Entertainment",
|
||||
"Educational",
|
||||
"Trend",
|
||||
"Story",
|
||||
"Challenge",
|
||||
"Review"
|
||||
])
|
||||
|
||||
tone_style = st.selectbox("Tone/Style", [
|
||||
"Energetic",
|
||||
"Professional",
|
||||
"Casual",
|
||||
"Humorous",
|
||||
"Dramatic",
|
||||
"Inspirational"
|
||||
])
|
||||
|
||||
with tab2:
|
||||
# Style and format options
|
||||
col1, col2 = st.columns(2)
|
||||
|
||||
with col1:
|
||||
duration_seconds = st.slider("Duration (seconds)", 15, 60, 60)
|
||||
language = st.selectbox("Language", [
|
||||
"English",
|
||||
"Spanish",
|
||||
"French",
|
||||
"German",
|
||||
"Italian",
|
||||
"Portuguese",
|
||||
"Russian",
|
||||
"Japanese",
|
||||
"Korean",
|
||||
"Chinese"
|
||||
])
|
||||
|
||||
with col2:
|
||||
include_captions = st.checkbox("Include Captions", value=True)
|
||||
include_text_overlay = st.checkbox("Include Text Overlay Positions", value=True)
|
||||
include_sound_effects = st.checkbox("Include Sound Effects", value=False)
|
||||
vertical_framing_notes = st.checkbox("Include Vertical Framing Notes", value=True)
|
||||
|
||||
with tab3:
|
||||
if st.session_state.generated_shorts_script:
|
||||
# Display the generated script
|
||||
st.subheader("Generated Shorts Script")
|
||||
|
||||
# Create tabs for different views
|
||||
script_tab1, script_tab2, script_tab3 = st.tabs(["Formatted", "Analysis", "Export"])
|
||||
|
||||
with script_tab1:
|
||||
st.markdown(st.session_state.generated_shorts_script)
|
||||
|
||||
with script_tab2:
|
||||
# Analyze the script
|
||||
analysis = analyze_shorts_script(st.session_state.generated_shorts_script)
|
||||
|
||||
# Display analysis results
|
||||
col1, col2 = st.columns(2)
|
||||
|
||||
with col1:
|
||||
st.metric("Estimated Duration", f"{analysis['duration_estimate']:.1f}s")
|
||||
st.metric("Pattern Interrupts", analysis['pattern_interrupts'])
|
||||
st.metric("Text Overlays", analysis['text_overlay_count'])
|
||||
|
||||
with col2:
|
||||
# Display optimization score with color
|
||||
score = analysis['optimization_score']
|
||||
color = "red" if score < 60 else "orange" if score < 80 else "green"
|
||||
st.markdown(f"### Optimization Score: <span style='color: {color}'>{score}%</span>",
|
||||
unsafe_allow_html=True)
|
||||
|
||||
with script_tab3:
|
||||
# Export options
|
||||
export_format = st.selectbox("Export Format", [
|
||||
"Text",
|
||||
"Markdown",
|
||||
"Shot List",
|
||||
"Storyboard"
|
||||
])
|
||||
|
||||
if st.button("Export Script"):
|
||||
# Implement export functionality based on selected format
|
||||
st.success(f"Script exported in {export_format} format!")
|
||||
st.download_button(
|
||||
"Download Script",
|
||||
st.session_state.generated_shorts_script,
|
||||
file_name=f"shorts_script.{export_format.lower()}",
|
||||
mime="text/plain"
|
||||
)
|
||||
|
||||
# Generate button
|
||||
if st.button("Generate Shorts Script"):
|
||||
if not main_topic:
|
||||
st.error("Please enter a main topic/concept.")
|
||||
return
|
||||
|
||||
with st.spinner("Generating Shorts script..."):
|
||||
script = generate_shorts_script(
|
||||
hook_type, main_topic, target_audience, tone_style, content_type,
|
||||
duration_seconds, include_captions, include_text_overlay,
|
||||
include_sound_effects, vertical_framing_notes, language
|
||||
)
|
||||
|
||||
if script:
|
||||
st.session_state.generated_shorts_script = script
|
||||
# Set active tab to Preview & Export
|
||||
st.session_state.active_tab = "Preview & Export"
|
||||
st.success("✨ Script generated successfully! Check the 'Preview & Export' tab to view, analyze, and download your script.")
|
||||
st.rerun()
|
||||
else:
|
||||
st.error("Failed to generate script. Please try again.")
|
||||
|
||||
# Add a message about preview and export if script exists but we're not on the Preview tab
|
||||
if st.session_state.generated_shorts_script and st.session_state.active_tab != "Preview & Export":
|
||||
st.info("💡 Your generated script is ready! Go to the 'Preview & Export' tab to view, analyze, and download it.")
|
||||
@@ -0,0 +1,972 @@
|
||||
"""
|
||||
YouTube Shorts Video Generator
|
||||
|
||||
This module provides functionality to generate YouTube Shorts videos using AI.
|
||||
It adapts the story video generator for the vertical format and shorter duration of Shorts.
|
||||
"""
|
||||
|
||||
import os
|
||||
import re
|
||||
import time
|
||||
import json
|
||||
import uuid
|
||||
import tempfile
|
||||
import logging
|
||||
import traceback
|
||||
from pathlib import Path
|
||||
from typing import List, Dict, Any, Tuple, Optional, Union, Callable
|
||||
from functools import wraps
|
||||
from datetime import datetime
|
||||
import random
|
||||
import functools
|
||||
|
||||
import streamlit as st
|
||||
import numpy as np
|
||||
from PIL import Image, ImageDraw, ImageFont
|
||||
import requests
|
||||
|
||||
# Try importing moviepy with proper error handling
|
||||
try:
|
||||
from moviepy.editor import (
|
||||
ImageSequenceClip,
|
||||
TextClip,
|
||||
CompositeVideoClip,
|
||||
AudioFileClip,
|
||||
AudioClip,
|
||||
CompositeAudioClip,
|
||||
)
|
||||
MOVIEPY_AVAILABLE = True
|
||||
except ImportError as e:
|
||||
st.error(
|
||||
"MoviePy is not properly installed. Please install it using:\n"
|
||||
"pip install moviepy imageio imageio-ffmpeg"
|
||||
)
|
||||
MOVIEPY_AVAILABLE = False
|
||||
|
||||
# Try importing gTTS with proper error handling
|
||||
try:
|
||||
from gtts import gTTS
|
||||
GTTS_AVAILABLE = True
|
||||
except ImportError:
|
||||
st.error(
|
||||
"gTTS is not installed. Please install it using:\n"
|
||||
"pip install gTTS"
|
||||
)
|
||||
GTTS_AVAILABLE = False
|
||||
|
||||
# Import LLM text generation and image generation
|
||||
from lib.gpt_providers.text_generation.main_text_generation import llm_text_gen
|
||||
from lib.gpt_providers.text_to_image_generation.main_generate_image_from_prompt import generate_image
|
||||
from .shorts_script_generator import generate_shorts_script, generate_shorts_narration
|
||||
from lib.ai_writers.ai_story_video_generator.story_video_generator import StoryVideoGenerator
|
||||
|
||||
# Configure logging
|
||||
log_dir = Path("logs")
|
||||
log_dir.mkdir(exist_ok=True)
|
||||
log_file = log_dir / f"shorts_generator_{datetime.now().strftime('%Y%m%d_%H%M%S')}.log"
|
||||
|
||||
logging.basicConfig(
|
||||
level=logging.INFO,
|
||||
format="%(asctime)s - %(name)s - %(levelname)s - %(message)s",
|
||||
handlers=[
|
||||
logging.FileHandler(log_file),
|
||||
logging.StreamHandler()
|
||||
]
|
||||
)
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
# Constants
|
||||
DEFAULT_FPS = 30 # Higher FPS for smoother Shorts
|
||||
DEFAULT_DURATION = 2 # seconds per scene (shorter for Shorts)
|
||||
DEFAULT_TRANSITION_DURATION = 0.5 # seconds for transition
|
||||
DEFAULT_FONT_SIZE = 32 # Larger font for vertical format
|
||||
DEFAULT_FONT_COLOR = "white"
|
||||
DEFAULT_MUSIC_URL = "https://freepd.com/music/Upbeat%20Uplifting%20Corporate.mp3" # Example free music URL
|
||||
DEFAULT_IMAGE_WIDTH = 1080 # Standard Shorts width
|
||||
DEFAULT_IMAGE_HEIGHT = 1920 # Standard Shorts height (9:16 aspect ratio)
|
||||
TEXT_AREA_HEIGHT_RATIO = 1/4 # Smaller text area for vertical format
|
||||
TEXT_PADDING = 30
|
||||
TEXT_OVERLAY_ALPHA = 180 # More opaque overlay for better readability
|
||||
|
||||
# Shorts-specific constants
|
||||
MAX_SHORTS_DURATION = 60 # Maximum duration for YouTube Shorts
|
||||
MIN_SHORTS_DURATION = 15 # Minimum duration for YouTube Shorts
|
||||
DEFAULT_SHORTS_DURATION = 30 # Default duration for Shorts
|
||||
MAX_SCENES = 15 # Maximum number of scenes to generate
|
||||
MIN_SCENES = 5 # Minimum number of scenes
|
||||
WORDS_PER_SECOND = 2.5 # Average speaking rate for narration
|
||||
|
||||
# Video resolutions for Shorts (vertical format)
|
||||
VIDEO_RESOLUTIONS = {
|
||||
"1080p": (1080, 1920), # Standard Shorts resolution
|
||||
"720p": (720, 1280), # Lower resolution option
|
||||
}
|
||||
|
||||
# Transition styles optimized for Shorts
|
||||
TRANSITION_STYLES = {
|
||||
"None": None,
|
||||
"Fade": "fade",
|
||||
"Slide Up": "slide_up",
|
||||
"Slide Down": "slide_down",
|
||||
"Zoom": "zoom",
|
||||
"Wipe": "wipe"
|
||||
}
|
||||
|
||||
# Content styles for Shorts
|
||||
CONTENT_STYLES = {
|
||||
"Tutorial": {
|
||||
"style": "tutorial",
|
||||
"description": "Step-by-step instructional content"
|
||||
},
|
||||
"Story": {
|
||||
"style": "story",
|
||||
"description": "Narrative-driven content"
|
||||
},
|
||||
"Tips": {
|
||||
"style": "tips",
|
||||
"description": "Quick tips and tricks"
|
||||
},
|
||||
"Review": {
|
||||
"style": "review",
|
||||
"description": "Product or service reviews"
|
||||
},
|
||||
"Behind the Scenes": {
|
||||
"style": "behind_scenes",
|
||||
"description": "Behind-the-scenes content"
|
||||
}
|
||||
}
|
||||
|
||||
# Narration languages
|
||||
NARRATION_LANGUAGES = {
|
||||
"English (US)": "en-us",
|
||||
"English (UK)": "en-gb",
|
||||
"Spanish": "es",
|
||||
"French": "fr",
|
||||
"German": "de",
|
||||
"Italian": "it",
|
||||
"Japanese": "ja",
|
||||
"Korean": "ko",
|
||||
"Chinese": "zh-cn",
|
||||
"Hindi": "hi"
|
||||
}
|
||||
|
||||
# Retry configuration
|
||||
MAX_RETRIES = 3
|
||||
INITIAL_RETRY_DELAY = 1 # Initial delay in seconds
|
||||
MAX_RETRY_DELAY = 30 # Maximum delay in seconds
|
||||
RETRYABLE_ERRORS = (
|
||||
ConnectionError,
|
||||
TimeoutError,
|
||||
requests.exceptions.RequestException,
|
||||
OSError, # For file system operations
|
||||
IOError, # For file system operations
|
||||
)
|
||||
|
||||
def retry_on_error(max_retries: int = MAX_RETRIES, initial_delay: int = INITIAL_RETRY_DELAY, max_delay: int = MAX_RETRY_DELAY):
|
||||
"""
|
||||
Decorator for retrying functions on specific errors with exponential backoff.
|
||||
|
||||
# ... existing code ...
|
||||
"""
|
||||
|
||||
def extract_narration_from_shorts_script(script: str) -> str:
|
||||
"""
|
||||
Extract and optimize narration from the script for Shorts format.
|
||||
Ensures narration is concise, valuable, and properly timed.
|
||||
"""
|
||||
scenes = re.split(r'\n\n+', script)
|
||||
narration_lines = []
|
||||
total_words = 0
|
||||
max_words = 75 # Target for 30-second video (2.5 words per second)
|
||||
|
||||
# Extract all potential narration lines first
|
||||
potential_lines = []
|
||||
for scene in scenes:
|
||||
match = re.search(r'Audio/Voiceover:\s*(.*)', scene)
|
||||
if match:
|
||||
narration = match.group(1).strip()
|
||||
narration = re.split(r'\n[A-Z][^:]+:', narration)[0].strip()
|
||||
if narration:
|
||||
potential_lines.append(narration)
|
||||
|
||||
# Process lines to create engaging narration
|
||||
if potential_lines:
|
||||
# Start with a hook
|
||||
first_line = potential_lines[0]
|
||||
if not any(word in first_line.lower() for word in ['discover', 'learn', 'find out', 'see how', 'watch']):
|
||||
first_line = f"Discover how to {first_line.lower()}"
|
||||
narration_lines.append(first_line)
|
||||
total_words += len(first_line.split())
|
||||
|
||||
# Process middle lines
|
||||
for line in potential_lines[1:-1]:
|
||||
# Add value-focused phrases
|
||||
if not any(word in line.lower() for word in ['because', 'why', 'how', 'what', 'when', 'where']):
|
||||
line = f"Here's why: {line}"
|
||||
|
||||
# Check word count
|
||||
words = line.split()
|
||||
if total_words + len(words) <= max_words:
|
||||
narration_lines.append(line)
|
||||
total_words += len(words)
|
||||
else:
|
||||
break
|
||||
|
||||
# Add a strong closing
|
||||
if len(potential_lines) > 1:
|
||||
last_line = potential_lines[-1]
|
||||
if not any(phrase in last_line.lower() for phrase in ['try it', 'get started', 'follow for more']):
|
||||
last_line = f"Ready to try it? {last_line}"
|
||||
if total_words + len(last_line.split()) <= max_words:
|
||||
narration_lines.append(last_line)
|
||||
|
||||
# If we have too few words, add a call to action
|
||||
if total_words < 50 and narration_lines:
|
||||
cta = "Follow for more tips like this!"
|
||||
if total_words + len(cta.split()) <= max_words:
|
||||
narration_lines.append(cta)
|
||||
|
||||
# Join with proper pacing and emphasis
|
||||
final_narration = ' '.join(narration_lines)
|
||||
|
||||
# Add emphasis to key points
|
||||
final_narration = re.sub(r'([.!?])\s+', r'\1\n\n', final_narration) # Add pauses
|
||||
|
||||
return final_narration
|
||||
|
||||
def generate_shorts_narration(script: str, language: str = "en-us", target_duration: int = 30) -> str:
|
||||
"""
|
||||
Generate a clean, natural-sounding narration script for YouTube Shorts.
|
||||
Focuses only on what the listener needs to hear, without technical details.
|
||||
"""
|
||||
# Calculate target word count based on duration and user-defined speaking rate
|
||||
words_per_second = getattr(st.session_state, 'svgen_words_per_second', WORDS_PER_SECOND)
|
||||
narration_padding = getattr(st.session_state, 'svgen_narration_padding', 0.5)
|
||||
target_words = int((target_duration - narration_padding) * words_per_second)
|
||||
|
||||
# Extract key information from the script
|
||||
scenes = re.split(r'\n\n+', script)
|
||||
audio_lines = []
|
||||
|
||||
for scene in scenes:
|
||||
# Extract only the audio/voiceover content
|
||||
audio_match = re.search(r'Audio/Voiceover:\s*(.*?)(?=\n|$)', scene)
|
||||
if audio_match:
|
||||
audio_lines.append(audio_match.group(1).strip())
|
||||
|
||||
# Create a specialized prompt for clean narration generation
|
||||
narration_prompt = f"""
|
||||
Create a natural, conversational narration script for a YouTube Shorts video.
|
||||
Focus ONLY on what the listener needs to hear - no technical details, scene descriptions, or timing markers.
|
||||
|
||||
Content Context:
|
||||
{script}
|
||||
|
||||
Requirements:
|
||||
1. Length: {target_duration} seconds (approximately {target_words} words)
|
||||
2. Style: Natural, conversational, and engaging
|
||||
3. Structure:
|
||||
- Start with a hook
|
||||
- Present key points
|
||||
- End with a call to action
|
||||
4. Tone: {st.session_state.svgen_content_style.lower()}
|
||||
|
||||
Important Guidelines:
|
||||
- Write ONLY the spoken words - no descriptions, timing, or technical details
|
||||
- Use natural language that sounds good when spoken
|
||||
- Keep sentences short and clear
|
||||
- Add natural pauses with ellipsis (...)
|
||||
- No scene numbers, timing markers, or technical instructions
|
||||
- No sound effect descriptions or music cues
|
||||
- No formatting markers or special characters
|
||||
- Target word count: {target_words} words (±10%)
|
||||
- Speaking rate: {words_per_second} words per second
|
||||
|
||||
Example of good narration:
|
||||
"Writer's block got you down? Meet your new secret weapon: an AI content writer! This tool helps you write ten times faster. No more blank page terror! Blog posts, social media, even killer emails - all generated in seconds. Ready to unleash your content creation superpowers? Try it free today!"
|
||||
|
||||
Format the narration as a single, flowing script with natural pauses.
|
||||
"""
|
||||
|
||||
try:
|
||||
# Generate narration using LLM
|
||||
narration = llm_text_gen(narration_prompt)
|
||||
if narration:
|
||||
# Clean up the narration
|
||||
narration = re.sub(r'\s+', ' ', narration) # Remove extra spaces
|
||||
narration = re.sub(r'[^\w\s.,!?…-]', '', narration) # Keep only essential punctuation
|
||||
narration = re.sub(r'([.!?])\s+', r'\1\n\n', narration) # Add natural pauses
|
||||
narration = re.sub(r'\*\*.*?\*\*', '', narration) # Remove any markdown
|
||||
narration = re.sub(r'\(.*?\)', '', narration) # Remove any parenthetical notes
|
||||
narration = re.sub(r'\n\s*\n', '\n\n', narration) # Clean up extra line breaks
|
||||
|
||||
# Verify word count
|
||||
word_count = len(narration.split())
|
||||
if word_count < target_words * 0.9 or word_count > target_words * 1.1:
|
||||
print(f'[WARNING] Generated narration word count ({word_count}) is outside target range ({target_words}±10%)')
|
||||
|
||||
return narration.strip()
|
||||
except Exception as e:
|
||||
print(f'[ERROR] Failed to generate narration: {e}')
|
||||
return None
|
||||
|
||||
def write_yt_shorts_video():
|
||||
"""
|
||||
Main function to generate a YouTube Shorts video.
|
||||
This function provides a Streamlit interface for users to generate Shorts videos.
|
||||
"""
|
||||
st.markdown("""
|
||||
<style>
|
||||
.stepper {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
margin-bottom: 2rem;
|
||||
}
|
||||
.step {
|
||||
flex: 1;
|
||||
text-align: center;
|
||||
padding: 0.5rem 0;
|
||||
border-bottom: 4px solid #e0e0e0;
|
||||
color: #888;
|
||||
font-weight: 600;
|
||||
font-size: 1.1rem;
|
||||
}
|
||||
.step.active {
|
||||
color: #2563eb;
|
||||
border-bottom: 4px solid #2563eb;
|
||||
background: #f0f6ff;
|
||||
border-radius: 8px 8px 0 0;
|
||||
}
|
||||
.card {
|
||||
background: #f8fafc;
|
||||
border-radius: 12px;
|
||||
box-shadow: 0 2px 8px rgba(0,0,0,0.04);
|
||||
padding: 2rem 2rem 1.5rem 2rem;
|
||||
margin-bottom: 2rem;
|
||||
}
|
||||
.section-title {
|
||||
font-size: 1.3rem;
|
||||
font-weight: 700;
|
||||
margin-bottom: 1rem;
|
||||
color: #222;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
.section-title svg {
|
||||
margin-right: 0.5rem;
|
||||
}
|
||||
.primary-btn {
|
||||
background: #2563eb;
|
||||
color: #fff;
|
||||
border-radius: 8px;
|
||||
font-size: 1.1rem;
|
||||
font-weight: 600;
|
||||
padding: 0.75rem 2.5rem;
|
||||
border: none;
|
||||
margin-top: 1.5rem;
|
||||
margin-bottom: 0.5rem;
|
||||
box-shadow: 0 2px 8px rgba(37,99,235,0.08);
|
||||
}
|
||||
</style>
|
||||
""", unsafe_allow_html=True)
|
||||
|
||||
# Stepper logic
|
||||
if 'shorts_stage' not in st.session_state:
|
||||
st.session_state.shorts_stage = 1
|
||||
if 'generated_script' not in st.session_state:
|
||||
st.session_state.generated_script = None
|
||||
if 'script_approved' not in st.session_state:
|
||||
st.session_state.script_approved = False
|
||||
|
||||
# Stepper UI
|
||||
st.markdown(f'''
|
||||
<div class="stepper">
|
||||
<div class="step {'active' if st.session_state.shorts_stage == 1 else ''}">1. Input Details</div>
|
||||
<div class="step {'active' if st.session_state.shorts_stage == 2 else ''}">2. Script Review</div>
|
||||
<div class="step {'active' if st.session_state.shorts_stage == 3 else ''}">3. Video Generation</div>
|
||||
</div>
|
||||
''', unsafe_allow_html=True)
|
||||
|
||||
# --- Stage 1: Input Details ---
|
||||
if st.session_state.shorts_stage == 1:
|
||||
print('[DEBUG] Stage 1: Input Details loaded')
|
||||
st.markdown('---')
|
||||
st.markdown('### 1️⃣ Input Video Details')
|
||||
st.info("Fill in all details below, then click **Generate Script** to continue.")
|
||||
with st.container():
|
||||
st.markdown('<div class="card">', unsafe_allow_html=True)
|
||||
st.markdown('<div class="section-title">📝 Video Content</div>', unsafe_allow_html=True)
|
||||
video_topic = st.text_input(
|
||||
"What's your video about?",
|
||||
placeholder="Enter the main topic or theme of your Shorts video",
|
||||
help="Be specific about what you want to create"
|
||||
)
|
||||
style_col, duration_col = st.columns(2)
|
||||
with style_col:
|
||||
content_style = st.selectbox(
|
||||
"Content Style",
|
||||
list(CONTENT_STYLES.keys()),
|
||||
help="Select the style that best fits your content"
|
||||
)
|
||||
with duration_col:
|
||||
video_duration = st.slider(
|
||||
"Duration (seconds)",
|
||||
MIN_SHORTS_DURATION,
|
||||
MAX_SHORTS_DURATION,
|
||||
DEFAULT_SHORTS_DURATION,
|
||||
help=f"Shorts must be between {MIN_SHORTS_DURATION} and {MAX_SHORTS_DURATION} seconds"
|
||||
)
|
||||
|
||||
# Calculate and display scene count based on duration
|
||||
scene_duration = DEFAULT_DURATION # seconds per scene
|
||||
max_possible_scenes = min(MAX_SCENES, int(video_duration / scene_duration))
|
||||
min_possible_scenes = max(MIN_SCENES, int(video_duration / (scene_duration * 2)))
|
||||
|
||||
scene_count = st.slider(
|
||||
"Number of Scenes",
|
||||
min_possible_scenes,
|
||||
max_possible_scenes,
|
||||
min(max_possible_scenes, 10), # Default to 10 or max possible
|
||||
help=f"Based on {scene_duration}s per scene, you can have {min_possible_scenes}-{max_possible_scenes} scenes"
|
||||
)
|
||||
st.markdown('</div>', unsafe_allow_html=True)
|
||||
|
||||
with st.container():
|
||||
settings_col = st.columns(1)[0]
|
||||
with settings_col:
|
||||
with st.expander("⚙️ Video Settings", expanded=True):
|
||||
res_col, trans_col = st.columns(2)
|
||||
with res_col:
|
||||
resolution = st.selectbox(
|
||||
"Resolution",
|
||||
list(VIDEO_RESOLUTIONS.keys()),
|
||||
help="Higher resolution = better quality but longer processing time"
|
||||
)
|
||||
with trans_col:
|
||||
transition_style = st.selectbox(
|
||||
"Transition Style",
|
||||
list(TRANSITION_STYLES.keys()),
|
||||
help="How scenes transition between each other"
|
||||
)
|
||||
|
||||
# Add timing controls
|
||||
st.markdown("---")
|
||||
st.markdown("#### ⏱️ Timing Settings")
|
||||
|
||||
# Scene timing controls
|
||||
timing_col1, timing_col2 = st.columns(2)
|
||||
with timing_col1:
|
||||
scene_duration = st.slider(
|
||||
"Seconds per Scene",
|
||||
min_value=1.0,
|
||||
max_value=5.0,
|
||||
value=DEFAULT_DURATION,
|
||||
step=0.5,
|
||||
help="How long each scene should be displayed"
|
||||
)
|
||||
st.session_state.svgen_scene_duration = scene_duration
|
||||
|
||||
with timing_col2:
|
||||
transition_duration = st.slider(
|
||||
"Transition Duration (seconds)",
|
||||
min_value=0.1,
|
||||
max_value=1.0,
|
||||
value=DEFAULT_TRANSITION_DURATION,
|
||||
step=0.1,
|
||||
help="Duration of transitions between scenes"
|
||||
)
|
||||
st.session_state.svgen_transition_duration = transition_duration
|
||||
|
||||
# Narration timing controls
|
||||
narr_timing_col1, narr_timing_col2 = st.columns(2)
|
||||
with narr_timing_col1:
|
||||
words_per_second = st.slider(
|
||||
"Speaking Rate (words/second)",
|
||||
min_value=1.5,
|
||||
max_value=3.5,
|
||||
value=WORDS_PER_SECOND,
|
||||
step=0.1,
|
||||
help="Adjust narration speed (default: 2.5 words/second)"
|
||||
)
|
||||
st.session_state.svgen_words_per_second = words_per_second
|
||||
|
||||
with narr_timing_col2:
|
||||
narration_padding = st.slider(
|
||||
"Narration Padding (seconds)",
|
||||
min_value=0.0,
|
||||
max_value=2.0,
|
||||
value=0.5,
|
||||
step=0.1,
|
||||
help="Extra time to add to narration duration"
|
||||
)
|
||||
st.session_state.svgen_narration_padding = narration_padding
|
||||
|
||||
# Calculate and display timing information
|
||||
total_scene_time = scene_duration * scene_count
|
||||
total_transition_time = transition_duration * (scene_count - 1)
|
||||
total_video_time = total_scene_time + total_transition_time
|
||||
|
||||
st.info(f"""
|
||||
**Timing Summary:**
|
||||
- Total Scene Time: {total_scene_time:.1f}s
|
||||
- Total Transition Time: {total_transition_time:.1f}s
|
||||
- Estimated Video Duration: {total_video_time:.1f}s
|
||||
- Target Narration Length: {int(total_video_time * words_per_second)} words
|
||||
""")
|
||||
with st.expander("🎙️ Narration Settings", expanded=True):
|
||||
narr_col1, narr_col2 = st.columns(2)
|
||||
with narr_col1:
|
||||
narration_language = st.selectbox(
|
||||
"Language",
|
||||
list(NARRATION_LANGUAGES.keys()),
|
||||
help="Select the language for narration"
|
||||
)
|
||||
with narr_col2:
|
||||
include_music = st.checkbox(
|
||||
"Include Background Music",
|
||||
value=True,
|
||||
help="Add background music to enhance the video"
|
||||
)
|
||||
st.markdown('---')
|
||||
can_generate_script = bool(video_topic and content_style and video_duration and resolution and narration_language)
|
||||
if st.button("📝 Generate Script", key="generate_script_btn", help="Generate a script for your Shorts video", use_container_width=True, disabled=not can_generate_script):
|
||||
print(f'[DEBUG] Generate Script button clicked. Topic: {video_topic}, Style: {content_style}, Duration: {video_duration}, Resolution: {resolution}, Language: {narration_language}')
|
||||
try:
|
||||
with st.spinner("Generating script..."):
|
||||
script = generate_shorts_script(
|
||||
hook_type="Question",
|
||||
main_topic=video_topic,
|
||||
target_audience="general",
|
||||
tone_style=content_style,
|
||||
content_type=CONTENT_STYLES[content_style]["style"],
|
||||
duration_seconds=video_duration,
|
||||
include_captions=True,
|
||||
include_text_overlay=True,
|
||||
include_sound_effects=True,
|
||||
vertical_framing_notes=True,
|
||||
language=narration_language
|
||||
)
|
||||
print(f'[DEBUG] Script generated: {bool(script)}')
|
||||
if script:
|
||||
st.session_state.generated_script = script
|
||||
st.session_state.script_approved = False
|
||||
st.session_state.shorts_stage = 2
|
||||
st.session_state.svgen_resolution = resolution
|
||||
st.session_state.svgen_transition_style = transition_style
|
||||
st.session_state.svgen_narration_language = narration_language
|
||||
st.session_state.svgen_include_music = include_music
|
||||
st.session_state.svgen_content_style = content_style
|
||||
st.session_state.svgen_video_duration = video_duration
|
||||
st.session_state.svgen_video_topic = video_topic
|
||||
print('[DEBUG] Script saved to session state and moving to Stage 2')
|
||||
st.success("Script generated! Review and edit below.")
|
||||
else:
|
||||
print('[ERROR] Script generation failed')
|
||||
st.error("Failed to generate script. Please try again.")
|
||||
except Exception as e:
|
||||
print(f'[ERROR] Exception during script generation: {e}')
|
||||
st.error(f"An error occurred while generating the script: {str(e)}")
|
||||
logger.error(f"Error in script generation: {str(e)}")
|
||||
logger.error(traceback.format_exc())
|
||||
if not can_generate_script:
|
||||
st.warning("Please fill in all required fields above to enable script generation.")
|
||||
st.markdown('---')
|
||||
st.info("Next: Review and edit your script.")
|
||||
|
||||
# --- Stage 2: Script Review & Edit ---
|
||||
if st.session_state.shorts_stage == 2:
|
||||
print('[DEBUG] Stage 2: Script Review & Edit loaded')
|
||||
st.markdown('---')
|
||||
st.markdown('### 2️⃣ Script Review & Edit')
|
||||
st.info("Review your generated script. Use the Edit tab to make changes. Approve to continue.")
|
||||
st.markdown('<div class="card">', unsafe_allow_html=True)
|
||||
st.markdown('<div class="section-title">📄 Script Preview & Edit</div>', unsafe_allow_html=True)
|
||||
preview_tab, edit_tab = st.tabs(["Preview", "Edit"])
|
||||
with preview_tab:
|
||||
st.markdown(st.session_state.generated_script)
|
||||
if not st.session_state.script_approved:
|
||||
if st.button("✅ Approve Script", key="approve_script_btn", use_container_width=True):
|
||||
st.session_state.script_approved = True
|
||||
print('[DEBUG] Script approved by user')
|
||||
st.success("Script approved! You can now generate your video.")
|
||||
with edit_tab:
|
||||
edited_script = st.text_area(
|
||||
"Edit Script",
|
||||
value=st.session_state.generated_script,
|
||||
height=400,
|
||||
help="Make any necessary changes to the script. The format should be maintained."
|
||||
)
|
||||
if edited_script != st.session_state.generated_script:
|
||||
print('[DEBUG] Script edited by user')
|
||||
st.session_state.generated_script = edited_script
|
||||
st.session_state.script_approved = False
|
||||
st.info("Script updated. Please review and approve the changes.")
|
||||
st.markdown('</div>', unsafe_allow_html=True)
|
||||
st.markdown('---')
|
||||
st.button("⬅️ Back to Details", key="back_to_details_btn", use_container_width=True, on_click=lambda: st.session_state.update({'shorts_stage': 1}))
|
||||
if st.session_state.script_approved:
|
||||
st.success("Script approved! You can now generate your video.")
|
||||
st.button("🎬 Proceed to Video Generation", key="proceed_to_video_btn", use_container_width=True, on_click=lambda: st.session_state.update({'shorts_stage': 3}))
|
||||
else:
|
||||
st.warning("Please approve your script before proceeding.")
|
||||
st.markdown('---')
|
||||
st.info("Next: Review and edit narration, then generate your video.")
|
||||
|
||||
# --- Stage 3: Video Generation ---
|
||||
if st.session_state.shorts_stage == 3:
|
||||
print('[DEBUG] Stage 3: Narration & Video Generation loaded')
|
||||
st.markdown('---')
|
||||
st.markdown('### 3️⃣ Narration & Video Generation')
|
||||
st.info("Edit or generate narration, preview audio, then click **Generate Video**.")
|
||||
st.markdown('<div class="card">', unsafe_allow_html=True)
|
||||
st.markdown('<div class="section-title">🗣️ Narration for Review & Edit</div>', unsafe_allow_html=True)
|
||||
narr_col1, narr_col2 = st.columns([4, 1])
|
||||
with narr_col1:
|
||||
if 'editable_narration' not in st.session_state:
|
||||
st.session_state.editable_narration = generate_shorts_narration(
|
||||
st.session_state.generated_script,
|
||||
language=st.session_state.svgen_narration_language,
|
||||
target_duration=st.session_state.svgen_video_duration
|
||||
)
|
||||
print('[DEBUG] Initial narration generated')
|
||||
|
||||
edited_narration = st.text_area(
|
||||
"Edit narration to be used for TTS:",
|
||||
value=st.session_state.editable_narration,
|
||||
height=120,
|
||||
key="editable_narration_area",
|
||||
help="Edit the narration to sound natural when spoken. No technical details needed."
|
||||
)
|
||||
st.session_state.editable_narration = edited_narration
|
||||
|
||||
# Calculate and display timing information
|
||||
narration_word_count = len(edited_narration.split())
|
||||
words_per_second = 2.5 # Standard speaking rate
|
||||
estimated_duration = narration_word_count / words_per_second
|
||||
|
||||
narration_stats = (
|
||||
f"Words: {narration_word_count} | "
|
||||
f"Est. duration: {estimated_duration:.1f}s | "
|
||||
f"Target: {st.session_state.svgen_video_duration}s"
|
||||
)
|
||||
st.caption(narration_stats)
|
||||
|
||||
# Display timing warnings
|
||||
if estimated_duration < 20:
|
||||
st.warning("⚠️ Narration is too short for a 30-second video. Consider generating a new narration.")
|
||||
elif estimated_duration > 35:
|
||||
st.warning("⚠️ Narration is too long for a 30-second video. Consider generating a new narration.")
|
||||
|
||||
# Narration Tips in an expander
|
||||
with st.expander("💡 Narration Tips", expanded=False):
|
||||
st.markdown("""
|
||||
### Tips for Natural Narration
|
||||
|
||||
- Write only what should be spoken
|
||||
- Keep it conversational and clear
|
||||
- Use natural pauses (...)
|
||||
- Focus on the message, not the technical details
|
||||
- End with a clear call to action
|
||||
""")
|
||||
|
||||
tts_col1, tts_col2 = st.columns(2)
|
||||
with tts_col1:
|
||||
tts_gender = st.selectbox("Voice Gender (affects some TTS engines)", ["Default", "Female", "Male"], key="tts_gender_select")
|
||||
with tts_col2:
|
||||
tts_speed = st.selectbox("Speech Speed", ["Normal", "Slow"], key="tts_speed_select")
|
||||
if st.button("🔊 Preview Narration Audio", key="preview_tts_btn"):
|
||||
print('[DEBUG] TTS preview button clicked')
|
||||
try:
|
||||
tts_kwargs = {"lang": NARRATION_LANGUAGES[st.session_state.svgen_narration_language]}
|
||||
tts_kwargs["slow"] = tts_speed == "Slow"
|
||||
tts = gTTS(text=edited_narration, **tts_kwargs)
|
||||
preview_audio_path = os.path.join(tempfile.gettempdir(), f"tts_preview_{os.getpid()}.mp3")
|
||||
tts.save(preview_audio_path)
|
||||
with open(preview_audio_path, "rb") as audio_file:
|
||||
audio_bytes = audio_file.read()
|
||||
st.audio(audio_bytes, format="audio/mp3")
|
||||
print('[DEBUG] TTS preview audio generated and played')
|
||||
except Exception as tts_err:
|
||||
print(f'[ERROR] Failed to generate TTS preview: {tts_err}')
|
||||
st.error(f"Failed to generate TTS preview: {tts_err}")
|
||||
if narration_word_count < 10:
|
||||
st.warning("Narration is very short. Consider adding more detail.")
|
||||
elif narration_word_count > 120:
|
||||
st.warning("Narration is quite long. Consider shortening for Shorts.")
|
||||
with narr_col2:
|
||||
if st.button("🔄 Generate New Narration", key="generate_narration_btn"):
|
||||
with st.spinner("Generating engaging narration..."):
|
||||
new_narration = generate_shorts_narration(
|
||||
st.session_state.generated_script,
|
||||
language=st.session_state.svgen_narration_language,
|
||||
target_duration=st.session_state.svgen_video_duration
|
||||
)
|
||||
if new_narration:
|
||||
st.session_state.editable_narration = new_narration
|
||||
print('[DEBUG] New narration generated')
|
||||
st.success("New narration generated successfully!")
|
||||
st.rerun()
|
||||
else:
|
||||
st.error("Failed to generate narration. Please try again.")
|
||||
|
||||
if st.button("🤖 Generate AI Narration", key="ai_narration_btn"):
|
||||
with st.spinner("Generating AI-optimized narration..."):
|
||||
ai_narr = generate_shorts_narration(
|
||||
st.session_state.generated_script,
|
||||
language=st.session_state.svgen_narration_language,
|
||||
target_duration=st.session_state.svgen_video_duration
|
||||
)
|
||||
if ai_narr:
|
||||
st.session_state.editable_narration = ai_narr
|
||||
print('[DEBUG] AI-generated narration updated')
|
||||
st.success("AI-generated narration updated.")
|
||||
st.rerun()
|
||||
else:
|
||||
st.error("Failed to generate AI narration. Please try again.")
|
||||
st.markdown('</div>', unsafe_allow_html=True)
|
||||
st.markdown('---')
|
||||
st.markdown('### 3️⃣ Video Generation')
|
||||
st.info("Click **Generate Video** to start the final process. This may take a few minutes.")
|
||||
st.markdown('<div class="card">', unsafe_allow_html=True)
|
||||
st.markdown('<div class="section-title"> Video Generation</div>', unsafe_allow_html=True)
|
||||
|
||||
# Video Information in an expander
|
||||
with st.expander("📋 Video Information", expanded=True):
|
||||
st.markdown("""
|
||||
### Video Details
|
||||
| Setting | Value |
|
||||
|---------|--------|
|
||||
| Video Topic | {} |
|
||||
| Content Style | {} |
|
||||
| Duration | {} seconds |
|
||||
| Resolution | {} |
|
||||
| Narration Language | {} |
|
||||
| Background Music | {} |
|
||||
""".format(
|
||||
st.session_state.svgen_video_topic,
|
||||
st.session_state.svgen_content_style,
|
||||
st.session_state.svgen_video_duration,
|
||||
st.session_state.svgen_resolution,
|
||||
st.session_state.svgen_narration_language,
|
||||
"Yes" if st.session_state.svgen_include_music else "No"
|
||||
))
|
||||
|
||||
st.markdown('</div>', unsafe_allow_html=True)
|
||||
st.markdown('<div style="text-align:center">', unsafe_allow_html=True)
|
||||
st.button("⬅️ Back to Script Review", key="back_to_script_btn", use_container_width=True, on_click=lambda: st.session_state.update({'shorts_stage': 2}))
|
||||
if st.button("🚀 Generate Video", key="generate_video_btn", use_container_width=True):
|
||||
print('[DEBUG] Generate Video button clicked')
|
||||
try:
|
||||
with st.spinner("Generating your Shorts video..."):
|
||||
st.info("Step 1/3: Generating images...")
|
||||
image_paths = []
|
||||
temp_dir = Path(tempfile.mkdtemp())
|
||||
# Filter out empty scenes and limit to MAX_SCENES
|
||||
scenes = [s.strip() for s in st.session_state.generated_script.split("\n\n") if s.strip()][:MAX_SCENES]
|
||||
resolution = st.session_state.svgen_resolution
|
||||
narration_language = st.session_state.svgen_narration_language
|
||||
scene_count = 0
|
||||
num_scenes_total = len(scenes)
|
||||
progress_bar = st.progress(0.0)
|
||||
status_text = st.empty()
|
||||
|
||||
# Initialize or load image cache
|
||||
if 'generated_image_paths' not in st.session_state:
|
||||
st.session_state.generated_image_paths = {}
|
||||
generated_image_paths = st.session_state.generated_image_paths
|
||||
|
||||
# Clear any invalid cache entries
|
||||
generated_image_paths = {k: v for k, v in generated_image_paths.items()
|
||||
if os.path.exists(v) and k < num_scenes_total}
|
||||
st.session_state.generated_image_paths = generated_image_paths
|
||||
|
||||
preview_container = st.container()
|
||||
preview_thumbnails = []
|
||||
|
||||
def retry_on_error(max_retries=3, initial_delay=1, max_delay=10):
|
||||
def decorator(func):
|
||||
@functools.wraps(func)
|
||||
def wrapper(*args, **kwargs):
|
||||
delay = initial_delay
|
||||
for attempt in range(max_retries):
|
||||
try:
|
||||
return func(*args, **kwargs)
|
||||
except Exception as e:
|
||||
if attempt == max_retries - 1:
|
||||
raise
|
||||
print(f'[WARN] Retry {attempt+1}/{max_retries} for image generation: {e}')
|
||||
time.sleep(delay)
|
||||
delay = min(delay * 2, max_delay)
|
||||
return None
|
||||
return wrapper
|
||||
return decorator
|
||||
|
||||
@retry_on_error(max_retries=3, initial_delay=2, max_delay=10)
|
||||
def safe_generate_image(prompt):
|
||||
return generate_image(prompt)
|
||||
|
||||
for i, scene in enumerate(scenes):
|
||||
print(f'[DEBUG] Processing scene {i+1}/{num_scenes_total}')
|
||||
status_text.text(f"Generating image for scene {i+1}/{num_scenes_total}...")
|
||||
|
||||
# Check cache first
|
||||
if i in generated_image_paths:
|
||||
image_paths.append(generated_image_paths[i])
|
||||
preview_thumbnails.append((generated_image_paths[i], i+1))
|
||||
print(f'[DEBUG] Using cached image for scene {i+1}')
|
||||
scene_count += 1
|
||||
progress_bar.progress(scene_count / num_scenes_total)
|
||||
continue
|
||||
|
||||
# Extract details for a more specific prompt
|
||||
visual_desc = scene.split("Visual Instructions:")[1].split("\n")[0] if "Visual Instructions:" in scene else scene
|
||||
narration_match = re.search(r'Audio/Voiceover:\s*(.*)', scene)
|
||||
narration_line = narration_match.group(1).strip() if narration_match else ""
|
||||
|
||||
# Enhanced prompt with more specific details and style guidance
|
||||
prompt = (
|
||||
f"Create a vertical (9:16) image for YouTube Shorts video.\n"
|
||||
f"Scene {i+1} of {num_scenes_total}:\n"
|
||||
f"Visual Description: {visual_desc}\n"
|
||||
f"Context: {narration_line}\n"
|
||||
f"Style Requirements:\n"
|
||||
f"- High contrast and vibrant colors for better mobile viewing\n"
|
||||
f"- Clear focal point in the center for vertical format\n"
|
||||
f"- Professional quality, cinematic lighting\n"
|
||||
f"- Text-safe areas on top and bottom\n"
|
||||
f"- Visually distinct from other scenes\n"
|
||||
f"- Modern, engaging composition\n"
|
||||
f"- Suitable for {st.session_state.svgen_content_style} style content\n"
|
||||
f"Technical Requirements:\n"
|
||||
f"- Vertical 9:16 aspect ratio\n"
|
||||
f"- High resolution, sharp details\n"
|
||||
f"- No text or watermarks\n"
|
||||
f"- No blurry or low-quality elements"
|
||||
)
|
||||
|
||||
try:
|
||||
image_path = safe_generate_image(prompt)
|
||||
if image_path:
|
||||
img = Image.open(image_path)
|
||||
target_size = VIDEO_RESOLUTIONS[resolution]
|
||||
img = img.resize(target_size, Image.LANCZOS)
|
||||
resized_path = temp_dir / f"scene_{i}.png"
|
||||
img.save(resized_path)
|
||||
image_paths.append(str(resized_path))
|
||||
generated_image_paths[i] = str(resized_path)
|
||||
st.session_state.generated_image_paths = generated_image_paths
|
||||
preview_thumbnails.append((str(resized_path), i+1))
|
||||
print(f'[DEBUG] Generated and cached new image for scene {i+1}')
|
||||
else:
|
||||
print(f'[ERROR] Image generation failed for scene {i+1}')
|
||||
st.warning(f"Image generation failed for scene {i+1}. Skipping.")
|
||||
except Exception as img_err:
|
||||
print(f'[ERROR] Exception during image generation for scene {i+1}: {img_err}')
|
||||
st.warning(f"Error generating image for scene {i+1}: {img_err}")
|
||||
|
||||
scene_count += 1
|
||||
progress_bar.progress(scene_count / num_scenes_total)
|
||||
|
||||
# Update preview after each image
|
||||
with preview_container:
|
||||
preview_container.empty() # Clear previous preview
|
||||
if preview_thumbnails:
|
||||
# Create a grid layout with 5 columns
|
||||
cols = st.columns(5)
|
||||
|
||||
# Display thumbnails in a grid
|
||||
for idx, (img_path, sc_num) in enumerate(preview_thumbnails):
|
||||
with cols[idx % 5]:
|
||||
# Create a smaller thumbnail
|
||||
img = Image.open(img_path)
|
||||
# Calculate aspect ratio to maintain 9:16
|
||||
target_width = 100 # Smaller width
|
||||
target_height = int(target_width * (16/9))
|
||||
img = img.resize((target_width, target_height), Image.LANCZOS)
|
||||
|
||||
# Display with a compact caption
|
||||
st.image(
|
||||
img,
|
||||
caption=f"Scene {sc_num}",
|
||||
use_column_width=True,
|
||||
key=f"preview_{sc_num}" # Add unique key for each image
|
||||
)
|
||||
|
||||
# Add a small progress indicator
|
||||
if idx == len(preview_thumbnails) - 1:
|
||||
st.caption(f"Generating scene {scene_count + 1}...")
|
||||
|
||||
# Add a clear divider between preview and next steps
|
||||
st.markdown("---")
|
||||
status_text.text("Image generation complete!")
|
||||
print(f'[DEBUG] Image generation complete. Total images: {len(image_paths)}')
|
||||
if not image_paths:
|
||||
print('[ERROR] No images generated')
|
||||
st.error("Failed to generate images. Please try again.")
|
||||
return
|
||||
st.info("Step 2/3: Generating narration...")
|
||||
narration_path = temp_dir / "narration.mp3"
|
||||
narration_text = st.session_state.editable_narration
|
||||
try:
|
||||
tts = gTTS(text=narration_text, lang=NARRATION_LANGUAGES[narration_language])
|
||||
tts.save(str(narration_path))
|
||||
print('[DEBUG] Narration audio generated and saved')
|
||||
|
||||
# Verify the audio file was created and is valid
|
||||
if not os.path.exists(str(narration_path)):
|
||||
raise Exception("Narration audio file was not created")
|
||||
|
||||
# Test the audio file by loading it
|
||||
test_audio = AudioFileClip(str(narration_path))
|
||||
if test_audio.duration <= 0:
|
||||
raise Exception("Generated audio file is invalid or empty")
|
||||
test_audio.close()
|
||||
|
||||
except Exception as tts_err:
|
||||
print(f'[ERROR] Failed to generate narration: {tts_err}')
|
||||
st.error(f"Failed to generate narration: {tts_err}")
|
||||
return
|
||||
|
||||
st.info("Step 3/3: Creating video...")
|
||||
video_generator = StoryVideoGenerator()
|
||||
try:
|
||||
# Verify audio file exists before video creation
|
||||
if not os.path.exists(str(narration_path)):
|
||||
raise Exception("Narration audio file not found")
|
||||
|
||||
video_path = video_generator.create_video(
|
||||
image_paths=image_paths,
|
||||
audio_path=str(narration_path),
|
||||
fps=DEFAULT_FPS,
|
||||
duration_per_image=getattr(st.session_state, 'svgen_scene_duration', DEFAULT_DURATION)
|
||||
)
|
||||
if video_path and os.path.exists(video_path):
|
||||
print(f'[DEBUG] Video generated at {video_path}')
|
||||
st.success("✨ Video generated successfully! Preview below and download your video.")
|
||||
st.video(video_path)
|
||||
safe_topic = re.sub(r'[^\w\-]+', '_', st.session_state.svgen_video_topic)
|
||||
download_filename = f"{safe_topic}_shorts_video.mp4"
|
||||
with open(video_path, "rb") as f:
|
||||
video_bytes = f.read()
|
||||
st.download_button(
|
||||
label="⬇️ Download Video",
|
||||
data=video_bytes,
|
||||
file_name=download_filename,
|
||||
mime="video/mp4"
|
||||
)
|
||||
else:
|
||||
print('[ERROR] Video file not found after generation')
|
||||
st.error("Failed to create video. Please try again.")
|
||||
except Exception as vid_err:
|
||||
print(f'[ERROR] Exception during video creation: {vid_err}')
|
||||
st.error(f"An error occurred while creating the video: {vid_err}")
|
||||
logger.error(f"Error in video generation: {vid_err}")
|
||||
logger.error(traceback.format_exc())
|
||||
except Exception as e:
|
||||
print(f'[ERROR] Exception during full video generation: {e}')
|
||||
st.error(f"An error occurred while generating the video: {str(e)}")
|
||||
logger.error(f"Error in video generation: {str(e)}")
|
||||
logger.error(traceback.format_exc())
|
||||
st.markdown('</div>', unsafe_allow_html=True)
|
||||
st.markdown('---')
|
||||
st.info("All done! You can download your video above or go back to make changes.")
|
||||
@@ -0,0 +1,406 @@
|
||||
"""
|
||||
YouTube Tags Generator Module
|
||||
|
||||
This module provides functionality for generating and optimizing YouTube video tags.
|
||||
"""
|
||||
|
||||
import streamlit as st
|
||||
import time
|
||||
import logging
|
||||
from lib.gpt_providers.text_generation.main_text_generation import llm_text_gen
|
||||
from pytrends.request import TrendReq
|
||||
import pandas as pd
|
||||
|
||||
# Configure logging
|
||||
logging.basicConfig(
|
||||
level=logging.INFO,
|
||||
format='%(asctime)s - %(name)s - %(levelname)s - %(message)s'
|
||||
)
|
||||
logger = logging.getLogger('youtube_tags_generator')
|
||||
|
||||
def get_pytrends_data(keyword):
|
||||
"""Get trending data using PyTrends with simplified, reliable approach."""
|
||||
logger.info(f"Getting PyTrends data for: '{keyword}'")
|
||||
|
||||
# Initialize empty results
|
||||
results = {
|
||||
'topics': [],
|
||||
'queries': [],
|
||||
'trending': []
|
||||
}
|
||||
|
||||
try:
|
||||
# Initialize PyTrends with minimal configuration
|
||||
pytrends = TrendReq(hl='en-US', tz=360)
|
||||
time.sleep(1) # Basic rate limiting
|
||||
|
||||
# 1. Get suggestions (most reliable method)
|
||||
try:
|
||||
suggestions = pytrends.suggestions(keyword)
|
||||
if suggestions:
|
||||
results['trending'] = [sugg['title'] for sugg in suggestions if sugg['title']][:3]
|
||||
except Exception as e:
|
||||
logger.warning(f"Error getting suggestions: {str(e)}")
|
||||
|
||||
# 2. Get trending searches as backup
|
||||
if not results['trending']:
|
||||
try:
|
||||
trending = pytrends.trending_searches(pn='united_states')
|
||||
if not trending.empty:
|
||||
results['trending'] = trending.head(3).values.tolist()
|
||||
except Exception as e:
|
||||
logger.warning(f"Error getting trending searches: {str(e)}")
|
||||
|
||||
# 3. Use keyword variations as fallback
|
||||
if not any(results.values()):
|
||||
results['trending'] = [keyword]
|
||||
results['queries'] = [keyword.lower(), keyword.title()]
|
||||
results['topics'] = [keyword.capitalize()]
|
||||
|
||||
return results
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error in PyTrends: {str(e)}")
|
||||
# Return basic keyword variations as fallback
|
||||
return {
|
||||
'topics': [keyword.capitalize()],
|
||||
'queries': [keyword.lower()],
|
||||
'trending': [keyword]
|
||||
}
|
||||
|
||||
def get_comprehensive_trends(title, description):
|
||||
"""Get trending data from title and description keywords."""
|
||||
logger.info(f"Getting comprehensive trends for title: '{title}'")
|
||||
|
||||
# Extract main keywords (only words longer than 3 chars)
|
||||
words = [w for w in title.split() if len(w) > 3]
|
||||
if description:
|
||||
desc_words = [w for w in description.split() if len(w) > 3]
|
||||
words.extend(desc_words)
|
||||
|
||||
# Remove duplicates and limit to 2 keywords to prevent rate limiting
|
||||
keywords = list(dict.fromkeys(words))[:2]
|
||||
|
||||
# Get trending data for main keywords
|
||||
all_trends = {
|
||||
'topics': [],
|
||||
'queries': [],
|
||||
'trending': []
|
||||
}
|
||||
|
||||
for keyword in keywords:
|
||||
try:
|
||||
trends = get_pytrends_data(keyword)
|
||||
for key in all_trends:
|
||||
if trends[key]:
|
||||
all_trends[key].extend(trends[key])
|
||||
time.sleep(1) # Rate limiting between keywords
|
||||
except Exception as e:
|
||||
logger.warning(f"Error getting trends for keyword '{keyword}': {str(e)}")
|
||||
continue
|
||||
|
||||
# Remove duplicates while preserving order
|
||||
for key in all_trends:
|
||||
seen = set()
|
||||
all_trends[key] = [x for x in all_trends[key] if x and not (x.lower() in seen or seen.add(x.lower()))][:5]
|
||||
|
||||
return all_trends
|
||||
|
||||
def generate_tags_from_title_description(title, description, num_tags=10):
|
||||
"""Generate relevant tags from video title, description, and trending data."""
|
||||
logger.info(f"Generating tags for title: '{title}'")
|
||||
|
||||
# Get comprehensive trending data
|
||||
trends = get_comprehensive_trends(title, description)
|
||||
|
||||
# Create a comprehensive context for GPT
|
||||
trend_context = f"""
|
||||
Related Topics: {', '.join(trends['topics'][:10])}
|
||||
Related Queries: {', '.join(trends['queries'][:10])}
|
||||
Trending Suggestions: {', '.join(trends['trending'][:10])}
|
||||
"""
|
||||
|
||||
system_prompt = """You are a YouTube SEO expert specializing in tag optimization.
|
||||
Generate relevant, searchable tags based on the video title, description, and trending data provided.
|
||||
Focus on a mix of specific and broad tags that will help with video discovery.
|
||||
Consider the trending topics and queries provided to maximize searchability.
|
||||
Return only the tags, separated by commas."""
|
||||
|
||||
user_prompt = f"""Generate {num_tags} relevant YouTube tags for a video with:
|
||||
Title: {title}
|
||||
Description: {description}
|
||||
|
||||
Consider this trending data:
|
||||
{trend_context}
|
||||
|
||||
Include a mix of:
|
||||
- Exact match phrases from title and description
|
||||
- Related trending topics and queries
|
||||
- Broader category tags
|
||||
- Specific niche tags
|
||||
- Popular search variations
|
||||
|
||||
Format: Return only the tags, separated by commas."""
|
||||
|
||||
try:
|
||||
tags = llm_text_gen(user_prompt, system_prompt=system_prompt)
|
||||
generated_tags = [tag.strip() for tag in tags.split(',')]
|
||||
|
||||
# Add some trending tags directly
|
||||
trending_tags = (
|
||||
trends['topics'][:3] + # Top 3 related topics
|
||||
trends['queries'][:3] + # Top 3 related queries
|
||||
trends['trending'][:3] # Top 3 trending suggestions
|
||||
)
|
||||
|
||||
# Combine and remove duplicates
|
||||
all_tags = generated_tags + trending_tags
|
||||
seen = set()
|
||||
final_tags = [tag for tag in all_tags if not (tag.lower() in seen or seen.add(tag.lower()))]
|
||||
|
||||
return final_tags
|
||||
except Exception as e:
|
||||
logger.error(f"Error generating tags: {str(e)}")
|
||||
return []
|
||||
|
||||
def analyze_tags(tags):
|
||||
"""Analyze tags for optimization opportunities."""
|
||||
analysis = {
|
||||
'total_tags': len(tags),
|
||||
'total_characters': sum(len(tag) for tag in tags),
|
||||
'avg_tag_length': sum(len(tag) for tag in tags) / len(tags) if tags else 0,
|
||||
'duplicate_tags': len(tags) - len(set(tags)),
|
||||
'tags_too_long': [tag for tag in tags if len(tag) > 30],
|
||||
'single_word_tags': [tag for tag in tags if len(tag.split()) == 1],
|
||||
'optimization_score': 0
|
||||
}
|
||||
|
||||
# Calculate optimization score (0-100)
|
||||
score = 100
|
||||
if analysis['total_tags'] < 5:
|
||||
score -= 30
|
||||
if analysis['total_characters'] > 500:
|
||||
score -= 20
|
||||
if analysis['duplicate_tags'] > 0:
|
||||
score -= 10 * analysis['duplicate_tags']
|
||||
if len(analysis['tags_too_long']) > 0:
|
||||
score -= 5 * len(analysis['tags_too_long'])
|
||||
if len(analysis['single_word_tags']) > len(tags) * 0.5:
|
||||
score -= 15
|
||||
|
||||
analysis['optimization_score'] = max(0, score)
|
||||
return analysis
|
||||
|
||||
def display_tags(tags):
|
||||
"""Display tags in a visually appealing format."""
|
||||
if not tags:
|
||||
return
|
||||
|
||||
# Create a container for all tags
|
||||
st.markdown("""
|
||||
<style>
|
||||
.tag-container {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 8px;
|
||||
margin-bottom: 16px;
|
||||
padding: 12px;
|
||||
background-color: #f8f9fa;
|
||||
border-radius: 8px;
|
||||
}
|
||||
.tag {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
background-color: #f0f2f6;
|
||||
border-radius: 16px;
|
||||
padding: 6px 12px;
|
||||
font-size: 13px;
|
||||
color: #2c3e50;
|
||||
border: 1px solid #e6e9ef;
|
||||
white-space: nowrap;
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
.tag:hover {
|
||||
background-color: #e6e9ef;
|
||||
border-color: #d1d5db;
|
||||
transform: translateY(-1px);
|
||||
}
|
||||
</style>
|
||||
<div class="tag-container">
|
||||
""", unsafe_allow_html=True)
|
||||
|
||||
# Display tags
|
||||
for tag in tags:
|
||||
st.markdown(f'<div class="tag">{tag}</div>', unsafe_allow_html=True)
|
||||
|
||||
st.markdown('</div>', unsafe_allow_html=True)
|
||||
|
||||
# Display tag count and character count
|
||||
tags_text = ", ".join(tags)
|
||||
char_count = len(tags_text)
|
||||
col1, col2 = st.columns(2)
|
||||
with col1:
|
||||
st.caption(f"Total tags: {len(tags)}")
|
||||
with col2:
|
||||
st.caption(f"Characters: {char_count}/500")
|
||||
|
||||
def write_yt_tags():
|
||||
"""Create a user interface for YouTube Tags Generator."""
|
||||
logger.info("Initializing YouTube Tags Generator UI")
|
||||
st.write("Generate optimized tags for your videos with trending tag suggestions to improve discoverability.")
|
||||
|
||||
# Initialize session state
|
||||
if "generated_tags" not in st.session_state:
|
||||
st.session_state.generated_tags = None
|
||||
if "tag_analysis" not in st.session_state:
|
||||
st.session_state.tag_analysis = None
|
||||
|
||||
# Create tabs for different sections
|
||||
tab1, tab2, tab3 = st.tabs(["Quick Generate", "Advanced Options", "Analysis"])
|
||||
|
||||
with tab1:
|
||||
# Basic information inputs
|
||||
title = st.text_input("Video Title",
|
||||
placeholder="Enter your video title")
|
||||
description = st.text_area("Video Description",
|
||||
placeholder="Enter your video description")
|
||||
|
||||
col1, col2 = st.columns(2)
|
||||
|
||||
with col1:
|
||||
num_tags = st.number_input("Number of Tags",
|
||||
min_value=5,
|
||||
max_value=30,
|
||||
value=15)
|
||||
|
||||
with col2:
|
||||
include_trending = st.checkbox("Include Trending Suggestions", value=True)
|
||||
|
||||
if st.button("Generate Tags"):
|
||||
if not title:
|
||||
st.error("Please enter a video title.")
|
||||
return
|
||||
|
||||
with st.spinner("Generating tags..."):
|
||||
# Generate tags using the comprehensive method
|
||||
tags = generate_tags_from_title_description(title, description, num_tags)
|
||||
|
||||
if tags:
|
||||
# Analyze tags
|
||||
st.session_state.tag_analysis = analyze_tags(tags)
|
||||
st.session_state.generated_tags = tags
|
||||
|
||||
# Display tags in the new format
|
||||
st.subheader("Generated Tags")
|
||||
display_tags(tags)
|
||||
|
||||
# Add copy button for all tags
|
||||
tags_text = ", ".join(tags)
|
||||
st.text_area("Tags (copy to use)", value=tags_text, height=100)
|
||||
|
||||
# Display character count
|
||||
char_count = len(tags_text)
|
||||
st.info(f"Total characters: {char_count}/500 ({500 - char_count} remaining)")
|
||||
|
||||
# Quick analysis summary
|
||||
col1, col2, col3 = st.columns(3)
|
||||
with col1:
|
||||
st.metric("Number of Tags", len(tags))
|
||||
with col2:
|
||||
st.metric("Optimization Score", f"{st.session_state.tag_analysis['optimization_score']}%")
|
||||
with col3:
|
||||
st.metric("Avg Tag Length", f"{st.session_state.tag_analysis['avg_tag_length']:.1f}")
|
||||
|
||||
# Display trending data summary if enabled
|
||||
if include_trending:
|
||||
st.subheader("Trending Data Used")
|
||||
trends = get_comprehensive_trends(title, description)
|
||||
|
||||
# Create columns for different trend types
|
||||
tcol1, tcol2, tcol3 = st.columns(3)
|
||||
|
||||
with tcol1:
|
||||
st.markdown("##### Related Topics")
|
||||
if trends['topics']:
|
||||
for topic in trends['topics'][:5]:
|
||||
st.markdown(f"• {topic}")
|
||||
else:
|
||||
st.markdown("*No related topics found*")
|
||||
|
||||
with tcol2:
|
||||
st.markdown("##### Related Queries")
|
||||
if trends['queries']:
|
||||
for query in trends['queries'][:5]:
|
||||
st.markdown(f"• {query}")
|
||||
else:
|
||||
st.markdown("*No related queries found*")
|
||||
|
||||
with tcol3:
|
||||
st.markdown("##### Trending Suggestions")
|
||||
if trends['trending']:
|
||||
for trend in trends['trending'][:5]:
|
||||
st.markdown(f"• {trend}")
|
||||
else:
|
||||
st.markdown("*No trending suggestions found*")
|
||||
else:
|
||||
st.error("Failed to generate tags. Please try again.")
|
||||
|
||||
with tab2:
|
||||
st.info("Advanced tag generation options coming soon!")
|
||||
st.markdown("""
|
||||
Future features will include:
|
||||
- Competitor tag analysis
|
||||
- Tag performance tracking
|
||||
- Category-specific tag suggestions
|
||||
- Multi-language tag generation
|
||||
- Tag sets management
|
||||
""")
|
||||
|
||||
with tab3:
|
||||
if st.session_state.tag_analysis:
|
||||
st.subheader("Tag Analysis")
|
||||
|
||||
# Create metrics
|
||||
col1, col2 = st.columns(2)
|
||||
|
||||
with col1:
|
||||
st.metric("Total Tags", st.session_state.tag_analysis['total_tags'])
|
||||
st.metric("Total Characters", st.session_state.tag_analysis['total_characters'])
|
||||
st.metric("Average Tag Length", f"{st.session_state.tag_analysis['avg_tag_length']:.1f}")
|
||||
|
||||
with col2:
|
||||
st.metric("Duplicate Tags", st.session_state.tag_analysis['duplicate_tags'])
|
||||
st.metric("Single Word Tags", len(st.session_state.tag_analysis['single_word_tags']))
|
||||
st.metric("Tags Too Long", len(st.session_state.tag_analysis['tags_too_long']))
|
||||
|
||||
# Optimization score with color
|
||||
score = st.session_state.tag_analysis['optimization_score']
|
||||
score_color = 'red' if score < 50 else 'orange' if score < 80 else 'green'
|
||||
st.markdown(f"""
|
||||
<div style='background-color: {score_color}; padding: 10px; border-radius: 5px; margin: 10px 0;'>
|
||||
<h3 style='color: white; margin: 0;'>Optimization Score: {score}%</h3>
|
||||
</div>
|
||||
""", unsafe_allow_html=True)
|
||||
|
||||
# Optimization suggestions
|
||||
st.subheader("Optimization Suggestions")
|
||||
suggestions = []
|
||||
|
||||
if st.session_state.tag_analysis['total_tags'] < 5:
|
||||
suggestions.append("❌ Add more tags (aim for at least 15)")
|
||||
if st.session_state.tag_analysis['total_characters'] > 500:
|
||||
suggestions.append("❌ Total character count exceeds limit (max 500)")
|
||||
if st.session_state.tag_analysis['duplicate_tags'] > 0:
|
||||
suggestions.append("❌ Remove duplicate tags")
|
||||
if len(st.session_state.tag_analysis['tags_too_long']) > 0:
|
||||
suggestions.append("❌ Some tags are too long (max 30 characters)")
|
||||
if len(st.session_state.tag_analysis['single_word_tags']) > st.session_state.tag_analysis['total_tags'] * 0.5:
|
||||
suggestions.append("❌ Too many single-word tags (use more specific phrases)")
|
||||
|
||||
if not suggestions:
|
||||
st.success("✅ Your tags are well-optimized!")
|
||||
else:
|
||||
for suggestion in suggestions:
|
||||
st.warning(suggestion)
|
||||
else:
|
||||
st.info("Generate tags first to see analysis")
|
||||
@@ -0,0 +1,622 @@
|
||||
"""
|
||||
YouTube Thumbnail Generator Module
|
||||
|
||||
This module provides functionality for generating YouTube video thumbnails.
|
||||
"""
|
||||
|
||||
import streamlit as st
|
||||
import time
|
||||
import logging
|
||||
import os
|
||||
import traceback
|
||||
from PIL import Image
|
||||
from lib.gpt_providers.text_generation.main_text_generation import llm_text_gen
|
||||
from lib.gpt_providers.text_to_image_generation.gen_gemini_images import generate_gemini_image, edit_image
|
||||
|
||||
# Configure logging
|
||||
logging.basicConfig(
|
||||
level=logging.INFO,
|
||||
format='%(asctime)s - %(name)s - %(levelname)s - %(message)s'
|
||||
)
|
||||
logger = logging.getLogger('youtube_thumbnail_generator')
|
||||
|
||||
|
||||
def generate_thumbnail_concepts(video_title, video_description, target_audience, content_type, style_preference, num_concepts=3):
|
||||
"""Generate thumbnail concept ideas based on video content."""
|
||||
logger.info(f"Generating thumbnail concepts for: '{video_title}'")
|
||||
logger.info(f"Parameters: target_audience={target_audience}, content_type={content_type}, style_preference={style_preference}, num_concepts={num_concepts}")
|
||||
|
||||
# Create a system prompt for thumbnail concept generation
|
||||
system_prompt = """You are a YouTube thumbnail expert specializing in creating engaging, click-worthy thumbnail concepts.
|
||||
Your task is to generate thumbnail concept ideas based on the provided video information.
|
||||
Focus ONLY on creating concepts that are optimized for YouTube, with proper visual hierarchy, text placement, and emotional triggers.
|
||||
Return ONLY the concept descriptions, without any additional commentary or explanations.
|
||||
Each concept should include:
|
||||
1. A main visual element or scene
|
||||
2. Text placement and content
|
||||
3. Color scheme suggestions
|
||||
4. Emotional trigger or hook
|
||||
5. Brief explanation of why this concept would be effective"""
|
||||
|
||||
# Build the prompt
|
||||
prompt = f"""
|
||||
**Instructions:**
|
||||
|
||||
Please generate {num_concepts} thumbnail concept ideas for a YouTube video with the following information:
|
||||
|
||||
**Video Title:** {video_title}
|
||||
**Video Description:** {video_description}
|
||||
**Target Audience:** {target_audience}
|
||||
**Content Type:** {content_type}
|
||||
**Style Preference:** {style_preference}
|
||||
|
||||
**Specific Instructions:**
|
||||
* Each concept should be clearly separated and numbered.
|
||||
* Focus on creating thumbnails that stand out in search results and recommendations.
|
||||
* Consider the target audience's interests and preferences.
|
||||
* Include specific details about visual elements, text placement, and color schemes.
|
||||
* Explain why each concept would be effective for this specific video.
|
||||
"""
|
||||
|
||||
try:
|
||||
logger.info("Sending request to LLM for thumbnail concepts")
|
||||
response = llm_text_gen(prompt, system_prompt=system_prompt)
|
||||
logger.info(f"Received response from LLM: {len(response)} characters")
|
||||
return response
|
||||
except Exception as err:
|
||||
logger.error(f"Error generating thumbnail concepts: {err}")
|
||||
logger.error(traceback.format_exc())
|
||||
st.error(f"Error: Failed to generate thumbnail concepts: {err}")
|
||||
return None
|
||||
|
||||
|
||||
def generate_thumbnail_design(concept_description, style_preference, aspect_ratio="16:9", keywords=None, style=None, focus=None):
|
||||
"""Generate a thumbnail image based on the concept description."""
|
||||
logger.info(f"Generating thumbnail design for concept: '{concept_description[:50]}...'")
|
||||
logger.info(f"Parameters: style_preference={style_preference}, aspect_ratio={aspect_ratio}, keywords={keywords}, style={style}, focus={focus}")
|
||||
|
||||
# Create a prompt for the image generation
|
||||
image_prompt = f"""
|
||||
Create a YouTube thumbnail image with the following specifications:
|
||||
|
||||
Concept: {concept_description}
|
||||
Style: {style_preference}
|
||||
Aspect Ratio: {aspect_ratio}
|
||||
|
||||
The image should be:
|
||||
- High contrast and visually striking
|
||||
- Suitable for a YouTube thumbnail
|
||||
- Include the specified visual elements and text
|
||||
- Follow the color scheme described
|
||||
- Optimized for small display sizes
|
||||
|
||||
Make sure the text is large and readable, and the main subject is centered and prominent.
|
||||
"""
|
||||
|
||||
try:
|
||||
logger.info("Sending request to Gemini for thumbnail image")
|
||||
# Generate the image using Gemini with enhanced prompt
|
||||
img_path = generate_gemini_image(
|
||||
image_prompt,
|
||||
keywords=keywords,
|
||||
style=style,
|
||||
focus=focus,
|
||||
enhance_prompt=True
|
||||
)
|
||||
logger.info(f"Received image from Gemini: {img_path}")
|
||||
return img_path
|
||||
except Exception as err:
|
||||
logger.error(f"Error generating thumbnail image: {err}")
|
||||
logger.error(traceback.format_exc())
|
||||
st.error(f"Error: Failed to generate thumbnail image: {err}")
|
||||
return None
|
||||
|
||||
|
||||
def edit_thumbnail_image(img_path, edit_instructions):
|
||||
"""Edit a thumbnail image based on user instructions."""
|
||||
logger.info(f"Editing thumbnail image: '{img_path}'")
|
||||
logger.info(f"Edit instructions: '{edit_instructions}'")
|
||||
|
||||
try:
|
||||
logger.info("Sending request to Gemini for image editing")
|
||||
# Edit the image using Gemini
|
||||
edited_img_path = edit_image(img_path, edit_instructions)
|
||||
logger.info(f"Image editing completed. Edited image path: {edited_img_path}")
|
||||
|
||||
# Return the path to the edited image
|
||||
return edited_img_path
|
||||
except Exception as err:
|
||||
logger.error(f"Error editing thumbnail image: {err}")
|
||||
logger.error(traceback.format_exc())
|
||||
st.error(f"Error: Failed to edit thumbnail image: {err}")
|
||||
return None
|
||||
|
||||
|
||||
def analyze_thumbnail(thumbnail_path):
|
||||
"""Analyze a thumbnail for effectiveness."""
|
||||
logger.info(f"Analyzing thumbnail: '{thumbnail_path}'")
|
||||
|
||||
# This would typically involve image analysis, but for now we'll use AI to provide feedback
|
||||
system_prompt = """You are a YouTube thumbnail expert specializing in analyzing and providing feedback on thumbnail designs.
|
||||
Your task is to analyze the thumbnail and provide constructive feedback on its effectiveness.
|
||||
Focus on aspects like visual hierarchy, text readability, emotional impact, and click-worthiness."""
|
||||
|
||||
# For now, we'll just return a placeholder analysis
|
||||
# In a real implementation, we would analyze the actual image
|
||||
logger.info("Generating thumbnail analysis")
|
||||
return """
|
||||
**Thumbnail Analysis:**
|
||||
|
||||
- **Visual Hierarchy:** The main subject is well-positioned and stands out against the background.
|
||||
- **Text Readability:** The text is clear and readable, with good contrast against the background.
|
||||
- **Emotional Impact:** The thumbnail creates curiosity and emotional connection with the target audience.
|
||||
- **Click-worthiness:** The design is likely to attract clicks due to its visual appeal and clear value proposition.
|
||||
|
||||
**Suggestions for Improvement:**
|
||||
- Consider adding a subtle border to make the thumbnail stand out more in search results.
|
||||
- The text could be slightly larger for better readability on mobile devices.
|
||||
- Adding a small icon or logo could help with brand recognition.
|
||||
"""
|
||||
|
||||
|
||||
def parse_concepts(concepts_text):
|
||||
"""Parse the concepts text into a list of individual concepts."""
|
||||
logger.info("Parsing concepts text into individual concepts")
|
||||
|
||||
concept_list = []
|
||||
current_concept = ""
|
||||
|
||||
for line in concepts_text.split('\n'):
|
||||
if line.strip().startswith(('1.', '2.', '3.', '4.', '5.')):
|
||||
if current_concept:
|
||||
concept_list.append(current_concept.strip())
|
||||
current_concept = line
|
||||
else:
|
||||
current_concept += "\n" + line
|
||||
|
||||
if current_concept:
|
||||
concept_list.append(current_concept.strip())
|
||||
|
||||
logger.info(f"Parsed {len(concept_list)} concepts from the response")
|
||||
return concept_list
|
||||
|
||||
|
||||
def write_yt_thumbnail():
|
||||
"""Create a user interface for YouTube Thumbnail Generator."""
|
||||
logger.info("Initializing YouTube Thumbnail Generator UI")
|
||||
st.title("YouTube Thumbnail Generator")
|
||||
st.write("Create engaging, click-worthy thumbnails for your YouTube videos.")
|
||||
|
||||
# Initialize session state for generated thumbnails if it doesn't exist
|
||||
if "generated_thumbnails" not in st.session_state:
|
||||
st.session_state.generated_thumbnails = []
|
||||
if "thumbnail_concepts" not in st.session_state:
|
||||
st.session_state.thumbnail_concepts = None
|
||||
if "current_thumbnail_path" not in st.session_state:
|
||||
st.session_state.current_thumbnail_path = None
|
||||
if "concept_list" not in st.session_state:
|
||||
st.session_state.concept_list = []
|
||||
if "editing_thumbnail" not in st.session_state:
|
||||
st.session_state.editing_thumbnail = False
|
||||
if "edit_instructions" not in st.session_state:
|
||||
st.session_state.edit_instructions = ""
|
||||
if "edited_thumbnail_path" not in st.session_state:
|
||||
st.session_state.edited_thumbnail_path = None
|
||||
if "show_edit_form" not in st.session_state:
|
||||
st.session_state.show_edit_form = False
|
||||
|
||||
# Create tabs for different sections
|
||||
tab1, tab2 = st.tabs(["Basic Info", "Style & Generation"])
|
||||
|
||||
with tab1:
|
||||
# Basic information inputs
|
||||
video_title = st.text_input("Video Title",
|
||||
placeholder="e.g., 10 Tips for Better Photography")
|
||||
video_description = st.text_area("Video Description",
|
||||
placeholder="Brief description of your video content")
|
||||
target_audience = st.text_input("Target Audience",
|
||||
placeholder="e.g., photography enthusiasts, beginners")
|
||||
|
||||
# Content type selection
|
||||
content_type = st.selectbox("Content Type", [
|
||||
"Tutorial/How-to",
|
||||
"Vlog",
|
||||
"Review",
|
||||
"Educational",
|
||||
"Entertainment",
|
||||
"News/Update",
|
||||
"Product Showcase",
|
||||
"Challenge",
|
||||
"Reaction",
|
||||
"Comparison"
|
||||
])
|
||||
|
||||
with tab2:
|
||||
# Style preferences
|
||||
st.subheader("Style Preferences")
|
||||
|
||||
# Create columns for style options
|
||||
col1, col2 = st.columns(2)
|
||||
|
||||
with col1:
|
||||
style_preference = st.selectbox("Thumbnail Style", [
|
||||
"Bold and Dramatic",
|
||||
"Clean and Minimal",
|
||||
"Colorful and Vibrant",
|
||||
"Dark and Moody",
|
||||
"Professional and Corporate",
|
||||
"Playful and Fun",
|
||||
"Retro/Vintage",
|
||||
"Modern and Sleek"
|
||||
])
|
||||
|
||||
num_concepts = st.slider("Number of Concepts", 1, 5, 3)
|
||||
|
||||
with col2:
|
||||
aspect_ratio = st.selectbox("Aspect Ratio", [
|
||||
"16:9 (Standard)",
|
||||
"1:1 (Square)",
|
||||
"4:3 (Classic)",
|
||||
"9:16 (Vertical)"
|
||||
])
|
||||
|
||||
include_text = st.checkbox("Include Text Overlay", value=True)
|
||||
if include_text:
|
||||
text_style = st.selectbox("Text Style", [
|
||||
"Bold and Impactful",
|
||||
"Clean and Readable",
|
||||
"Stylized and Thematic",
|
||||
"Minimal and Subtle"
|
||||
])
|
||||
|
||||
# Advanced AI Prompt Settings
|
||||
st.subheader("Advanced AI Prompt Settings")
|
||||
|
||||
# Create columns for advanced settings
|
||||
col3, col4 = st.columns(2)
|
||||
|
||||
with col3:
|
||||
# Image style selection
|
||||
image_style = st.selectbox("Image Style", [
|
||||
"Auto (AI will choose best style)",
|
||||
"Photorealistic",
|
||||
"Artistic",
|
||||
"Cartoon/Anime",
|
||||
"Sketch/Drawing",
|
||||
"Digital Art",
|
||||
"3D Render"
|
||||
])
|
||||
|
||||
# Extract style for the generate_gemini_image function
|
||||
style = None
|
||||
if image_style == "Photorealistic":
|
||||
style = "photorealistic"
|
||||
elif image_style == "Artistic":
|
||||
style = "artistic"
|
||||
elif image_style == "Cartoon/Anime":
|
||||
style = "cartoon"
|
||||
elif image_style == "Sketch/Drawing":
|
||||
style = "sketch"
|
||||
elif image_style == "Digital Art":
|
||||
style = "digital_art"
|
||||
elif image_style == "3D Render":
|
||||
style = "3d_render"
|
||||
|
||||
with col4:
|
||||
# Focus selection for photorealistic images
|
||||
focus = None
|
||||
if style == "photorealistic":
|
||||
focus = st.selectbox("Image Focus", [
|
||||
"Auto (AI will choose best focus)",
|
||||
"Portraits",
|
||||
"Objects",
|
||||
"Motion",
|
||||
"Wide-angle"
|
||||
])
|
||||
|
||||
# Extract focus for the generate_gemini_image function
|
||||
if focus == "Portraits":
|
||||
focus = "portraits"
|
||||
elif focus == "Objects":
|
||||
focus = "objects"
|
||||
elif focus == "Motion":
|
||||
focus = "motion"
|
||||
elif focus == "Wide-angle":
|
||||
focus = "wide-angle"
|
||||
elif focus == "Auto (AI will choose best focus)":
|
||||
focus = None
|
||||
|
||||
# Keywords for enhanced prompt generation
|
||||
st.subheader("Keywords for Enhanced Prompt")
|
||||
st.write("Add keywords to enhance the AI prompt generation. These will help create more detailed and accurate thumbnails.")
|
||||
|
||||
# Create a text area for keywords
|
||||
keywords_input = st.text_area(
|
||||
"Keywords (comma-separated)",
|
||||
placeholder="e.g., vibrant, energetic, bold, eye-catching, professional"
|
||||
)
|
||||
|
||||
# Process keywords
|
||||
keywords = None
|
||||
if keywords_input:
|
||||
keywords = [k.strip() for k in keywords_input.split(",") if k.strip()]
|
||||
logger.info(f"User provided keywords: {keywords}")
|
||||
|
||||
# Generate button
|
||||
if st.button("Generate Thumbnail Concepts"):
|
||||
if not video_title:
|
||||
st.error("Please enter a video title.")
|
||||
return
|
||||
|
||||
with st.spinner("Generating thumbnail concepts..."):
|
||||
logger.info("User clicked Generate Thumbnail Concepts button")
|
||||
concepts = generate_thumbnail_concepts(
|
||||
video_title,
|
||||
video_description,
|
||||
target_audience,
|
||||
content_type,
|
||||
style_preference,
|
||||
num_concepts
|
||||
)
|
||||
|
||||
if concepts:
|
||||
# Store the concepts in session state
|
||||
st.session_state.thumbnail_concepts = concepts
|
||||
# Parse the concepts and store in session state
|
||||
st.session_state.concept_list = parse_concepts(concepts)
|
||||
logger.info("Stored thumbnail concepts in session state")
|
||||
|
||||
# Display the concepts in tabs
|
||||
st.subheader("Thumbnail Concepts")
|
||||
|
||||
# Create tabs for each concept
|
||||
concept_tabs = st.tabs([f"Concept {i+1}" for i in range(len(st.session_state.concept_list))])
|
||||
|
||||
for i, tab in enumerate(concept_tabs):
|
||||
with tab:
|
||||
st.markdown(st.session_state.concept_list[i])
|
||||
|
||||
# Add a button to generate image for this concept
|
||||
if st.button(f"Generate Image for Concept {i+1}", key=f"gen_img_{i}"):
|
||||
with st.spinner(f"Generating thumbnail image for concept {i+1}..."):
|
||||
logger.info(f"User selected concept {i+1} for image generation")
|
||||
# Get the selected concept
|
||||
selected_concept = st.session_state.concept_list[i]
|
||||
|
||||
# Generate the thumbnail image with enhanced prompt
|
||||
img_path = generate_thumbnail_design(
|
||||
selected_concept,
|
||||
style_preference,
|
||||
aspect_ratio.split()[0], # Extract just the ratio part
|
||||
keywords=keywords,
|
||||
style=style,
|
||||
focus=focus
|
||||
)
|
||||
|
||||
if img_path:
|
||||
# Store the current thumbnail path in session state
|
||||
st.session_state.current_thumbnail_path = img_path
|
||||
logger.info(f"Stored current thumbnail path in session state: {img_path}")
|
||||
|
||||
# Display the generated image
|
||||
st.subheader("Generated Thumbnail")
|
||||
st.image(img_path, use_container_width=True)
|
||||
|
||||
# Add download button
|
||||
with open(img_path, "rb") as file:
|
||||
st.download_button(
|
||||
label="Download Thumbnail",
|
||||
data=file,
|
||||
file_name=f"youtube_thumbnail_{int(time.time())}.png",
|
||||
mime="image/png"
|
||||
)
|
||||
|
||||
# Add image editing section
|
||||
st.subheader("Edit Thumbnail")
|
||||
st.write("Make changes to your thumbnail by providing instructions below:")
|
||||
|
||||
# Create a text area for edit instructions
|
||||
edit_instructions = st.text_area(
|
||||
"Edit Instructions",
|
||||
placeholder="e.g., Make the background darker, Add a red border, Change the text color to white",
|
||||
key=f"edit_instructions_{i}"
|
||||
)
|
||||
|
||||
# Store edit instructions in session state
|
||||
st.session_state.edit_instructions = edit_instructions
|
||||
|
||||
# Add a button to apply edits
|
||||
if st.button("Apply Edits", key=f"apply_edits_{i}"):
|
||||
if not edit_instructions:
|
||||
st.warning("Please provide edit instructions.")
|
||||
else:
|
||||
# Set editing flag
|
||||
st.session_state.editing_thumbnail = True
|
||||
st.session_state.show_edit_form = True
|
||||
|
||||
# Rerun to update the UI
|
||||
st.rerun()
|
||||
|
||||
# Add analysis button
|
||||
if st.button("Analyze Thumbnail", key=f"analyze_{i}"):
|
||||
logger.info("User clicked Analyze Thumbnail button")
|
||||
analysis = analyze_thumbnail(img_path)
|
||||
st.subheader("Thumbnail Analysis")
|
||||
st.markdown(analysis)
|
||||
else:
|
||||
st.error("Failed to generate thumbnail concepts. Please try again.")
|
||||
|
||||
# Display previously generated concepts if they exist in session state
|
||||
elif st.session_state.thumbnail_concepts and st.session_state.concept_list:
|
||||
logger.info("Displaying previously generated concepts from session state")
|
||||
st.subheader("Thumbnail Concepts")
|
||||
|
||||
# Create tabs for each concept
|
||||
concept_tabs = st.tabs([f"Concept {i+1}" for i in range(len(st.session_state.concept_list))])
|
||||
|
||||
for i, tab in enumerate(concept_tabs):
|
||||
with tab:
|
||||
st.markdown(st.session_state.concept_list[i])
|
||||
|
||||
# Add a button to generate image for this concept
|
||||
if st.button(f"Generate Image for Concept {i+1}", key=f"gen_img_existing_{i}"):
|
||||
with st.spinner(f"Generating thumbnail image for concept {i+1}..."):
|
||||
logger.info(f"User selected concept {i+1} for image generation")
|
||||
# Get the selected concept
|
||||
selected_concept = st.session_state.concept_list[i]
|
||||
|
||||
# Generate the thumbnail image with enhanced prompt
|
||||
img_path = generate_thumbnail_design(
|
||||
selected_concept,
|
||||
style_preference,
|
||||
aspect_ratio.split()[0], # Extract just the ratio part
|
||||
keywords=keywords,
|
||||
style=style,
|
||||
focus=focus
|
||||
)
|
||||
|
||||
if img_path:
|
||||
# Store the current thumbnail path in session state
|
||||
st.session_state.current_thumbnail_path = img_path
|
||||
logger.info(f"Stored current thumbnail path in session state: {img_path}")
|
||||
|
||||
# Display the generated image
|
||||
st.subheader("Generated Thumbnail")
|
||||
st.image(img_path, use_container_width=True)
|
||||
|
||||
# Add download button
|
||||
with open(img_path, "rb") as file:
|
||||
st.download_button(
|
||||
label="Download Thumbnail",
|
||||
data=file,
|
||||
file_name=f"youtube_thumbnail_{int(time.time())}.png",
|
||||
mime="image/png"
|
||||
)
|
||||
|
||||
# Add image editing section
|
||||
st.subheader("Edit Thumbnail")
|
||||
st.write("Make changes to your thumbnail by providing instructions below:")
|
||||
|
||||
# Create a text area for edit instructions
|
||||
edit_instructions = st.text_area(
|
||||
"Edit Instructions",
|
||||
placeholder="e.g., Make the background darker, Add a red border, Change the text color to white",
|
||||
key=f"edit_instructions_existing_{i}"
|
||||
)
|
||||
|
||||
# Store edit instructions in session state
|
||||
st.session_state.edit_instructions = edit_instructions
|
||||
|
||||
# Add a button to apply edits
|
||||
if st.button("Apply Edits", key=f"apply_edits_existing_{i}"):
|
||||
if not edit_instructions:
|
||||
st.warning("Please provide edit instructions.")
|
||||
else:
|
||||
# Set editing flag
|
||||
st.session_state.editing_thumbnail = True
|
||||
st.session_state.show_edit_form = True
|
||||
|
||||
# Rerun to update the UI
|
||||
st.rerun()
|
||||
|
||||
# Add analysis button
|
||||
if st.button("Analyze Thumbnail", key=f"analyze_existing_{i}"):
|
||||
logger.info("User clicked Analyze Thumbnail button")
|
||||
analysis = analyze_thumbnail(img_path)
|
||||
st.subheader("Thumbnail Analysis")
|
||||
st.markdown(analysis)
|
||||
|
||||
# Display current thumbnail if it exists in session state
|
||||
elif st.session_state.current_thumbnail_path:
|
||||
logger.info(f"Displaying current thumbnail from session state: {st.session_state.current_thumbnail_path}")
|
||||
st.subheader("Current Thumbnail")
|
||||
st.image(st.session_state.current_thumbnail_path, use_container_width=True)
|
||||
|
||||
# Add download button
|
||||
with open(st.session_state.current_thumbnail_path, "rb") as file:
|
||||
st.download_button(
|
||||
label="Download Thumbnail",
|
||||
data=file,
|
||||
file_name=f"youtube_thumbnail_{int(time.time())}.png",
|
||||
mime="image/png"
|
||||
)
|
||||
|
||||
# Add image editing section
|
||||
st.subheader("Edit Thumbnail")
|
||||
st.write("Make changes to your thumbnail by providing instructions below:")
|
||||
|
||||
# Create a text area for edit instructions
|
||||
edit_instructions = st.text_area(
|
||||
"Edit Instructions",
|
||||
placeholder="e.g., Make the background darker, Add a red border, Change the text color to white",
|
||||
key="edit_instructions_current",
|
||||
value=st.session_state.edit_instructions if st.session_state.edit_instructions else ""
|
||||
)
|
||||
|
||||
# Store edit instructions in session state
|
||||
st.session_state.edit_instructions = edit_instructions
|
||||
|
||||
# Add a button to apply edits
|
||||
if st.button("Apply Edits", key="apply_edits_current"):
|
||||
if not edit_instructions:
|
||||
st.warning("Please provide edit instructions.")
|
||||
else:
|
||||
# Set editing flag
|
||||
st.session_state.editing_thumbnail = True
|
||||
st.session_state.show_edit_form = True
|
||||
|
||||
# Rerun to update the UI
|
||||
st.rerun()
|
||||
|
||||
# Add analysis button
|
||||
if st.button("Analyze Thumbnail", key="analyze_current"):
|
||||
logger.info("User clicked Analyze Thumbnail button")
|
||||
analysis = analyze_thumbnail(st.session_state.current_thumbnail_path)
|
||||
st.subheader("Thumbnail Analysis")
|
||||
st.markdown(analysis)
|
||||
|
||||
# Handle the editing process
|
||||
if st.session_state.editing_thumbnail and st.session_state.show_edit_form:
|
||||
st.subheader("Editing Thumbnail")
|
||||
|
||||
# Show a spinner while editing
|
||||
with st.spinner("Editing thumbnail..."):
|
||||
logger.info(f"User provided edit instructions: '{st.session_state.edit_instructions}'")
|
||||
# Edit the thumbnail image
|
||||
edited_img_path = edit_thumbnail_image(st.session_state.current_thumbnail_path, st.session_state.edit_instructions)
|
||||
|
||||
if edited_img_path:
|
||||
# Update the current thumbnail path in session state
|
||||
st.session_state.edited_thumbnail_path = edited_img_path
|
||||
logger.info(f"Updated current thumbnail path in session state: {edited_img_path}")
|
||||
|
||||
# Reset editing flags
|
||||
st.session_state.editing_thumbnail = False
|
||||
st.session_state.show_edit_form = False
|
||||
|
||||
# Display the edited image
|
||||
st.subheader("Edited Thumbnail")
|
||||
st.image(edited_img_path, use_container_width=True)
|
||||
|
||||
# Add download button for the edited image
|
||||
with open(edited_img_path, "rb") as file:
|
||||
st.download_button(
|
||||
label="Download Edited Thumbnail",
|
||||
data=file,
|
||||
file_name=f"youtube_thumbnail_edited_{int(time.time())}.png",
|
||||
mime="image/png"
|
||||
)
|
||||
|
||||
# Update the current thumbnail path to the edited one
|
||||
st.session_state.current_thumbnail_path = edited_img_path
|
||||
|
||||
# Add a button to continue editing
|
||||
if st.button("Continue Editing"):
|
||||
st.session_state.show_edit_form = True
|
||||
st.rerun()
|
||||
else:
|
||||
# Reset editing flags
|
||||
st.session_state.editing_thumbnail = False
|
||||
st.session_state.show_edit_form = False
|
||||
|
||||
st.error("Failed to edit the thumbnail. Please try again with different instructions.")
|
||||
@@ -0,0 +1,452 @@
|
||||
"""
|
||||
YouTube Title Generator Module
|
||||
|
||||
This module provides functionality for generating YouTube video titles.
|
||||
"""
|
||||
|
||||
import streamlit as st
|
||||
import time
|
||||
import logging
|
||||
from lib.gpt_providers.text_generation.main_text_generation import llm_text_gen
|
||||
|
||||
# Configure logging
|
||||
logging.basicConfig(
|
||||
level=logging.INFO,
|
||||
format='%(asctime)s - %(name)s - %(levelname)s - %(message)s'
|
||||
)
|
||||
logger = logging.getLogger('youtube_title_generator')
|
||||
|
||||
|
||||
def analyze_title(title):
|
||||
"""Analyze a YouTube title for SEO and clickbait."""
|
||||
logger.info(f"Analyzing title: '{title}'")
|
||||
|
||||
# Character count
|
||||
char_count = len(title)
|
||||
optimal_length = 50 <= char_count <= 60
|
||||
logger.info(f"Character count: {char_count}, Optimal length: {optimal_length}")
|
||||
|
||||
# Clickbait detection. TBD: Use AI to detect clickbait.
|
||||
clickbait_phrases = [
|
||||
"shocking", "you won't believe", "gone wrong", "gone sexual",
|
||||
"free v-bucks", "free robux", "100%", "gone viral", "viral",
|
||||
"you need to see this", "wait till the end", "at 3am", "3am",
|
||||
"don't watch this", "watch till the end", "gone too far",
|
||||
"insane", "unbelievable", "mind-blowing", "life-changing",
|
||||
"secret", "hidden", "revealed", "exposed", "leaked",
|
||||
"never before seen", "first time ever", "world's first",
|
||||
"no one knows", "experts hate this", "doctors hate this",
|
||||
"this will change your life", "this will blow your mind",
|
||||
"you've been doing it wrong", "the truth about", "the real reason",
|
||||
"what they don't want you to know", "what they're hiding",
|
||||
"what they don't tell you", "what you need to know",
|
||||
"what you should know", "what you must know", "what you must see",
|
||||
"what you must watch", "what you must do", "what you must have",
|
||||
"what you must buy", "what you must try", "what you must avoid",
|
||||
"what you must stop doing", "what you must start doing",
|
||||
"what you must change", "what you must learn", "what you must understand",
|
||||
"what you must realize", "what you must accept", "what you must believe",
|
||||
"what you must know about", "what you must see about", "what you must watch about",
|
||||
"what you must do about", "what you must have about", "what you must buy about",
|
||||
"what you must try about", "what you must avoid about", "what you must stop doing about",
|
||||
"what you must start doing about", "what you must change about", "what you must learn about",
|
||||
"what you must understand about", "what you must realize about", "what you must accept about",
|
||||
"what you must believe about", "what you must know about", "what you must see about",
|
||||
"what you must watch about", "what you must do about", "what you must have about",
|
||||
"what you must buy about", "what you must try about", "what you must avoid about",
|
||||
"what you must stop doing about", "what you must start doing about", "what you must change about",
|
||||
"what you must learn about", "what you must understand about", "what you must realize about",
|
||||
"what you must accept about", "what you must believe about"
|
||||
]
|
||||
|
||||
clickbait_score = 0
|
||||
detected_phrases = []
|
||||
for phrase in clickbait_phrases:
|
||||
if phrase.lower() in title.lower():
|
||||
clickbait_score += 1
|
||||
detected_phrases.append(phrase)
|
||||
|
||||
is_clickbait = clickbait_score > 0
|
||||
logger.info(f"Clickbait detection: score={clickbait_score}, is_clickbait={is_clickbait}")
|
||||
if detected_phrases:
|
||||
logger.info(f"Detected clickbait phrases: {', '.join(detected_phrases)}")
|
||||
|
||||
# SEO elements
|
||||
has_number = any(char.isdigit() for char in title)
|
||||
has_question = "?" in title
|
||||
has_colon = ":" in title
|
||||
has_brackets = "[" in title or "]" in title or "(" in title or ")" in title
|
||||
|
||||
logger.info(f"SEO elements: has_number={has_number}, has_question={has_question}, has_colon={has_colon}, has_brackets={has_brackets}")
|
||||
|
||||
# Calculate SEO score
|
||||
seo_score = 0
|
||||
if optimal_length:
|
||||
seo_score += 3
|
||||
if has_number:
|
||||
seo_score += 1
|
||||
if has_question:
|
||||
seo_score += 1
|
||||
if has_colon:
|
||||
seo_score += 1
|
||||
if has_brackets:
|
||||
seo_score += 1
|
||||
if not is_clickbait:
|
||||
seo_score += 2
|
||||
|
||||
logger.info(f"Final SEO score: {seo_score}/10")
|
||||
|
||||
return {
|
||||
"char_count": char_count,
|
||||
"optimal_length": optimal_length,
|
||||
"is_clickbait": is_clickbait,
|
||||
"clickbait_score": clickbait_score,
|
||||
"seo_score": seo_score,
|
||||
"has_number": has_number,
|
||||
"has_question": has_question,
|
||||
"has_colon": has_colon,
|
||||
"has_brackets": has_brackets
|
||||
}
|
||||
|
||||
|
||||
def generate_youtube_title(target_audience, main_points, tone_style, use_case, num_titles=5, progress_bar=None):
|
||||
""" Generate youtube title generator """
|
||||
logger.info(f"Starting title generation with parameters: target_audience='{target_audience}', main_points='{main_points}', tone_style='{tone_style}', use_case='{use_case}', num_titles={num_titles}")
|
||||
|
||||
# Create a custom system prompt that doesn't include blog-specific instructions
|
||||
system_prompt = """You are a YouTube title expert specializing in creating engaging, clickable video titles.
|
||||
Your task is to generate YouTube video titles based on the provided information.
|
||||
Focus ONLY on creating titles that are optimized for YouTube.
|
||||
Return ONLY the titles, one per line, without any numbering or additional text."""
|
||||
|
||||
prompt = f"""
|
||||
**Instructions:**
|
||||
|
||||
Please generate {num_titles} YouTube title options for a video about **{main_points}** based on the following information:
|
||||
|
||||
|
||||
**Target Audience:** {target_audience}
|
||||
|
||||
**Tone and Style:** {tone_style}
|
||||
|
||||
**Use Case:** {use_case}
|
||||
|
||||
**Specific Instructions:**
|
||||
|
||||
* Make the titles catchy and attention-grabbing.
|
||||
* Use relevant keywords to improve SEO.
|
||||
* Tailor the language and tone to the target audience.
|
||||
* Ensure the title reflects the content and use case of the video.
|
||||
* Return ONLY the titles, one per line, without any numbering or additional text.
|
||||
"""
|
||||
|
||||
logger.info("Generated prompt for title generation")
|
||||
logger.debug(f"Prompt: {prompt}")
|
||||
logger.debug(f"System prompt: {system_prompt}")
|
||||
|
||||
try:
|
||||
# Update progress bar if provided
|
||||
if progress_bar:
|
||||
progress_bar.progress(30)
|
||||
progress_bar.text("Analyzing your content and target audience...")
|
||||
logger.info("Progress bar updated: 30% - Analyzing content and target audience")
|
||||
|
||||
# Simulate some processing time to show progress
|
||||
time.sleep(1)
|
||||
|
||||
if progress_bar:
|
||||
progress_bar.progress(60)
|
||||
progress_bar.text("Generating creative title options...")
|
||||
logger.info("Progress bar updated: 60% - Generating creative title options")
|
||||
|
||||
# Get the response from the language model with custom system prompt
|
||||
logger.info("Calling LLM for title generation with custom system prompt")
|
||||
start_time = time.time()
|
||||
response = llm_text_gen(prompt, system_prompt=system_prompt)
|
||||
end_time = time.time()
|
||||
logger.info(f"LLM response received in {end_time - start_time:.2f} seconds")
|
||||
logger.debug(f"Raw LLM response: {response}")
|
||||
|
||||
if progress_bar:
|
||||
progress_bar.progress(90)
|
||||
progress_bar.text("Processing and formatting titles...")
|
||||
logger.info("Progress bar updated: 90% - Processing and formatting titles")
|
||||
|
||||
# Split the response into individual titles
|
||||
titles = [title.strip() for title in response.split('\n') if title.strip()]
|
||||
logger.info(f"Generated {len(titles)} titles")
|
||||
for i, title in enumerate(titles, 1):
|
||||
logger.info(f"Title {i}: '{title}'")
|
||||
|
||||
if progress_bar:
|
||||
progress_bar.progress(100)
|
||||
progress_bar.text("Titles generated successfully!")
|
||||
logger.info("Progress bar updated: 100% - Titles generated successfully")
|
||||
|
||||
return titles
|
||||
except Exception as err:
|
||||
logger.error(f"Error generating titles: {err}", exc_info=True)
|
||||
if progress_bar:
|
||||
progress_bar.progress(100)
|
||||
progress_bar.text("Error generating titles. Please try again.")
|
||||
logger.info("Progress bar updated: 100% - Error generating titles")
|
||||
st.error(f"Error: Failed to get response from LLM: {err}")
|
||||
return None
|
||||
|
||||
|
||||
def write_yt_title():
|
||||
"""Create a user interface for YouTube Title Generator."""
|
||||
logger.info("Initializing YouTube Title Generator UI")
|
||||
st.write("Generate engaging YouTube video titles that drive clicks and views.")
|
||||
|
||||
# Initialize session state for generated titles if it doesn't exist
|
||||
if "generated_titles" not in st.session_state:
|
||||
st.session_state.generated_titles = None
|
||||
|
||||
# Main points input (full width)
|
||||
main_points = st.text_area("Main Points/Keywords (comma-separated)",
|
||||
placeholder="e.g., cooking tips, healthy recipes, quick meals")
|
||||
|
||||
# Create columns for the other inputs
|
||||
col1, col2, col3, col4 = st.columns(4)
|
||||
|
||||
with col1:
|
||||
tone_style = st.selectbox("Tone/Style",
|
||||
["Professional", "Casual", "Humorous", "Educational", "Entertaining", "Inspirational"])
|
||||
|
||||
with col2:
|
||||
target_audience = st.text_input("Target Audience",
|
||||
placeholder="e.g., beginners, professionals, parents")
|
||||
|
||||
with col3:
|
||||
use_case = st.selectbox("Use Case",
|
||||
["How-to/Tutorial", "Vlog", "Review", "Educational", "Entertainment", "News"])
|
||||
|
||||
with col4:
|
||||
num_titles = st.number_input("Number of Titles",
|
||||
min_value=1,
|
||||
max_value=20,
|
||||
value=5,
|
||||
step=1)
|
||||
|
||||
if st.button("Generate Titles"):
|
||||
logger.info("Generate Titles button clicked")
|
||||
logger.info(f"User inputs: main_points='{main_points}', tone_style='{tone_style}', target_audience='{target_audience}', use_case='{use_case}', num_titles={num_titles}")
|
||||
|
||||
if not main_points:
|
||||
logger.warning("No main points provided")
|
||||
st.error("Please enter main points/keywords.")
|
||||
return
|
||||
|
||||
# Create a progress bar
|
||||
progress_bar = st.progress(0)
|
||||
progress_bar.text("Initializing title generation...")
|
||||
logger.info("Created progress bar for title generation")
|
||||
|
||||
# Generate titles with progress updates
|
||||
logger.info("Calling generate_youtube_title function")
|
||||
titles = generate_youtube_title(main_points, tone_style, target_audience, use_case, num_titles, progress_bar)
|
||||
|
||||
# Clear the progress bar after a short delay
|
||||
time.sleep(1)
|
||||
progress_bar.empty()
|
||||
logger.info("Cleared progress bar")
|
||||
|
||||
if titles:
|
||||
logger.info(f"Successfully generated {len(titles)} titles")
|
||||
|
||||
# Store titles in session state for persistence
|
||||
st.session_state.generated_titles = titles
|
||||
|
||||
# Display titles section
|
||||
st.markdown("""
|
||||
<div style='background-color: #f0f2f6; padding: 20px; border-radius: 10px; margin-bottom: 20px;'>
|
||||
<h2 style='color: #FF0000; text-align: center;'>Generated YouTube Titles</h2>
|
||||
<p style='text-align: center;'>Click on a title to see detailed analysis and copy options</p>
|
||||
</div>
|
||||
""", unsafe_allow_html=True)
|
||||
|
||||
# Display titles with analysis
|
||||
for i, title in enumerate(titles, 1):
|
||||
logger.info(f"Analyzing title {i}: '{title}'")
|
||||
|
||||
# Create a more visually appealing expander
|
||||
with st.expander(f"Title {i}: {title}", expanded=False):
|
||||
# Add a divider for better visual separation
|
||||
st.markdown("---")
|
||||
|
||||
# Title display with better formatting
|
||||
st.markdown(f"""
|
||||
<div style='background-color: #f8f9fa; padding: 15px; border-radius: 5px; border-left: 5px solid #FF0000;'>
|
||||
<h3 style='margin: 0;'>{title}</h3>
|
||||
</div>
|
||||
""", unsafe_allow_html=True)
|
||||
|
||||
# Analysis section
|
||||
st.markdown("### Analysis")
|
||||
analysis = analyze_title(title)
|
||||
|
||||
# Create columns for analysis metrics
|
||||
col1, col2 = st.columns(2)
|
||||
|
||||
with col1:
|
||||
# Character count
|
||||
st.markdown("#### Character Count")
|
||||
st.write(f"**{analysis['char_count']}** characters")
|
||||
if analysis['optimal_length']:
|
||||
st.success("✅ Optimal length (50-60 characters)")
|
||||
else:
|
||||
st.warning("⚠️ Not optimal length (should be 50-60 characters)")
|
||||
|
||||
# Clickbait detection
|
||||
st.markdown("#### Clickbait Detection")
|
||||
if analysis['is_clickbait']:
|
||||
st.error(f"⚠️ Possible clickbait detected (score: {analysis['clickbait_score']})")
|
||||
else:
|
||||
st.success("✅ No clickbait detected")
|
||||
|
||||
with col2:
|
||||
# SEO score
|
||||
st.markdown("#### SEO Score")
|
||||
score_color = "#28a745" if analysis['seo_score'] >= 7 else "#ffc107" if analysis['seo_score'] >= 5 else "#dc3545"
|
||||
st.markdown(f"<h2 style='color: {score_color};'>{analysis['seo_score']}/10</h2>", unsafe_allow_html=True)
|
||||
if analysis['seo_score'] >= 7:
|
||||
st.success("✅ Good SEO score")
|
||||
elif analysis['seo_score'] >= 5:
|
||||
st.warning("⚠️ Moderate SEO score")
|
||||
else:
|
||||
st.error("❌ Low SEO score")
|
||||
|
||||
# SEO elements
|
||||
st.markdown("#### SEO Elements")
|
||||
elements = []
|
||||
if analysis['has_number']:
|
||||
elements.append("✅ Contains numbers")
|
||||
if analysis['has_question']:
|
||||
elements.append("✅ Contains question mark")
|
||||
if analysis['has_colon']:
|
||||
elements.append("✅ Contains colon")
|
||||
if analysis['has_brackets']:
|
||||
elements.append("✅ Contains brackets/parentheses")
|
||||
|
||||
for element in elements:
|
||||
st.write(element)
|
||||
|
||||
# Copy functionality using session state
|
||||
st.markdown("### Copy Title")
|
||||
st.code(title, language="text")
|
||||
|
||||
# Use a different approach for copy functionality
|
||||
copy_key = f"copy_{i}"
|
||||
if st.button(f"Copy Title {i}", key=copy_key):
|
||||
# Use JavaScript to copy to clipboard
|
||||
escaped_title = title.replace('"', '\\"')
|
||||
st.markdown(
|
||||
f"""
|
||||
<script>
|
||||
navigator.clipboard.writeText("{escaped_title}");
|
||||
</script>
|
||||
""",
|
||||
unsafe_allow_html=True
|
||||
)
|
||||
st.success(f"✅ Title {i} copied to clipboard!")
|
||||
else:
|
||||
logger.error("Failed to generate titles")
|
||||
st.error("Failed to generate titles. Please try again.")
|
||||
|
||||
# Display previously generated titles if they exist in session state
|
||||
elif st.session_state.generated_titles:
|
||||
titles = st.session_state.generated_titles
|
||||
|
||||
# Display titles section
|
||||
st.markdown("""
|
||||
<div style='background-color: #f0f2f6; padding: 20px; border-radius: 10px; margin-bottom: 20px;'>
|
||||
<h2 style='color: #FF0000; text-align: center;'>Generated YouTube Titles</h2>
|
||||
<p style='text-align: center;'>Click on a title to see detailed analysis and copy options</p>
|
||||
</div>
|
||||
""", unsafe_allow_html=True)
|
||||
|
||||
# Display titles with analysis
|
||||
for i, title in enumerate(titles, 1):
|
||||
logger.info(f"Analyzing title {i}: '{title}'")
|
||||
|
||||
# Create a more visually appealing expander
|
||||
with st.expander(f"Title {i}: {title}", expanded=False):
|
||||
# Add a divider for better visual separation
|
||||
st.markdown("---")
|
||||
|
||||
# Title display with better formatting
|
||||
st.markdown(f"""
|
||||
<div style='background-color: #f8f9fa; padding: 15px; border-radius: 5px; border-left: 5px solid #FF0000;'>
|
||||
<h3 style='margin: 0;'>{title}</h3>
|
||||
</div>
|
||||
""", unsafe_allow_html=True)
|
||||
|
||||
# Analysis section
|
||||
st.markdown("### Analysis")
|
||||
analysis = analyze_title(title)
|
||||
|
||||
# Create columns for analysis metrics
|
||||
col1, col2 = st.columns(2)
|
||||
|
||||
with col1:
|
||||
# Character count
|
||||
st.markdown("#### Character Count")
|
||||
st.write(f"**{analysis['char_count']}** characters")
|
||||
if analysis['optimal_length']:
|
||||
st.success("✅ Optimal length (50-60 characters)")
|
||||
else:
|
||||
st.warning("⚠️ Not optimal length (should be 50-60 characters)")
|
||||
|
||||
# Clickbait detection
|
||||
st.markdown("#### Clickbait Detection")
|
||||
if analysis['is_clickbait']:
|
||||
st.error(f"⚠️ Possible clickbait detected (score: {analysis['clickbait_score']})")
|
||||
else:
|
||||
st.success("✅ No clickbait detected")
|
||||
|
||||
with col2:
|
||||
# SEO score
|
||||
st.markdown("#### SEO Score")
|
||||
score_color = "#28a745" if analysis['seo_score'] >= 7 else "#ffc107" if analysis['seo_score'] >= 5 else "#dc3545"
|
||||
st.markdown(f"<h2 style='color: {score_color};'>{analysis['seo_score']}/10</h2>", unsafe_allow_html=True)
|
||||
if analysis['seo_score'] >= 7:
|
||||
st.success("✅ Good SEO score")
|
||||
elif analysis['seo_score'] >= 5:
|
||||
st.warning("⚠️ Moderate SEO score")
|
||||
else:
|
||||
st.error("❌ Low SEO score")
|
||||
|
||||
# SEO elements
|
||||
st.markdown("#### SEO Elements")
|
||||
elements = []
|
||||
if analysis['has_number']:
|
||||
elements.append("✅ Contains numbers")
|
||||
if analysis['has_question']:
|
||||
elements.append("✅ Contains question mark")
|
||||
if analysis['has_colon']:
|
||||
elements.append("✅ Contains colon")
|
||||
if analysis['has_brackets']:
|
||||
elements.append("✅ Contains brackets/parentheses")
|
||||
|
||||
for element in elements:
|
||||
st.write(element)
|
||||
|
||||
# Copy functionality using session state
|
||||
st.markdown("### Copy Title")
|
||||
st.code(title, language="text")
|
||||
|
||||
# Use a different approach for copy functionality
|
||||
copy_key = f"copy_{i}"
|
||||
if st.button(f"Copy Title {i}", key=copy_key):
|
||||
# Use JavaScript to copy to clipboard
|
||||
escaped_title = title.replace('"', '\\"')
|
||||
st.markdown(
|
||||
f"""
|
||||
<script>
|
||||
navigator.clipboard.writeText("{escaped_title}");
|
||||
</script>
|
||||
""",
|
||||
unsafe_allow_html=True
|
||||
)
|
||||
st.success(f"✅ Title {i} copied to clipboard!")
|
||||
Reference in New Issue
Block a user