Spaces:
Runtime error
Runtime error
Commit ·
47b4017
0
Parent(s):
Clean repository for Hugging Face deployment
Browse files- .gitignore +28 -0
- README.md +127 -0
- app.py +289 -0
- modules/composer.py +58 -0
- modules/frame_extractor.py +19 -0
- modules/subtitle_burner.py +40 -0
- modules/subtitles.py +38 -0
- modules/summarizer.py +12 -0
- modules/thumbnail.py +48 -0
- requirements.txt +0 -0
.gitignore
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Virtual Environment
|
| 2 |
+
venv/
|
| 3 |
+
|
| 4 |
+
# Python cache
|
| 5 |
+
__pycache__/
|
| 6 |
+
*.pyc
|
| 7 |
+
*.pyo
|
| 8 |
+
*.pyd
|
| 9 |
+
|
| 10 |
+
# Environment variables
|
| 11 |
+
.env
|
| 12 |
+
|
| 13 |
+
# Build folders
|
| 14 |
+
dist/
|
| 15 |
+
build/
|
| 16 |
+
|
| 17 |
+
# Project generated files
|
| 18 |
+
assets/uploads/
|
| 19 |
+
assets/outputs/
|
| 20 |
+
!assets/uploads/.gitkeep
|
| 21 |
+
!assets/outputs/.gitkeep
|
| 22 |
+
|
| 23 |
+
# OS files
|
| 24 |
+
.DS_Store
|
| 25 |
+
Thumbs.db
|
| 26 |
+
|
| 27 |
+
# IDE settings
|
| 28 |
+
.vscode/
|
README.md
ADDED
|
@@ -0,0 +1,127 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# CLIPFORGE
|
| 2 |
+
|
| 3 |
+
🚀 CLPFORGE
|
| 4 |
+
AI-Powered Content Creation Engine
|
| 5 |
+
|
| 6 |
+
CLPFORGE is an AI-driven content generation toolkit that helps creators instantly generate:
|
| 7 |
+
|
| 8 |
+
🎬 Video Ideas
|
| 9 |
+
|
| 10 |
+
📝 Scripts
|
| 11 |
+
|
| 12 |
+
🗣 Subtitles
|
| 13 |
+
|
| 14 |
+
🖼 Thumbnail Concepts & AI Prompts
|
| 15 |
+
|
| 16 |
+
Built for creators who want speed, consistency, and higher engagement without wasting hours brainstorming.
|
| 17 |
+
|
| 18 |
+
🔥 Features
|
| 19 |
+
1️⃣ Idea Generator
|
| 20 |
+
|
| 21 |
+
Generates high-potential content ideas based on:
|
| 22 |
+
|
| 23 |
+
Niche
|
| 24 |
+
|
| 25 |
+
Target Audience
|
| 26 |
+
|
| 27 |
+
Content Type (Shorts, Long-form, Reels, etc.)
|
| 28 |
+
|
| 29 |
+
Trending-style prompt patterns
|
| 30 |
+
|
| 31 |
+
Focus: Ideas engineered for attention and scalability, not generic topics.
|
| 32 |
+
|
| 33 |
+
2️⃣ Script Generator
|
| 34 |
+
|
| 35 |
+
Creates structured, retention-optimized scripts including:
|
| 36 |
+
|
| 37 |
+
Hook
|
| 38 |
+
|
| 39 |
+
Pattern Interrupts
|
| 40 |
+
|
| 41 |
+
Retention Loops
|
| 42 |
+
|
| 43 |
+
Body Content
|
| 44 |
+
|
| 45 |
+
Call-to-Action (CTA)
|
| 46 |
+
|
| 47 |
+
Supports:
|
| 48 |
+
|
| 49 |
+
Short-form (30–60 sec)
|
| 50 |
+
|
| 51 |
+
Long-form YouTube scripts
|
| 52 |
+
|
| 53 |
+
Educational breakdowns
|
| 54 |
+
|
| 55 |
+
Storytelling formats
|
| 56 |
+
|
| 57 |
+
3️⃣ Subtitle Generator
|
| 58 |
+
|
| 59 |
+
Generates subtitles designed for engagement:
|
| 60 |
+
|
| 61 |
+
Short, punchy lines
|
| 62 |
+
|
| 63 |
+
Scroll-stopping formatting
|
| 64 |
+
|
| 65 |
+
Retention-focused phrasing
|
| 66 |
+
|
| 67 |
+
Optional SRT-style structure support
|
| 68 |
+
|
| 69 |
+
4️⃣ Thumbnail Generator
|
| 70 |
+
|
| 71 |
+
Generates:
|
| 72 |
+
|
| 73 |
+
High-CTR thumbnail text
|
| 74 |
+
|
| 75 |
+
Visual direction concepts
|
| 76 |
+
|
| 77 |
+
AI image prompts (Midjourney / DALL·E / Stable Diffusion ready)
|
| 78 |
+
|
| 79 |
+
Built around:
|
| 80 |
+
|
| 81 |
+
Curiosity Gap
|
| 82 |
+
|
| 83 |
+
Emotional Triggers
|
| 84 |
+
|
| 85 |
+
Click-through psychology
|
| 86 |
+
|
| 87 |
+
🧠 How It Works
|
| 88 |
+
|
| 89 |
+
CLPFORGE uses prompt engineering + AI models to:
|
| 90 |
+
|
| 91 |
+
Analyze niche or topic input
|
| 92 |
+
|
| 93 |
+
Generate multiple idea variations
|
| 94 |
+
|
| 95 |
+
Structure scripts for audience retention
|
| 96 |
+
|
| 97 |
+
Format subtitles for engagement
|
| 98 |
+
|
| 99 |
+
Suggest high-CTR thumbnail concepts
|
| 100 |
+
|
| 101 |
+
The system focuses on content performance, not just content generation.
|
| 102 |
+
|
| 103 |
+
🎯 Target Users
|
| 104 |
+
|
| 105 |
+
YouTube Creators
|
| 106 |
+
|
| 107 |
+
Short-form Content Creators
|
| 108 |
+
|
| 109 |
+
Digital Marketers
|
| 110 |
+
|
| 111 |
+
Content Agencies
|
| 112 |
+
|
| 113 |
+
Personal Brands
|
| 114 |
+
|
| 115 |
+
🚧 Roadmap
|
| 116 |
+
|
| 117 |
+
Planned improvements:
|
| 118 |
+
|
| 119 |
+
Direct AI thumbnail image generation
|
| 120 |
+
|
| 121 |
+
Trend scraping integration
|
| 122 |
+
|
| 123 |
+
Multi-language support
|
| 124 |
+
|
| 125 |
+
SEO optimization module
|
| 126 |
+
|
| 127 |
+
Performance analytics dashboard
|
app.py
ADDED
|
@@ -0,0 +1,289 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import gradio as gr
|
| 2 |
+
import shutil
|
| 3 |
+
import os
|
| 4 |
+
|
| 5 |
+
from dotenv import load_dotenv
|
| 6 |
+
load_dotenv()
|
| 7 |
+
|
| 8 |
+
from modules.summarizer import summarize_text
|
| 9 |
+
from modules.frame_extractor import extract_frame
|
| 10 |
+
from modules.thumbnail import generate_thumbnail
|
| 11 |
+
from modules.composer import compose_videos
|
| 12 |
+
from modules.subtitles import generate_subtitles
|
| 13 |
+
from modules.subtitle_burner import burn_subtitles
|
| 14 |
+
|
| 15 |
+
|
| 16 |
+
UPLOAD_DIR = "assets/uploads"
|
| 17 |
+
os.makedirs(UPLOAD_DIR, exist_ok=True)
|
| 18 |
+
|
| 19 |
+
|
| 20 |
+
# ---------------- FUNCTIONS ----------------
|
| 21 |
+
|
| 22 |
+
def save_uploaded_video(video_file):
|
| 23 |
+
if video_file is None:
|
| 24 |
+
return "❌ No video uploaded."
|
| 25 |
+
|
| 26 |
+
filename = os.path.basename(video_file)
|
| 27 |
+
destination = os.path.join(UPLOAD_DIR, filename)
|
| 28 |
+
shutil.copy(video_file, destination)
|
| 29 |
+
|
| 30 |
+
return f"✅ Video uploaded: {filename}"
|
| 31 |
+
|
| 32 |
+
|
| 33 |
+
def summarize_ui(text):
|
| 34 |
+
if not text.strip():
|
| 35 |
+
return "Please enter text."
|
| 36 |
+
return summarize_text(text)
|
| 37 |
+
|
| 38 |
+
|
| 39 |
+
def create_thumbnail(video_file, title_text):
|
| 40 |
+
if video_file is None:
|
| 41 |
+
return None
|
| 42 |
+
frame = extract_frame(video_file)
|
| 43 |
+
return generate_thumbnail(frame, title_text)
|
| 44 |
+
|
| 45 |
+
|
| 46 |
+
def compose_ui(video_files):
|
| 47 |
+
if not video_files:
|
| 48 |
+
return None
|
| 49 |
+
return compose_videos(video_files)
|
| 50 |
+
|
| 51 |
+
|
| 52 |
+
def subtitle_ui(video):
|
| 53 |
+
if video is None:
|
| 54 |
+
return None
|
| 55 |
+
return generate_subtitles(video)
|
| 56 |
+
|
| 57 |
+
|
| 58 |
+
def burn_subtitles_ui(video, subtitle_file):
|
| 59 |
+
if video is None or subtitle_file is None:
|
| 60 |
+
return None
|
| 61 |
+
return burn_subtitles(video, subtitle_file)
|
| 62 |
+
|
| 63 |
+
|
| 64 |
+
# ---------------- CSS ----------------
|
| 65 |
+
|
| 66 |
+
custom_css = """
|
| 67 |
+
body{
|
| 68 |
+
background:linear-gradient(135deg,#020617,#0f172a);
|
| 69 |
+
font-family:Inter;
|
| 70 |
+
}
|
| 71 |
+
|
| 72 |
+
.hero{
|
| 73 |
+
text-align:center;
|
| 74 |
+
padding:50px;
|
| 75 |
+
}
|
| 76 |
+
|
| 77 |
+
.hero-title{
|
| 78 |
+
font-size:44px;
|
| 79 |
+
font-weight:800;
|
| 80 |
+
background:linear-gradient(90deg,#60a5fa,#a78bfa,#c084fc);
|
| 81 |
+
-webkit-background-clip:text;
|
| 82 |
+
-webkit-text-fill-color:transparent;
|
| 83 |
+
}
|
| 84 |
+
|
| 85 |
+
.navbar{
|
| 86 |
+
display:flex;
|
| 87 |
+
gap:20px;
|
| 88 |
+
justify-content:center;
|
| 89 |
+
margin-bottom:25px;
|
| 90 |
+
}
|
| 91 |
+
|
| 92 |
+
.card{
|
| 93 |
+
background:#0f172a;
|
| 94 |
+
padding:25px;
|
| 95 |
+
border-radius:14px;
|
| 96 |
+
box-shadow:0 8px 20px rgba(0,0,0,0.4);
|
| 97 |
+
}
|
| 98 |
+
"""
|
| 99 |
+
|
| 100 |
+
|
| 101 |
+
# ---------------- UI ----------------
|
| 102 |
+
|
| 103 |
+
with gr.Blocks(css=custom_css, theme=gr.themes.Soft()) as app:
|
| 104 |
+
|
| 105 |
+
with gr.Column(elem_classes="hero"):
|
| 106 |
+
gr.Markdown("""
|
| 107 |
+
<div class="hero-title">🎬 ClipForge AI</div>
|
| 108 |
+
Create viral short-form content using AI
|
| 109 |
+
""")
|
| 110 |
+
|
| 111 |
+
# NAVIGATION BAR
|
| 112 |
+
with gr.Row():
|
| 113 |
+
btn_upload = gr.Button("📥 Upload")
|
| 114 |
+
btn_summary = gr.Button("📝 Summary")
|
| 115 |
+
btn_thumbnail = gr.Button("🖼 Thumbnail")
|
| 116 |
+
btn_subtitle = gr.Button("🎤 Subtitles")
|
| 117 |
+
btn_composer = gr.Button("🎞 Composer")
|
| 118 |
+
btn_burn = gr.Button("🔥 Burn")
|
| 119 |
+
|
| 120 |
+
# SECTIONS
|
| 121 |
+
|
| 122 |
+
with gr.Column(visible=True) as upload_section:
|
| 123 |
+
with gr.Column(elem_classes="card"):
|
| 124 |
+
video_input = gr.Video()
|
| 125 |
+
upload_btn = gr.Button("Upload Video")
|
| 126 |
+
upload_status = gr.Textbox(label="Status")
|
| 127 |
+
|
| 128 |
+
upload_btn.click(
|
| 129 |
+
save_uploaded_video,
|
| 130 |
+
inputs=video_input,
|
| 131 |
+
outputs=upload_status
|
| 132 |
+
)
|
| 133 |
+
|
| 134 |
+
|
| 135 |
+
with gr.Column(visible=False) as summary_section:
|
| 136 |
+
with gr.Column(elem_classes="card"):
|
| 137 |
+
input_text = gr.Textbox(lines=5)
|
| 138 |
+
summarize_btn = gr.Button("Generate Summary")
|
| 139 |
+
output_text = gr.Textbox()
|
| 140 |
+
|
| 141 |
+
summarize_btn.click(
|
| 142 |
+
summarize_ui,
|
| 143 |
+
inputs=input_text,
|
| 144 |
+
outputs=output_text
|
| 145 |
+
)
|
| 146 |
+
|
| 147 |
+
|
| 148 |
+
with gr.Column(visible=False) as thumb_section:
|
| 149 |
+
with gr.Column(elem_classes="card"):
|
| 150 |
+
video_for_thumb = gr.Video()
|
| 151 |
+
title_text = gr.Textbox()
|
| 152 |
+
thumb_btn = gr.Button("Generate Thumbnail")
|
| 153 |
+
thumbnail_output = gr.Image()
|
| 154 |
+
|
| 155 |
+
thumb_btn.click(
|
| 156 |
+
create_thumbnail,
|
| 157 |
+
inputs=[video_for_thumb, title_text],
|
| 158 |
+
outputs=thumbnail_output
|
| 159 |
+
)
|
| 160 |
+
|
| 161 |
+
|
| 162 |
+
with gr.Column(visible=False) as subtitle_section:
|
| 163 |
+
with gr.Column(elem_classes="card"):
|
| 164 |
+
video_input_sub = gr.Video()
|
| 165 |
+
subtitle_btn = gr.Button("Generate Subtitles")
|
| 166 |
+
subtitle_file = gr.File()
|
| 167 |
+
|
| 168 |
+
subtitle_btn.click(
|
| 169 |
+
subtitle_ui,
|
| 170 |
+
inputs=video_input_sub,
|
| 171 |
+
outputs=subtitle_file
|
| 172 |
+
)
|
| 173 |
+
|
| 174 |
+
|
| 175 |
+
with gr.Column(visible=False) as composer_section:
|
| 176 |
+
with gr.Column(elem_classes="card"):
|
| 177 |
+
video_inputs = gr.File(file_types=[".mp4"], file_count="multiple")
|
| 178 |
+
compose_btn = gr.Button("Compose Video")
|
| 179 |
+
final_video = gr.Video()
|
| 180 |
+
|
| 181 |
+
compose_btn.click(
|
| 182 |
+
compose_ui,
|
| 183 |
+
inputs=video_inputs,
|
| 184 |
+
outputs=final_video
|
| 185 |
+
)
|
| 186 |
+
|
| 187 |
+
|
| 188 |
+
with gr.Column(visible=False) as burn_section:
|
| 189 |
+
with gr.Column(elem_classes="card"):
|
| 190 |
+
video_input_burn = gr.Video()
|
| 191 |
+
subtitle_input = gr.File()
|
| 192 |
+
burn_btn = gr.Button("Burn Subtitles")
|
| 193 |
+
output_video = gr.Video()
|
| 194 |
+
|
| 195 |
+
burn_btn.click(
|
| 196 |
+
burn_subtitles_ui,
|
| 197 |
+
inputs=[video_input_burn, subtitle_input],
|
| 198 |
+
outputs=output_video
|
| 199 |
+
)
|
| 200 |
+
|
| 201 |
+
|
| 202 |
+
# ---------- NAVIGATION FUNCTIONS (FIXED) ----------
|
| 203 |
+
|
| 204 |
+
def show_upload():
|
| 205 |
+
return (
|
| 206 |
+
gr.update(visible=True),
|
| 207 |
+
gr.update(visible=False),
|
| 208 |
+
gr.update(visible=False),
|
| 209 |
+
gr.update(visible=False),
|
| 210 |
+
gr.update(visible=False),
|
| 211 |
+
gr.update(visible=False),
|
| 212 |
+
)
|
| 213 |
+
|
| 214 |
+
|
| 215 |
+
def show_summary():
|
| 216 |
+
return (
|
| 217 |
+
gr.update(visible=False),
|
| 218 |
+
gr.update(visible=True),
|
| 219 |
+
gr.update(visible=False),
|
| 220 |
+
gr.update(visible=False),
|
| 221 |
+
gr.update(visible=False),
|
| 222 |
+
gr.update(visible=False),
|
| 223 |
+
)
|
| 224 |
+
|
| 225 |
+
|
| 226 |
+
def show_thumb():
|
| 227 |
+
return (
|
| 228 |
+
gr.update(visible=False),
|
| 229 |
+
gr.update(visible=False),
|
| 230 |
+
gr.update(visible=True),
|
| 231 |
+
gr.update(visible=False),
|
| 232 |
+
gr.update(visible=False),
|
| 233 |
+
gr.update(visible=False),
|
| 234 |
+
)
|
| 235 |
+
|
| 236 |
+
|
| 237 |
+
def show_subtitle():
|
| 238 |
+
return (
|
| 239 |
+
gr.update(visible=False),
|
| 240 |
+
gr.update(visible=False),
|
| 241 |
+
gr.update(visible=False),
|
| 242 |
+
gr.update(visible=True),
|
| 243 |
+
gr.update(visible=False),
|
| 244 |
+
gr.update(visible=False),
|
| 245 |
+
)
|
| 246 |
+
|
| 247 |
+
|
| 248 |
+
def show_composer():
|
| 249 |
+
return (
|
| 250 |
+
gr.update(visible=False),
|
| 251 |
+
gr.update(visible=False),
|
| 252 |
+
gr.update(visible=False),
|
| 253 |
+
gr.update(visible=False),
|
| 254 |
+
gr.update(visible=True),
|
| 255 |
+
gr.update(visible=False),
|
| 256 |
+
)
|
| 257 |
+
|
| 258 |
+
|
| 259 |
+
def show_burn():
|
| 260 |
+
return (
|
| 261 |
+
gr.update(visible=False),
|
| 262 |
+
gr.update(visible=False),
|
| 263 |
+
gr.update(visible=False),
|
| 264 |
+
gr.update(visible=False),
|
| 265 |
+
gr.update(visible=False),
|
| 266 |
+
gr.update(visible=True),
|
| 267 |
+
)
|
| 268 |
+
|
| 269 |
+
|
| 270 |
+
btn_upload.click(show_upload,
|
| 271 |
+
outputs=[upload_section, summary_section, thumb_section, subtitle_section, composer_section, burn_section])
|
| 272 |
+
|
| 273 |
+
btn_summary.click(show_summary,
|
| 274 |
+
outputs=[upload_section, summary_section, thumb_section, subtitle_section, composer_section, burn_section])
|
| 275 |
+
|
| 276 |
+
btn_thumbnail.click(show_thumb,
|
| 277 |
+
outputs=[upload_section, summary_section, thumb_section, subtitle_section, composer_section, burn_section])
|
| 278 |
+
|
| 279 |
+
btn_subtitle.click(show_subtitle,
|
| 280 |
+
outputs=[upload_section, summary_section, thumb_section, subtitle_section, composer_section, burn_section])
|
| 281 |
+
|
| 282 |
+
btn_composer.click(show_composer,
|
| 283 |
+
outputs=[upload_section, summary_section, thumb_section, subtitle_section, composer_section, burn_section])
|
| 284 |
+
|
| 285 |
+
btn_burn.click(show_burn,
|
| 286 |
+
outputs=[upload_section, summary_section, thumb_section, subtitle_section, composer_section, burn_section])
|
| 287 |
+
|
| 288 |
+
|
| 289 |
+
app.launch()
|
modules/composer.py
ADDED
|
@@ -0,0 +1,58 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from moviepy import VideoFileClip, concatenate_videoclips
|
| 2 |
+
import os
|
| 3 |
+
import shutil
|
| 4 |
+
|
| 5 |
+
OUTPUT_DIR = "assets/outputs"
|
| 6 |
+
UPLOAD_DIR = "assets/uploads"
|
| 7 |
+
|
| 8 |
+
os.makedirs(OUTPUT_DIR, exist_ok=True)
|
| 9 |
+
os.makedirs(UPLOAD_DIR, exist_ok=True)
|
| 10 |
+
|
| 11 |
+
def normalize_video(input_path, output_path):
|
| 12 |
+
"""
|
| 13 |
+
Re-encode video to constant frame rate and valid keyframe.
|
| 14 |
+
This fixes WhatsApp/mobile video issues on Windows.
|
| 15 |
+
"""
|
| 16 |
+
clip = VideoFileClip(input_path)
|
| 17 |
+
clip.write_videofile(
|
| 18 |
+
output_path,
|
| 19 |
+
codec="libx264",
|
| 20 |
+
audio_codec="aac",
|
| 21 |
+
fps=24,
|
| 22 |
+
preset="medium",
|
| 23 |
+
ffmpeg_params=["-movflags", "+faststart"]
|
| 24 |
+
)
|
| 25 |
+
clip.close()
|
| 26 |
+
|
| 27 |
+
def compose_videos(video_files):
|
| 28 |
+
clips = []
|
| 29 |
+
normalized_paths = []
|
| 30 |
+
|
| 31 |
+
for file in video_files:
|
| 32 |
+
filename = os.path.basename(file)
|
| 33 |
+
stable_path = os.path.join(UPLOAD_DIR, filename)
|
| 34 |
+
shutil.copy(file, stable_path)
|
| 35 |
+
|
| 36 |
+
normalized_path = os.path.join(
|
| 37 |
+
UPLOAD_DIR, f"normalized_{filename}"
|
| 38 |
+
)
|
| 39 |
+
|
| 40 |
+
normalize_video(stable_path, normalized_path)
|
| 41 |
+
normalized_paths.append(normalized_path)
|
| 42 |
+
|
| 43 |
+
for path in normalized_paths:
|
| 44 |
+
clips.append(VideoFileClip(path))
|
| 45 |
+
|
| 46 |
+
final_clip = concatenate_videoclips(clips)
|
| 47 |
+
output_path = os.path.join(OUTPUT_DIR, "final_video.mp4")
|
| 48 |
+
|
| 49 |
+
final_clip.write_videofile(
|
| 50 |
+
output_path,
|
| 51 |
+
codec="libx264",
|
| 52 |
+
audio_codec="aac"
|
| 53 |
+
)
|
| 54 |
+
|
| 55 |
+
for clip in clips:
|
| 56 |
+
clip.close()
|
| 57 |
+
|
| 58 |
+
return output_path
|
modules/frame_extractor.py
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import cv2
|
| 2 |
+
import os
|
| 3 |
+
|
| 4 |
+
OUTPUT_DIR = "assets/outputs"
|
| 5 |
+
os.makedirs(OUTPUT_DIR, exist_ok=True)
|
| 6 |
+
|
| 7 |
+
def extract_frame(video_path):
|
| 8 |
+
cap = cv2.VideoCapture(video_path)
|
| 9 |
+
|
| 10 |
+
success, frame = cap.read()
|
| 11 |
+
cap.release()
|
| 12 |
+
|
| 13 |
+
if not success:
|
| 14 |
+
raise ValueError("Could not read frame from video.")
|
| 15 |
+
|
| 16 |
+
frame_path = os.path.join(OUTPUT_DIR, "frame.jpg")
|
| 17 |
+
cv2.imwrite(frame_path, frame)
|
| 18 |
+
|
| 19 |
+
return frame_path
|
modules/subtitle_burner.py
ADDED
|
@@ -0,0 +1,40 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from moviepy import VideoFileClip, TextClip, CompositeVideoClip
|
| 2 |
+
import pysrt
|
| 3 |
+
import os
|
| 4 |
+
|
| 5 |
+
OUTPUT_DIR = "assets/outputs"
|
| 6 |
+
os.makedirs(OUTPUT_DIR, exist_ok=True)
|
| 7 |
+
|
| 8 |
+
def burn_subtitles(video_path, subtitle_path):
|
| 9 |
+
|
| 10 |
+
video = VideoFileClip(video_path)
|
| 11 |
+
subs = pysrt.open(subtitle_path)
|
| 12 |
+
|
| 13 |
+
subtitle_clips = []
|
| 14 |
+
|
| 15 |
+
for sub in subs:
|
| 16 |
+
|
| 17 |
+
start = sub.start.ordinal / 1000
|
| 18 |
+
end = sub.end.ordinal / 1000
|
| 19 |
+
|
| 20 |
+
txt_clip = TextClip(
|
| 21 |
+
text=sub.text,
|
| 22 |
+
font_size=40,
|
| 23 |
+
color="white",
|
| 24 |
+
stroke_color="black",
|
| 25 |
+
stroke_width=2,
|
| 26 |
+
size=(int(video.w * 0.9), None),
|
| 27 |
+
method="caption"
|
| 28 |
+
)
|
| 29 |
+
|
| 30 |
+
txt_clip = txt_clip.with_start(start).with_end(end).with_position(("center","bottom"))
|
| 31 |
+
|
| 32 |
+
subtitle_clips.append(txt_clip)
|
| 33 |
+
|
| 34 |
+
final = CompositeVideoClip([video] + subtitle_clips)
|
| 35 |
+
|
| 36 |
+
output_path = os.path.join(OUTPUT_DIR, "captioned_video.mp4")
|
| 37 |
+
|
| 38 |
+
final.write_videofile(output_path, codec="libx264")
|
| 39 |
+
|
| 40 |
+
return output_path
|
modules/subtitles.py
ADDED
|
@@ -0,0 +1,38 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from faster_whisper import WhisperModel
|
| 2 |
+
import os
|
| 3 |
+
|
| 4 |
+
OUTPUT_DIR = "assets/outputs"
|
| 5 |
+
os.makedirs(OUTPUT_DIR, exist_ok=True)
|
| 6 |
+
|
| 7 |
+
# Force CPU usage
|
| 8 |
+
model = WhisperModel("base", device="cpu", compute_type="int8")
|
| 9 |
+
|
| 10 |
+
def generate_subtitles(video_path):
|
| 11 |
+
|
| 12 |
+
segments, info = model.transcribe(video_path)
|
| 13 |
+
|
| 14 |
+
srt_path = os.path.join(OUTPUT_DIR, "subtitles.srt")
|
| 15 |
+
|
| 16 |
+
with open(srt_path, "w", encoding="utf-8") as srt:
|
| 17 |
+
|
| 18 |
+
for i, segment in enumerate(segments, start=1):
|
| 19 |
+
|
| 20 |
+
start = format_time(segment.start)
|
| 21 |
+
end = format_time(segment.end)
|
| 22 |
+
text = segment.text.strip()
|
| 23 |
+
|
| 24 |
+
srt.write(f"{i}\n")
|
| 25 |
+
srt.write(f"{start} --> {end}\n")
|
| 26 |
+
srt.write(f"{text}\n\n")
|
| 27 |
+
|
| 28 |
+
return srt_path
|
| 29 |
+
|
| 30 |
+
|
| 31 |
+
def format_time(seconds):
|
| 32 |
+
|
| 33 |
+
hrs = int(seconds // 3600)
|
| 34 |
+
mins = int((seconds % 3600) // 60)
|
| 35 |
+
secs = int(seconds % 60)
|
| 36 |
+
millis = int((seconds - int(seconds)) * 1000)
|
| 37 |
+
|
| 38 |
+
return f"{hrs:02}:{mins:02}:{secs:02},{millis:03}"
|
modules/summarizer.py
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from huggingface_hub import InferenceClient
|
| 2 |
+
import os
|
| 3 |
+
|
| 4 |
+
client = InferenceClient(token=os.getenv("HF_TOKEN"))
|
| 5 |
+
|
| 6 |
+
|
| 7 |
+
def summarize_text(text):
|
| 8 |
+
response = client.summarization(
|
| 9 |
+
text,
|
| 10 |
+
model="facebook/bart-large-cnn"
|
| 11 |
+
)
|
| 12 |
+
return response["summary_text"]
|
modules/thumbnail.py
ADDED
|
@@ -0,0 +1,48 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from PIL import Image, ImageDraw, ImageFont
|
| 2 |
+
import os
|
| 3 |
+
|
| 4 |
+
OUTPUT_DIR = "assets/outputs"
|
| 5 |
+
os.makedirs(OUTPUT_DIR, exist_ok=True)
|
| 6 |
+
|
| 7 |
+
def generate_thumbnail(image_path, text):
|
| 8 |
+
if not os.path.exists(image_path):
|
| 9 |
+
raise FileNotFoundError("Frame image not found.")
|
| 10 |
+
|
| 11 |
+
# 1. Load & resize to YouTube thumbnail size
|
| 12 |
+
img = Image.open(image_path).convert("RGB")
|
| 13 |
+
img = img.resize((1280, 720))
|
| 14 |
+
|
| 15 |
+
draw = ImageDraw.Draw(img)
|
| 16 |
+
|
| 17 |
+
# 2. Add dark gradient overlay (bottom)
|
| 18 |
+
overlay = Image.new("RGBA", img.size, (0, 0, 0, 0))
|
| 19 |
+
overlay_draw = ImageDraw.Draw(overlay)
|
| 20 |
+
|
| 21 |
+
for y in range(400, 720):
|
| 22 |
+
alpha = int((y - 400) / 320 * 180)
|
| 23 |
+
overlay_draw.rectangle(
|
| 24 |
+
[(0, y), (1280, y + 1)],
|
| 25 |
+
fill=(0, 0, 0, alpha)
|
| 26 |
+
)
|
| 27 |
+
|
| 28 |
+
img = Image.alpha_composite(img.convert("RGBA"), overlay).convert("RGB")
|
| 29 |
+
draw = ImageDraw.Draw(img)
|
| 30 |
+
|
| 31 |
+
# 3. Load font (fallback safe)
|
| 32 |
+
try:
|
| 33 |
+
font = ImageFont.truetype("arialbd.ttf", 80)
|
| 34 |
+
except:
|
| 35 |
+
font = ImageFont.load_default()
|
| 36 |
+
|
| 37 |
+
# 4. Draw bold title text (bottom-left)
|
| 38 |
+
x, y = 60, 500
|
| 39 |
+
|
| 40 |
+
# Text shadow
|
| 41 |
+
draw.text((x + 4, y + 4), text, font=font, fill="black")
|
| 42 |
+
draw.text((x, y), text, font=font, fill="white")
|
| 43 |
+
|
| 44 |
+
# 5. Save thumbnail
|
| 45 |
+
output_path = os.path.join(OUTPUT_DIR, "thumbnail.jpg")
|
| 46 |
+
img.save(output_path, quality=95)
|
| 47 |
+
|
| 48 |
+
return output_path
|
requirements.txt
ADDED
|
Binary file (2.62 kB). View file
|
|
|