Spaces:
Configuration error
Configuration error
import os | |
import sys | |
import gradio as gr | |
import logging | |
import tempfile | |
import uuid | |
from pathlib import Path | |
import time | |
from dotenv import load_dotenv | |
import numpy as np | |
# Configure logging and environment | |
logging.basicConfig(level=logging.INFO) | |
logger = logging.getLogger(__name__) | |
load_dotenv() # Load environment variables from .env file | |
# Define content types with descriptions and emojis | |
CONTENT_TYPES = { | |
"interesting": {"emoji": "๐ง ", "desc": "Intellectually engaging and thought-provoking moments"}, | |
"funny": {"emoji": "๐", "desc": "Humorous moments that will make viewers laugh"}, | |
"dramatic": {"emoji": "๐ญ", "desc": "Powerful emotional moments with high tension or impact"}, | |
"educational": {"emoji": "๐", "desc": "Moments that teach valuable information clearly"}, | |
"surprising": {"emoji": "๐ฎ", "desc": "Unexpected twists or shocking revelations"}, | |
"inspiring": {"emoji": "โจ", "desc": "Motivational or uplifting moments"} | |
} | |
# Add current directory to Python path for imports | |
current_dir = os.path.dirname(os.path.abspath(__file__)) | |
if current_dir not in sys.path: | |
sys.path.insert(0, current_dir) | |
# Check for Modal availability - prefer local processing on Spaces | |
USE_MODAL = False | |
try: | |
import modal | |
# Only set USE_MODAL if we're not running on HF Spaces | |
if not os.environ.get("SPACE_ID"): | |
USE_MODAL = os.environ.get("USE_MODAL", "False").lower() == "true" | |
except ImportError: | |
USE_MODAL = False | |
# Custom CSS for better styling | |
custom_css = """ | |
.gradio-container { | |
max-width: 1200px !important; | |
margin-left: auto !important; | |
margin-right: auto !important; | |
background-color: #f9f9f9 !important; | |
} | |
h1, h2, h3 { | |
font-family: 'Poppins', sans-serif !important; | |
color: #222222 !important; | |
} | |
.content-type-btn { | |
border-radius: 10px !important; | |
border: 2px solid transparent !important; | |
transition: all 0.3s ease !important; | |
} | |
.content-type-btn:hover { | |
transform: translateY(-2px) !important; | |
box-shadow: 0 4px 8px rgba(0,0,0,0.1) !important; | |
} | |
.content-type-btn.selected { | |
border: 2px solid #ff4b4b !important; | |
background-color: rgba(255, 75, 75, 0.1) !important; | |
} | |
.container { | |
margin: 0; | |
padding: 0; | |
} | |
.tips-box { | |
border-left: 4px solid #ff4b4b; | |
padding: 10px 15px; | |
background-color: rgba(255, 75, 75, 0.05); | |
border-radius: 0 8px 8px 0; | |
margin: 10px 0; | |
} | |
.progress-container { | |
border-radius: 12px; | |
border: 1px solid #ddd; | |
padding: 15px; | |
background-color: white; | |
box-shadow: 0 2px 5px rgba(0,0,0,0.05); | |
transition: all 0.3s ease; | |
} | |
.progress-container:hover { | |
box-shadow: 0 4px 10px rgba(0,0,0,0.1); | |
} | |
.results-gallery img { | |
border-radius: 8px; | |
transition: all 0.3s ease; | |
} | |
.results-gallery img:hover { | |
transform: scale(1.02); | |
box-shadow: 0 4px 15px rgba(0,0,0,0.1); | |
} | |
footer { | |
margin-top: 20px; | |
text-align: center; | |
font-size: 0.9em; | |
color: #666; | |
} | |
""" | |
def process_video(youtube_url, num_clips, min_duration, max_duration, content_type, progress=gr.Progress()): | |
"""Process video to generate shorts using either Modal or local processing""" | |
result_clips = [] | |
result_titles = [] | |
result_captions = [] | |
result_hashtags = [] | |
try: | |
# Update progress | |
progress(0, desc="Starting processing...") | |
time.sleep(0.5) # Small delay for better UX | |
if USE_MODAL: | |
# Use Modal processing if available | |
try: | |
import modal | |
from modal_deploy import app | |
progress(0.1, desc=f"Connecting to Modal cloud services...") | |
# Download video | |
progress(0.2, desc="Downloading YouTube video...") | |
result = app.download_youtube_video.call(youtube_url) | |
video_path = result[0] if isinstance(result, tuple) else result.get("path") | |
video_title = result[1] if isinstance(result, tuple) else result.get("title", "Unknown Title") | |
if not video_path: | |
return [], "Download failed: Video could not be accessed", "", "", "" | |
# Transcribe video | |
progress(0.3, desc="Transcribing video audio...") | |
transcript_result = app.transcribe_video_enhanced.call(video_path) | |
# Select highlights based on content type | |
progress(0.5, desc=f"Selecting {content_type} highlights...") | |
try: | |
highlights = app.smart_highlight_selector.call( | |
transcript_result, video_title, num_clips, min_duration, max_duration, content_type) | |
except: | |
highlights = app.select_highlights.call( | |
transcript_result, video_title, num_clips, max_duration) | |
# Create clips | |
progress(0.7, desc="Creating video clips...") | |
try: | |
clips = app.create_smart_clips.call( | |
video_path, transcript_result, min_duration, max_duration, num_clips) | |
except: | |
clips = app.clip_video.call(video_path, highlights) | |
# Generate captions | |
progress(0.9, desc="Generating captions and hashtags...") | |
for clip_info in clips: | |
caption = app.generate_caption.call(clip_info, transcript_result, video_title) | |
result_clips.append(clip_info.get("path")) | |
result_titles.append(caption.get("title", "No title")) | |
result_captions.append(caption.get("caption", "No caption")) | |
result_hashtags.append(caption.get("hashtags", "#shorts")) | |
progress(1.0, desc="Processing complete!") | |
return result_clips, f"Successfully created {len(result_clips)} clips!", \ | |
"\n\n".join(result_titles), "\n\n".join(result_captions), "\n\n".join(result_hashtags) | |
except Exception as e: | |
logger.error(f"Modal processing error: {str(e)}") | |
return [], f"Error with Modal processing: {str(e)}", "", "", "" | |
else: | |
# Use local processing | |
progress(0.1, desc="Initializing local processing...") | |
# Import required utilities for local processing | |
from utils.video import download_video_from_url | |
from local_processors import ( | |
transcribe_video_locally, | |
select_highlights_locally, | |
clip_video_locally, | |
generate_caption_locally | |
) | |
# Download video | |
progress(0.2, desc="Downloading YouTube video...") | |
try: | |
video_path, video_title = download_video_from_url(youtube_url) | |
except Exception as e: | |
logger.error(f"Error downloading video: {str(e)}") | |
return [], f"Download failed: {str(e)}", "", "", "" | |
# Transcribe video | |
progress(0.4, desc="Transcribing video...") | |
transcript_data = transcribe_video_locally(video_path) | |
# Select highlights | |
progress(0.6, desc=f"Selecting {content_type} highlights...") | |
highlights = select_highlights_locally(transcript_data, video_title, num_clips, max_duration) | |
# Adjust highlight durations | |
for highlight in highlights: | |
if highlight["end_time"] - highlight["start_time"] < min_duration: | |
highlight["end_time"] = highlight["start_time"] + min_duration | |
if highlight["end_time"] - highlight["start_time"] > max_duration: | |
highlight["end_time"] = highlight["start_time"] + max_duration | |
# Create clips | |
progress(0.8, desc=f"Creating {content_type} clips...") | |
clip_infos = clip_video_locally(video_path, highlights, content_type) | |
# Generate captions | |
progress(0.9, desc="Generating captions...") | |
for clip_info in clip_infos: | |
caption_data = generate_caption_locally(clip_info, transcript_data, video_title) | |
# Add to results | |
result_clips.append(clip_info["path"]) | |
result_titles.append(caption_data["title"]) | |
result_captions.append(caption_data["caption"]) | |
result_hashtags.append(caption_data["hashtags"]) | |
progress(1.0, desc="Processing complete!") | |
return result_clips, f"Successfully created {len(result_clips)} clips!", \ | |
"\n\n".join(result_titles), "\n\n".join(result_captions), "\n\n".join(result_hashtags) | |
except Exception as e: | |
logger.error(f"Error in processing: {str(e)}") | |
return [], f"Error: {str(e)}", "", "", "" | |
def create_app(): | |
"""Create and configure the Gradio application""" | |
# Define theme and layout | |
theme = gr.themes.Soft( | |
primary_hue="red", | |
secondary_hue="blue", | |
).set( | |
body_background_fill="#f9f9f9", | |
block_radius="8px", | |
button_primary_background_fill="#ff4b4b", | |
button_primary_background_fill_hover="#ff6b6b", | |
) | |
with gr.Blocks(css=custom_css, theme=theme, title="YouTube Shorts Generator") as app: | |
# Header with branding | |
with gr.Row(elem_id="header"): | |
with gr.Column(scale=1): | |
gr.Image("./assets/logo.png", show_label=False, height=80, width=80) | |
with gr.Column(scale=5): | |
gr.Markdown("# ๐ฌ YouTube Shorts Generator") | |
gr.Markdown("Transform long videos into engaging social media shorts with AI") | |
# Tabs for different input methods | |
with gr.Tabs() as tabs: | |
# YouTube URL Tab | |
with gr.TabItem("YouTube URL ๐", id="youtube_tab"): | |
with gr.Row(): | |
with gr.Column(scale=2): | |
# Input options | |
youtube_url = gr.Textbox( | |
label="YouTube Video URL", | |
placeholder="https://www.youtube.com/watch?v=...", | |
info="Paste a YouTube video URL to process", | |
show_label=True | |
) | |
# Configuration panel | |
with gr.Box(): | |
gr.Markdown("### ๐ ๏ธ Configure Your Shorts") | |
with gr.Row(): | |
num_clips = gr.Slider( | |
minimum=1, maximum=5, value=2, step=1, | |
label="Number of clips to generate", | |
info="How many separate shorts to create" | |
) | |
with gr.Group(): | |
gr.Markdown("##### โฑ๏ธ Duration Settings (seconds)") | |
with gr.Row(): | |
min_duration = gr.Slider( | |
minimum=10, maximum=30, value=15, step=5, | |
label="Minimum duration" | |
) | |
max_duration = gr.Slider( | |
minimum=30, maximum=90, value=45, step=5, | |
label="Maximum duration" | |
) | |
gr.Markdown("##### ๐ฏ Content Type") | |
with gr.Row(): | |
content_type = gr.Dropdown( | |
choices=list(CONTENT_TYPES.keys()), | |
value="interesting", | |
label="Content Style", | |
info="Select what kind of moments to extract", | |
multiselect=False | |
) | |
with gr.Row(): | |
gr.Markdown( | |
value=f"**{CONTENT_TYPES['interesting']['emoji']} Type Info:** {CONTENT_TYPES['interesting']['desc']}", | |
elem_id="content_description" | |
) | |
# Process button | |
process_btn = gr.Button( | |
"๐ Generate Shorts", | |
variant="primary", | |
size="lg" | |
) | |
gr.Markdown( | |
""" | |
<div class="tips-box"> | |
<strong>๐ก Pro Tip:</strong> For best results, choose a content type that | |
matches your video's style and select an appropriate duration for your target platform. | |
</div> | |
""", | |
elem_classes=["tips"] | |
) | |
with gr.Column(scale=2, elem_classes=["progress-container"]): | |
gr.Markdown("### ๐ Processing Status") | |
# Status and results display | |
status = gr.Textbox(label="Status", interactive=False) | |
# Results gallery | |
results_gallery = gr.Gallery( | |
label="Generated Shorts", | |
columns=2, | |
object_fit="contain", | |
height="auto", | |
show_label=True, | |
elem_classes=["results-gallery"] | |
) | |
# Results details collapsible | |
with gr.Accordion("๐ Generated Content", open=False): | |
title_results = gr.Textbox(label="๐ค Generated Titles", lines=3) | |
caption_results = gr.Textbox(label="๐ Generated Captions", lines=3) | |
hashtag_results = gr.Textbox(label="๐ท๏ธ Generated Hashtags", lines=3) | |
# Upload Tab (for future implementation) | |
with gr.TabItem("Upload Video ๐ค", id="upload_tab"): | |
gr.Markdown("## ๐ง Coming Soon!") | |
gr.Markdown("Direct video upload functionality will be available in a future update.") | |
gr.Markdown("Please use the YouTube URL tab for now.") | |
# How to use section | |
with gr.Accordion("๐ How to Use", open=False): | |
gr.Markdown(""" | |
### How to Create YouTube Shorts | |
1. **Input a YouTube URL**: Enter the link to any YouTube video you want to turn into shorts | |
2. **Configure settings**: | |
- Number of Clips: How many short clips to generate | |
- Duration Range: Set min/max duration (15-90 seconds) | |
- Content Type: Choose what kind of moments to look for (funny, educational, etc.) | |
3. **Click Generate**: The system will: | |
- Download and transcribe the video | |
- Find the best moments based on your content preference | |
- Create variable-length clips optimized for engagement | |
- Generate titles, captions and hashtags | |
4. **Use the Results**: Download clips and copy the generated text for your posts | |
**Processing Time**: Depending on video length, processing can take 2-5 minutes | |
""") | |
# Footer with credits | |
gr.Markdown(""" | |
<footer> | |
<p>Created for the MCP Hackathon 2025 | <a href="https://github.com/VanshGoyal000/shorts-generator" target="_blank">GitHub</a></p> | |
</footer> | |
""") | |
# Update content description when type changes | |
def update_description(content_type): | |
emoji = CONTENT_TYPES[content_type]['emoji'] | |
desc = CONTENT_TYPES[content_type]['desc'] | |
return f"**{emoji} Type Info:** {desc}" | |
content_type.change(update_description, inputs=[content_type], outputs="content_description") | |
# Process when button is clicked | |
process_btn.click( | |
process_video, | |
inputs=[youtube_url, num_clips, min_duration, max_duration, content_type], | |
outputs=[results_gallery, status, title_results, caption_results, hashtag_results] | |
) | |
return app | |
# Run the app directly when this file is executed | |
if __name__ == "__main__": | |
# Set up logging | |
logging.basicConfig(level=logging.INFO) | |
logger.getLogger().setLevel(logging.INFO) | |
# Check for Windows-specific fixes | |
if os.name == 'nt': | |
try: | |
from utils.windows_fix import apply_windows_asyncio_fixes | |
apply_windows_asyncio_fixes() | |
except ImportError: | |
logger.warning("Windows-specific fixes not available, some features may not work correctly.") | |
# Create and launch app | |
app = create_app() | |
app.queue(concurrency_count=1).launch(share=True) | |