import cv2 import numpy as np import gradio as gr import subprocess import os import time from pathlib import Path # --- CORE ARTISTIC FILTERS --- def apply_artistic_filter(img, style): try: if style == "Original (Direct)": return img if "Pencil" in style: gray = cv2.cvtColor(img, cv2.COLOR_RGB2GRAY) inv = 255 - gray blur = cv2.GaussianBlur(inv, (31, 31), 0) return cv2.cvtColor(cv2.divide(gray, 255 - blur, scale=256), cv2.COLOR_GRAY2RGB) elif "Charcoal" in style: gray = cv2.cvtColor(img, cv2.COLOR_RGB2GRAY) blur = cv2.medianBlur(gray, 7) return cv2.cvtColor(cv2.adaptiveThreshold(blur, 255, cv2.ADAPTIVE_THRESH_GAUSSIAN_C, cv2.THRESH_BINARY, 15, 5), cv2.COLOR_GRAY2RGB) elif "Watercolor" in style: if hasattr(cv2, 'stylization'): return cv2.stylization(img, sigma_s=60, sigma_r=0.6) return cv2.bilateralFilter(img, 15, 75, 75) elif style == "Neon Glow": edges = cv2.Canny(img, 100, 200) neon = cv2.cvtColor(edges, cv2.COLOR_GRAY2RGB) neon[:, :, 1], neon[:, :, 2] = edges, 255 return cv2.bitwise_and(neon, neon, mask=edges) return img except: return img # --- BRUSH TEXTURES --- def get_brush_mask(length, brush_type): if brush_type == "Sharp Row Reveal": return np.zeros(length, dtype=np.int32) elif brush_type == "Flat Brush": return np.random.randint(-2, 3, length) elif brush_type == "Rough Bristle": return np.random.randint(-15, 16, length) elif brush_type == "Fan Brush": x = np.linspace(0, 4 * np.pi, length) return (np.sin(x) * 15).astype(np.int32) return np.zeros(length, dtype=np.int32) # --- VIDEO GENERATION ENGINE --- def generate_video_engine(img_array, art_style, brush_texture, duration, direction, resolution, motion_effect, original_name): res_map = {"720p (HD)": (1280, 720), "1080p (Full HD)": (1920, 1080), "4K (Ultra HD)": (3840, 2160)} target_w, target_h = res_map[resolution] fps = 30 total_frames = int(duration * fps) paint_frames = int(total_frames * 0.8) resized_img = cv2.resize(img_array, (target_w, target_h), interpolation=cv2.INTER_LANCZOS4) target_img = apply_artistic_filter(resized_img, art_style) # Filename Logic temp_path = f'temp_{int(time.time() * 1000)}.mp4' final_filename = f"{original_name}.mp4" video = cv2.VideoWriter(temp_path, cv2.VideoWriter_fourcc(*'mp4v'), fps, (target_w, target_h)) mask_len = target_h if "Horizontal" in direction else target_w brush_offsets = get_brush_mask(mask_len, brush_texture) for i in range(total_frames): t = i / total_frames p_t = min(1.0, i / paint_frames) canvas_img = target_img.copy() # MOTION EFFECTS if motion_effect == "CapCut 3D Zoom": scale = 1.0 + (pow(t, 2) * 0.3) offset = int(target_w * 0.03 * t) src_pts = np.float32([[0,0], [target_w,0], [0,target_h], [target_w,target_h]]) dst_pts = np.float32([[-offset, -offset], [target_w+offset, -offset], [0,target_h], [target_w,target_h]]) M_3d = cv2.getPerspectiveTransform(src_pts, dst_pts) canvas_img = cv2.warpPerspective(target_img, M_3d, (target_w, target_h), borderMode=cv2.BORDER_REPLICATE) M_scale = cv2.getRotationMatrix2D((target_w/2, target_h/2), 0, scale) canvas_img = cv2.warpAffine(canvas_img, M_scale, (target_w, target_h), borderMode=cv2.BORDER_REPLICATE) elif motion_effect == "Spiral Cinematic": M = cv2.getRotationMatrix2D((target_w/2, target_h/2), t * 15, 1.0 + (t * 0.15)) canvas_img = cv2.warpAffine(target_img, M, (target_w, target_h), borderMode=cv2.BORDER_REPLICATE) elif motion_effect == "3D Parallax Tilt": shift = int(target_w * 0.05 * t) pts1 = np.float32([[0,0], [target_w,0], [0,target_h], [target_w,target_h]]) pts2 = np.float32([[shift,shift], [target_w-shift,0], [0,target_h], [target_w-shift,target_h-shift]]) M_p = cv2.getPerspectiveTransform(pts1, pts2) canvas_img = cv2.warpPerspective(target_img, M_p, (target_w, target_h), borderMode=cv2.BORDER_REPLICATE) # REVEAL LOGIC frame = np.ones((target_h, target_w, 3), dtype=np.uint8) * 255 if "Horizontal" in direction: base_w = int(p_t * target_w) for y in range(target_h): reveal_w = max(0, min(target_w, base_w + brush_offsets[y])) if reveal_w > 0: frame[y, 0:reveal_w] = canvas_img[y, 0:reveal_w] else: base_h = int(p_t * target_h) for x in range(target_w): reveal_h = max(0, min(target_h, base_h + brush_offsets[x])) if reveal_h > 0: frame[0:reveal_h, x] = canvas_img[0:reveal_h, x] video.write(cv2.cvtColor(frame, cv2.COLOR_RGB2BGR)) video.release() # Final Encode if os.path.exists(final_filename): os.remove(final_filename) subprocess.call(['ffmpeg', '-y', '-i', temp_path, '-vcodec', 'libx264', '-crf', '18', '-pix_fmt', 'yuv420p', final_filename]) if os.path.exists(temp_path): os.remove(temp_path) return final_filename # --- BATCH HANDLER --- def batch_handler(files, art_style, brush, duration, direction, resolution, motion): if not files: return None, None processed_videos = [] for f in files: # Extract original filename original_name = Path(f.name).stem img = cv2.imread(f.name) img = cv2.cvtColor(img, cv2.COLOR_BGR2RGB) # Pass name to engine video_path = generate_video_engine(img, art_style, brush, duration, direction, resolution, motion, original_name) processed_videos.append(video_path) return processed_videos, processed_videos[0] if processed_videos else None # --- UI --- with gr.Blocks() as demo: gr.Markdown("# 🎌 Ultimate Batch SpeedPaint Master") with gr.Row(): with gr.Column(): file_input = gr.File(file_count="multiple", label="Upload Images") motion_opt = gr.Dropdown(["None", "CapCut 3D Zoom", "Spiral Cinematic", "3D Parallax Tilt"], value="CapCut 3D Zoom", label="Motion Style") art_opt = gr.Dropdown(["Original (Direct)", "Soft Pencil", "Pencil Sketch", "Charcoal Dust", "Watercolor Wash", "Neon Glow"], value="Original (Direct)", label="Filter") brush_opt = gr.Dropdown(["Sharp Row Reveal", "Flat Brush", "Rough Bristle", "Fan Brush"], value="Rough Bristle", label="Brush") with gr.Row(): dur_slider = gr.Slider(2, 20, value=6, step=1, label="Seconds") res_opt = gr.Dropdown(["720p (HD)", "1080p (Full HD)", "4K (Ultra HD)"], value="1080p (Full HD)", label="Res") dir_opt = gr.Radio(["Vertical (Top-Bottom)", "Horizontal (Left-Right)"], value="Vertical (Top-Bottom)", label="Direction") run_btn = gr.Button("🚀 Start Batch Render", variant="primary") with gr.Column(): video_preview = gr.Video(label="Preview") file_download = gr.File(label="Download Videos (Named as Original Images)") run_btn.click( fn=batch_handler, inputs=[file_input, art_opt, brush_opt, dur_slider, dir_opt, res_opt, motion_opt], outputs=[file_download, video_preview] ) demo.launch()