|
|
|
|
|
|
|
|
import gradio as gr |
|
|
from fpdf import FPDF |
|
|
import os |
|
|
import re |
|
|
from datetime import datetime |
|
|
import tempfile |
|
|
from io import BytesIO |
|
|
from PIL import Image, ImageDraw, ImageFont |
|
|
|
|
|
|
|
|
try: |
|
|
from moviepy.editor import ImageClip, concatenate_videoclips, TextClip, CompositeVideoClip, ColorClip |
|
|
from moviepy.video.fx.all import crop |
|
|
VIDEO_ENABLED = True |
|
|
except ImportError: |
|
|
VIDEO_ENABLED = False |
|
|
print("MoviePy not available - using basic video generation") |
|
|
|
|
|
class GymWorkoutPDF(FPDF): |
|
|
def footer(self): |
|
|
self.set_y(-10) |
|
|
self.set_font('Arial', 'I', 7) |
|
|
self.cell(0, 8, f'GYM WORKOUT PLAN | Page {self.page_no()}', 0, 0, 'C') |
|
|
|
|
|
def clean_text(text, max_length=45): |
|
|
if not text: |
|
|
return "Workout" |
|
|
text = str(text) |
|
|
emoji_pattern = re.compile( |
|
|
"[" |
|
|
u"\U0001F600-\U0001F64F" |
|
|
u"\U0001F300-\U0001F5FF" |
|
|
u"\U0001F680-\U0001F6FF" |
|
|
u"\U0001F1E0-\U0001F1FF" |
|
|
"]+", |
|
|
flags=re.UNICODE |
|
|
) |
|
|
text = emoji_pattern.sub('', text) |
|
|
replacements = { |
|
|
'β': '-', 'β': '-', 'β': '"', 'β': '"', 'β’': '*', 'Β°': 'deg' |
|
|
} |
|
|
for old, new in replacements.items(): |
|
|
text = text.replace(old, new) |
|
|
text = re.sub(r'[^\x00-\x7F\u0020-\u007E]', '', text) |
|
|
text = re.sub(r'\s+', ' ', text.strip()) |
|
|
return text[:max_length] |
|
|
|
|
|
def create_screenshot_template(image_path, workout_name, workout_num, main_title): |
|
|
try: |
|
|
|
|
|
img = Image.open(image_path) |
|
|
img = img.resize((800, 600), Image.Resampling.LANCZOS) |
|
|
|
|
|
|
|
|
template = Image.new('RGB', (800, 600), color=(34, 139, 34)) |
|
|
|
|
|
draw = ImageDraw.Draw(template) |
|
|
|
|
|
|
|
|
try: |
|
|
font_large = ImageFont.truetype("arial.ttf", 48) |
|
|
font_medium = ImageFont.truetype("arial.ttf", 36) |
|
|
font_small = ImageFont.truetype("arial.ttf", 24) |
|
|
except: |
|
|
font_large = ImageFont.load_default() |
|
|
font_medium = ImageFont.load_default() |
|
|
font_small = ImageFont.load_default() |
|
|
|
|
|
clean_name = clean_text(workout_name) |
|
|
clean_title = clean_text(main_title) |
|
|
|
|
|
|
|
|
draw.text((50, 20), f"WORKOUT {workout_num}", fill="white", font=font_large) |
|
|
draw.text((50, 80), clean_name.upper(), fill="yellow", font=font_medium) |
|
|
draw.text((50, 120), f"PROGRAM: {clean_title}", fill="white", font=font_small) |
|
|
|
|
|
|
|
|
img_resized = img.resize((700, 400), Image.Resampling.LANCZOS) |
|
|
template.paste(img_resized, (50, 150)) |
|
|
|
|
|
|
|
|
draw.text((50, 540), "Workout Execution Plan - Print & Train!", fill="white", font=font_small) |
|
|
|
|
|
|
|
|
with tempfile.NamedTemporaryFile(suffix=".jpg", delete=False) as tmp_file: |
|
|
template_path = tmp_file.name |
|
|
template.save(template_path, "JPEG", quality=95) |
|
|
return template_path |
|
|
|
|
|
except Exception as e: |
|
|
print(f"Template error: {e}") |
|
|
return image_path |
|
|
|
|
|
def generate_content(main_title, workout_names, image_files): |
|
|
if not main_title.strip(): |
|
|
return None, None, "Error: Main Title Required!" |
|
|
|
|
|
names_list = [n.strip() for n in workout_names.split(',') if n.strip()] |
|
|
|
|
|
if not names_list: |
|
|
return None, None, "Error: Add Workout Names!" |
|
|
|
|
|
if len(names_list) > 30: |
|
|
return None, None, "Error: Max 30 Workouts!" |
|
|
|
|
|
if not image_files: |
|
|
return None, None, "Error: Upload at least 1 image!" |
|
|
|
|
|
available_images = len(image_files) |
|
|
status_msg = f"Using {available_images} images for {len(names_list)} workouts (placeholders for missing)" |
|
|
|
|
|
try: |
|
|
clean_main_title = clean_text(main_title, 50) |
|
|
pdf = GymWorkoutPDF() |
|
|
pdf.set_margins(18, 18, 18) |
|
|
|
|
|
|
|
|
pdf.add_page() |
|
|
pdf.set_font('Arial', 'B', 20) |
|
|
pdf.set_text_color(0, 128, 0) |
|
|
pdf.cell(0, 18, "GYM WORKOUT PROGRAM", ln=1, align='C') |
|
|
pdf.set_font('Arial', 'B', 16) |
|
|
pdf.cell(0, 15, clean_main_title.upper(), ln=1, align='C') |
|
|
|
|
|
pdf.ln(15) |
|
|
pdf.set_font('Arial', '', 11) |
|
|
pdf.cell(0, 10, f"TOTAL WORKOUTS: {len(names_list)}", ln=1, align='C') |
|
|
pdf.cell(0, 10, f"CREATED: {datetime.now().strftime('%d/%m/%Y')}", ln=1, align='C') |
|
|
|
|
|
template_paths = [] |
|
|
|
|
|
for i, workout_name in enumerate(names_list): |
|
|
pdf.add_page() |
|
|
pdf.set_font('Arial', 'B', 16) |
|
|
pdf.set_text_color(0, 128, 0) |
|
|
pdf.cell(0, 14, f"WORKOUT {i+1}", ln=1, align='C') |
|
|
|
|
|
clean_name = clean_text(workout_name, 40) |
|
|
pdf.set_font('Arial', 'B', 14) |
|
|
pdf.multi_cell(0, 12, clean_name.upper(), align='C') |
|
|
|
|
|
img_path = image_files[i] if i < available_images else None |
|
|
template_path = None |
|
|
|
|
|
if img_path and os.path.exists(img_path): |
|
|
template_path = create_screenshot_template( |
|
|
img_path, workout_name, i+1, main_title |
|
|
) |
|
|
template_paths.append(template_path) |
|
|
|
|
|
|
|
|
img_width = 110 |
|
|
x_pos = (pdf.w - img_width) / 2 |
|
|
pdf.image(template_path, x=x_pos, y=pdf.get_y(), w=img_width, h=130) |
|
|
pdf.set_draw_color(0, 128, 0) |
|
|
pdf.rect(x_pos, pdf.get_y(), img_width, 130, 'D') |
|
|
else: |
|
|
pdf.set_fill_color(240, 248, 240) |
|
|
pdf.set_draw_color(0, 128, 0) |
|
|
pdf.rect(x_pos, pdf.get_y(), img_width, 130, 'FD') |
|
|
pdf.set_font('Arial', 'B', 11) |
|
|
pdf.set_xy(x_pos + 10, pdf.get_y() + 50) |
|
|
pdf.cell(90, 10, f"WORKOUT {i+1}", ln=1, align='C') |
|
|
|
|
|
|
|
|
with tempfile.NamedTemporaryFile(suffix=".pdf", delete=False) as tmp_pdf: |
|
|
pdf_path = tmp_pdf.name |
|
|
pdf.output(pdf_path) |
|
|
|
|
|
|
|
|
video_path = None |
|
|
if VIDEO_ENABLED and template_paths: |
|
|
try: |
|
|
clips = [] |
|
|
hd_width, hd_height = 1920, 1080 |
|
|
duration_per_slide = 6 |
|
|
|
|
|
|
|
|
title_clip = ColorClip(size=(hd_width, hd_height), color=(0, 128, 0)).set_duration(4) |
|
|
title_text = TextClip( |
|
|
f"GYM WORKOUT PROGRAM\n{clean_main_title.upper()}", |
|
|
fontsize=100, color='white', font='Arial-Bold' |
|
|
).set_position('center').set_duration(4) |
|
|
title_slide = CompositeVideoClip([title_clip, title_text]) |
|
|
clips.append(title_slide) |
|
|
|
|
|
|
|
|
for i, template_img in enumerate(template_paths): |
|
|
if os.path.exists(template_img): |
|
|
img_clip = ImageClip(template_img).resize(height=hd_height) |
|
|
img_clip = img_clip.set_duration(duration_per_slide) |
|
|
|
|
|
img_clip = img_clip.fx(crop, x1=100, y1=50, x2=hd_width-100, y2=hd_height-100) |
|
|
|
|
|
clips.append(img_clip) |
|
|
|
|
|
if clips: |
|
|
video = concatenate_videoclips(clips, method="compose") |
|
|
with tempfile.NamedTemporaryFile(suffix=".mp4", delete=False) as tmp_video: |
|
|
video_path = tmp_video.name |
|
|
video.write_videofile( |
|
|
video_path, |
|
|
fps=24, |
|
|
codec='libx264', |
|
|
audio=False, |
|
|
verbose=False, |
|
|
logger=None |
|
|
) |
|
|
video.close() |
|
|
|
|
|
except Exception as e: |
|
|
print(f"Video error: {e}") |
|
|
video_path = None |
|
|
|
|
|
|
|
|
for path in template_paths: |
|
|
try: |
|
|
os.remove(path) |
|
|
except: |
|
|
pass |
|
|
|
|
|
video_status = "HD Video Generated!" if video_path else "Video Generation Failed - Check MoviePy/FFmpeg" |
|
|
success_msg = f""" |
|
|
SUCCESS! Generated for {len(names_list)} workouts! |
|
|
PDF with generated screenshot templates |
|
|
{video_status} |
|
|
Images processed: {available_images} |
|
|
""" |
|
|
|
|
|
return pdf_path, video_path, success_msg |
|
|
|
|
|
except Exception as e: |
|
|
return None, None, f"Error: {str(e)}" |
|
|
|
|
|
|
|
|
with gr.Blocks(title="Gym Workout Generator") as demo: |
|
|
|
|
|
gr.Markdown("# Gym Workout Generator\nPDF + HD Video + Screenshot Templates") |
|
|
|
|
|
gr.Markdown(""" |
|
|
### How to Use: |
|
|
1. Enter Title |
|
|
2. Add Workout Names (comma separated) |
|
|
3. Upload Images/Screenshots (optional - placeholders for missing) |
|
|
4. Get PDF + Video with generated templates! |
|
|
|
|
|
Tip: Uploaded images get auto-converted to gym screenshot templates! |
|
|
""") |
|
|
|
|
|
main_title = gr.Textbox(label="MAIN PROGRAM TITLE *") |
|
|
workout_names = gr.Textbox(label="WORKOUT NAMES * (Comma separated)") |
|
|
|
|
|
images_input = gr.File(label="Images/Screenshots (Optional)", file_count="multiple", file_types=["image"]) |
|
|
|
|
|
generate_btn = gr.Button("Generate") |
|
|
|
|
|
pdf_output = gr.File(label="PDF") |
|
|
video_output = gr.File(label="HD Video") |
|
|
status = gr.Markdown("Ready...") |
|
|
|
|
|
generate_btn.click( |
|
|
fn=generate_content, |
|
|
inputs=[main_title, workout_names, images_input], |
|
|
outputs=[pdf_output, video_output, status] |
|
|
) |
|
|
|
|
|
if __name__ == "__main__": |
|
|
demo.launch() |