Spaces:
Runtime error
Runtime error
| import streamlit as st | |
| import cv2 | |
| import numpy as np | |
| from PIL import Image, ImageDraw, ImageFont | |
| from sklearn.cluster import KMeans | |
| import io | |
| import tempfile | |
| import os | |
| from pathlib import Path | |
| import gc | |
| # Configure page | |
| st.set_page_config( | |
| page_title="Live Drawing Studio", | |
| page_icon="π¨", | |
| layout="wide" | |
| ) | |
| # Custom CSS | |
| st.markdown(""" | |
| <style> | |
| .main { | |
| background: linear-gradient(135deg, #1a0b2e 0%, #2d1b4e 100%); | |
| } | |
| .stApp { | |
| background: linear-gradient(135deg, #1a0b2e 0%, #2d1b4e 100%); | |
| } | |
| h1 { | |
| color: #e0e0ff; | |
| text-align: center; | |
| font-size: 3rem; | |
| margin-bottom: 2rem; | |
| text-shadow: 3px 3px 6px rgba(0,0,0,0.5); | |
| font-weight: 700; | |
| letter-spacing: 2px; | |
| } | |
| .upload-section { | |
| background: rgba(25, 15, 45, 0.95); | |
| padding: 2rem; | |
| border-radius: 15px; | |
| box-shadow: 0 8px 32px rgba(0,0,0,0.3); | |
| border: 1px solid rgba(138, 92, 246, 0.3); | |
| } | |
| .stButton>button { | |
| width: 100%; | |
| background: linear-gradient(135deg, #6a11cb 0%, #2575fc 100%); | |
| color: white; | |
| font-size: 1.2rem; | |
| padding: 0.75rem; | |
| border-radius: 10px; | |
| border: none; | |
| font-weight: bold; | |
| transition: all 0.3s; | |
| box-shadow: 0 4px 15px rgba(106, 17, 203, 0.4); | |
| } | |
| .stButton>button:hover { | |
| transform: translateY(-2px); | |
| box-shadow: 0 6px 20px rgba(106, 17, 203, 0.6); | |
| background: linear-gradient(135deg, #7c20db 0%, #3585fc 100%); | |
| } | |
| .stSlider { | |
| padding: 10px 0; | |
| } | |
| div[data-baseweb="select"] > div { | |
| background-color: rgba(45, 27, 78, 0.8); | |
| border-color: rgba(138, 92, 246, 0.4); | |
| } | |
| div[data-baseweb="input"] > div { | |
| background-color: rgba(45, 27, 78, 0.8); | |
| border-color: rgba(138, 92, 246, 0.4); | |
| } | |
| .stTextArea textarea { | |
| background-color: rgba(45, 27, 78, 0.8); | |
| border-color: rgba(138, 92, 246, 0.4); | |
| color: #e0e0ff; | |
| } | |
| h2, h3 { | |
| color: #c7b8ea; | |
| font-weight: 600; | |
| } | |
| .stProgress > div > div { | |
| background: linear-gradient(90deg, #6a11cb 0%, #2575fc 100%); | |
| } | |
| label { | |
| color: #b8a8d8 !important; | |
| font-weight: 500; | |
| } | |
| </style> | |
| """, unsafe_allow_html=True) | |
| def detect_best_aspect_ratio(image): | |
| """Detect the best aspect ratio for the image""" | |
| height, width = image.shape[:2] | |
| current_ratio = width / height | |
| ratios = { | |
| "16:9": 16/9, | |
| "9:16": 9/16, | |
| "4:5": 4/5, | |
| "1:1": 1 | |
| } | |
| # Find closest ratio | |
| best_ratio = min(ratios.items(), key=lambda x: abs(x[1] - current_ratio)) | |
| return best_ratio[0], current_ratio | |
| def extract_dominant_colors(image, n_colors=3): | |
| """Extract dominant neon-suitable colors from the image""" | |
| # Resize for faster processing | |
| small = cv2.resize(image, (150, 150)) | |
| pixels = small.reshape(-1, 3).astype(np.float32) | |
| # Remove very dark pixels (likely background) | |
| brightness = pixels.mean(axis=1) | |
| bright_pixels = pixels[brightness > 30] | |
| if len(bright_pixels) < 10: | |
| # Fallback to default neon colors | |
| return [(255, 0, 128), (0, 255, 255), (255, 128, 0)] | |
| # Cluster to find dominant colors | |
| kmeans = KMeans(n_clusters=min(n_colors, len(bright_pixels)), random_state=42, n_init=10) | |
| kmeans.fit(bright_pixels) | |
| colors = kmeans.cluster_centers_.astype(int) | |
| # Enhance colors for neon effect (increase saturation and brightness) | |
| enhanced_colors = [] | |
| for color in colors: | |
| # Convert BGR to HSV | |
| bgr = np.uint8([[color]]) | |
| hsv = cv2.cvtColor(bgr, cv2.COLOR_BGR2HSV)[0][0] | |
| # Boost saturation and value for neon look | |
| hsv[1] = min(255, int(hsv[1] * 1.5)) # Saturation | |
| hsv[2] = min(255, int(hsv[2] * 1.3)) # Brightness | |
| # Convert back to BGR | |
| enhanced_bgr = cv2.cvtColor(np.uint8([[hsv]]), cv2.COLOR_HSV2BGR)[0][0] | |
| enhanced_colors.append(tuple(map(int, enhanced_bgr))) | |
| return enhanced_colors | |
| def resize_image_smart(image, target_width=1920, target_height=1080): | |
| """Smart resize that maintains aspect ratio and fits within target dimensions""" | |
| height, width = image.shape[:2] | |
| # Calculate scaling factor to fit within target dimensions | |
| width_scale = target_width / width | |
| height_scale = target_height / height | |
| scale = min(width_scale, height_scale, 1.0) # Don't upscale | |
| if scale < 1.0: | |
| new_width = int(width * scale) | |
| new_height = int(height * scale) | |
| image = cv2.resize(image, (new_width, new_height), interpolation=cv2.INTER_AREA) | |
| return image | |
| def edge_detection_improved(image, method='canny'): | |
| """Improved edge detection that preserves image details""" | |
| gray = cv2.cvtColor(image, cv2.COLOR_BGR2GRAY) | |
| # Gentle contrast enhancement | |
| clahe = cv2.createCLAHE(clipLimit=1.5, tileGridSize=(8, 8)) | |
| gray = clahe.apply(gray) | |
| if method == 'canny': | |
| # Fine-tuned Canny for better detail preservation | |
| blurred = cv2.GaussianBlur(gray, (3, 3), 0) | |
| edges = cv2.Canny(blurred, 50, 150) | |
| elif method == 'pencil': | |
| gray_blur = cv2.GaussianBlur(gray, (21, 21), 0) | |
| edges = cv2.divide(gray, gray_blur, scale=256.0) | |
| edges = 255 - edges | |
| edges = cv2.threshold(edges, 200, 255, cv2.THRESH_BINARY)[1] | |
| elif method == 'contour': | |
| blurred = cv2.GaussianBlur(gray, (3, 3), 0) | |
| edges = cv2.Canny(blurred, 50, 150) | |
| else: # adaptive | |
| blurred = cv2.GaussianBlur(gray, (3, 3), 0) | |
| edges = cv2.adaptiveThreshold( | |
| blurred, 255, cv2.ADAPTIVE_THRESH_GAUSSIAN_C, | |
| cv2.THRESH_BINARY_INV, 9, 2 | |
| ) | |
| # Only minimal processing to keep edges thin | |
| kernel = np.ones((2, 2), np.uint8) | |
| edges = cv2.morphologyEx(edges, cv2.MORPH_CLOSE, kernel, iterations=1) | |
| return edges | |
| def find_contour_drawing_order(edges): | |
| """Find contours and create a natural drawing order""" | |
| # Use CHAIN_APPROX_NONE to get all contour points for smooth drawing | |
| contours, _ = cv2.findContours(edges, cv2.RETR_LIST, cv2.CHAIN_APPROX_NONE) | |
| if not contours: | |
| return None | |
| # Sort contours by area (largest first) | |
| contours = sorted(contours, key=lambda c: cv2.contourArea(c), reverse=True) | |
| # Convert contours to drawing strokes | |
| strokes = [] | |
| for contour in contours: | |
| if len(contour) > 10: # Skip very small contours | |
| # Get all points for smooth continuous drawing | |
| points = contour.reshape(-1, 2) | |
| strokes.append(points) | |
| return strokes | |
| def create_enhanced_neon_glow(edge_image, colors, glow_size=20): | |
| """Create multi-layered neon glow effect with blended colors""" | |
| height, width = edge_image.shape | |
| result = np.zeros((height, width, 3), dtype=np.float32) | |
| # Find edge pixels | |
| edge_pixels = edge_image > 127 | |
| if not edge_pixels.any(): | |
| return result.astype(np.uint8) | |
| # Blend all colors together for more vibrant effect | |
| if len(colors) > 0: | |
| # Average the colors for base | |
| avg_color = np.mean(colors, axis=0) | |
| # Create colored edge image | |
| colored = np.zeros((height, width, 3), dtype=np.float32) | |
| colored[edge_pixels] = avg_color | |
| # Multi-layer glow with decreasing size and intensity | |
| for layer in range(5): | |
| blur_size = glow_size - (layer * 3) | |
| if blur_size < 3: | |
| blur_size = 3 | |
| blur_size = blur_size if blur_size % 2 == 1 else blur_size + 1 | |
| intensity = 1.2 - (layer * 0.15) # Stronger glow | |
| glow_layer = cv2.GaussianBlur(colored, (blur_size, blur_size), 0) | |
| result += glow_layer * intensity | |
| # Add individual color highlights for variety | |
| if len(colors) > 1: | |
| for i, color in enumerate(colors): | |
| colored_single = np.zeros((height, width, 3), dtype=np.float32) | |
| colored_single[edge_pixels] = color | |
| # Smaller, more focused glow for each color | |
| blur_size = max(5, glow_size // 2) | |
| blur_size = blur_size if blur_size % 2 == 1 else blur_size + 1 | |
| single_glow = cv2.GaussianBlur(colored_single, (blur_size, blur_size), 0) | |
| result += single_glow * 0.3 | |
| # Add bright white core for intensity | |
| core = np.zeros((height, width, 3), dtype=np.float32) | |
| core[edge_pixels] = [255, 255, 255] | |
| core_blur = cv2.GaussianBlur(core, (5, 5), 0) | |
| result += core_blur * 0.6 | |
| result = np.clip(result, 0, 255).astype(np.uint8) | |
| return result | |
| def create_human_like_drawing(image, edges, strokes, num_frames, colors, glow_size=20, bg_color=(0, 0, 0), hold_drawn_frames=0, hold_final_frames=0): | |
| """Create drawing animation that progressively reveals the original image with accurate colors""" | |
| height, width = edges.shape | |
| frames = [] | |
| # Create black background | |
| bg = np.zeros((height, width, 3), dtype=np.uint8) | |
| # Create a mask for progressive revealing | |
| reveal_mask = np.zeros((height, width), dtype=np.uint8) | |
| if strokes is None or len(strokes) == 0: | |
| st.warning("No strokes detected. Using progressive reveal method.") | |
| # Fallback: Reveal progressively from edge pixels | |
| edge_pixels = np.column_stack(np.where(edges > 127)) | |
| if len(edge_pixels) == 0: | |
| return [bg] * 20 | |
| # Sort for natural progression | |
| edge_pixels = edge_pixels[np.lexsort((edge_pixels[:, 1], edge_pixels[:, 0]))] | |
| pixels_per_frame = max(5, len(edge_pixels) // num_frames) | |
| for i in range(num_frames): | |
| start_idx = i * pixels_per_frame | |
| end_idx = min((i + 1) * pixels_per_frame, len(edge_pixels)) | |
| # Reveal pixels with thin lines | |
| for y, x in edge_pixels[start_idx:end_idx]: | |
| cv2.circle(reveal_mask, (x, y), 1, 255, -1) | |
| # Create frame by blending revealed original image | |
| frame = bg.copy() | |
| # Dilate mask slightly for better coverage | |
| display_mask = cv2.dilate(reveal_mask, np.ones((5, 5), np.uint8), iterations=1) | |
| mask_bool = display_mask > 0 | |
| # Copy original image colors exactly where mask is true | |
| frame[mask_bool] = image[mask_bool] | |
| frames.append(frame) | |
| if i % 10 == 0: | |
| gc.collect() | |
| else: | |
| # Draw stroke by stroke with thin lines | |
| total_points = sum(len(stroke) for stroke in strokes) | |
| points_per_frame = max(3, total_points // num_frames) | |
| frame_count = 0 | |
| stroke_idx = 0 | |
| point_idx = 0 | |
| while frame_count < num_frames and stroke_idx < len(strokes): | |
| points_this_frame = 0 | |
| # Draw multiple line segments per frame | |
| while points_this_frame < points_per_frame and stroke_idx < len(strokes): | |
| stroke = strokes[stroke_idx] | |
| points_to_draw = min(5, len(stroke) - point_idx) | |
| for i in range(points_to_draw - 1): | |
| if point_idx + i + 1 < len(stroke): | |
| pt1 = tuple(stroke[point_idx + i].astype(int)) | |
| pt2 = tuple(stroke[point_idx + i + 1].astype(int)) | |
| # Draw thin lines (thickness 1) | |
| cv2.line(reveal_mask, pt1, pt2, 255, 1, cv2.LINE_AA) | |
| point_idx += points_to_draw | |
| points_this_frame += points_to_draw | |
| if point_idx >= len(stroke) - 1: | |
| stroke_idx += 1 | |
| point_idx = 0 | |
| break | |
| # Create frame by revealing original image | |
| frame = bg.copy() | |
| # Dilate mask for better coverage | |
| display_mask = cv2.dilate(reveal_mask, np.ones((5, 5), np.uint8), iterations=1) | |
| mask_bool = display_mask > 0 | |
| # Copy exact colors from original image | |
| frame[mask_bool] = image[mask_bool] | |
| frames.append(frame) | |
| frame_count += 1 | |
| if frame_count % 10 == 0: | |
| gc.collect() | |
| # Hold the drawn image (last frame with revealed parts) | |
| if hold_drawn_frames > 0: | |
| drawn_final = frames[-1].copy() | |
| frames.extend([drawn_final] * hold_drawn_frames) | |
| # Add final complete frame - show 100% original image | |
| final_frame = image.copy() | |
| frames.extend([final_frame] * max(hold_final_frames, 25)) # Hold for specified frames or minimum 25 | |
| gc.collect() | |
| return frames | |
| def resize_to_ratio(image, ratio): | |
| """Resize image to specified aspect ratio with padding instead of cropping""" | |
| height, width = image.shape[:2] | |
| if ratio == "16:9": | |
| target_ratio = 16 / 9 | |
| elif ratio == "9:16": | |
| target_ratio = 9 / 16 | |
| elif ratio == "4:5": | |
| target_ratio = 4 / 5 | |
| else: # 1:1 | |
| target_ratio = 1 | |
| current_ratio = width / height | |
| # Calculate new dimensions with padding | |
| if current_ratio > target_ratio: | |
| # Image is wider - fit width | |
| new_width = width | |
| new_height = int(width / target_ratio) | |
| else: | |
| # Image is taller - fit height | |
| new_height = height | |
| new_width = int(height * target_ratio) | |
| # Create canvas with padding | |
| canvas = np.zeros((new_height, new_width, 3), dtype=np.uint8) | |
| # Center the image | |
| y_offset = (new_height - height) // 2 | |
| x_offset = (new_width - width) // 2 | |
| canvas[y_offset:y_offset + height, x_offset:x_offset + width] = image | |
| return canvas | |
| def create_outro_frame(text, width, height, bg_color=(10, 10, 15), | |
| text_color=(255, 255, 255), logo_image=None): | |
| """Create outro frame with text and optional logo""" | |
| img = Image.new('RGB', (width, height), bg_color) | |
| draw = ImageDraw.Draw(img) | |
| # Add logo if provided | |
| if logo_image is not None: | |
| try: | |
| logo = Image.open(logo_image) | |
| logo_size = min(width, height) // 3 | |
| logo.thumbnail((logo_size, logo_size), Image.Resampling.LANCZOS) | |
| logo_x = (width - logo.width) // 2 | |
| logo_y = height // 5 | |
| if logo.mode == 'RGBA': | |
| img.paste(logo, (logo_x, logo_y), logo) | |
| else: | |
| img.paste(logo, (logo_x, logo_y)) | |
| except Exception as e: | |
| st.warning(f"Could not load logo: {e}") | |
| # Add text with better formatting | |
| try: | |
| font_size = max(30, min(width, height) // 15) | |
| try: | |
| font = ImageFont.truetype("arial.ttf", font_size) | |
| except: | |
| try: | |
| font = ImageFont.truetype("C:/Windows/Fonts/arial.ttf", font_size) | |
| except: | |
| font = ImageFont.load_default() | |
| # Wrap text | |
| words = text.split() | |
| lines = [] | |
| current_line = [] | |
| for word in words: | |
| test_line = ' '.join(current_line + [word]) | |
| bbox = draw.textbbox((0, 0), test_line, font=font) | |
| if bbox[2] - bbox[0] < width * 0.85: | |
| current_line.append(word) | |
| else: | |
| if current_line: | |
| lines.append(' '.join(current_line)) | |
| current_line = [word] | |
| if current_line: | |
| lines.append(' '.join(current_line)) | |
| # Draw text with glow | |
| text_y = height // 2 if logo_image is None else height // 2 + height // 10 | |
| for i, line in enumerate(lines): | |
| bbox = draw.textbbox((0, 0), line, font=font) | |
| text_width = bbox[2] - bbox[0] | |
| x = (width - text_width) // 2 | |
| y = text_y + i * (font_size + 15) | |
| # Glow effect | |
| for offset_x in range(-3, 4): | |
| for offset_y in range(-3, 4): | |
| if offset_x != 0 or offset_y != 0: | |
| dist = np.sqrt(offset_x**2 + offset_y**2) | |
| alpha = int(100 * (1 - dist / 4)) | |
| draw.text((x + offset_x, y + offset_y), line, | |
| fill=(alpha, alpha, alpha + 20), font=font) | |
| # Main text | |
| draw.text((x, y), line, fill=text_color, font=font) | |
| except Exception as e: | |
| draw.text((width // 4, height // 2), text[:50], fill=text_color) | |
| return cv2.cvtColor(np.array(img), cv2.COLOR_RGB2BGR) | |
| def add_audio_to_video(video_path, audio_path, output_path, start_time=0.0, fadeout_duration=2.0): | |
| """Add audio to video using ffmpeg with start time and fade out""" | |
| import subprocess | |
| try: | |
| # Build ffmpeg command with audio filters | |
| audio_filters = [] | |
| # Add fade out filter | |
| if fadeout_duration > 0: | |
| # Get video duration to calculate fade start | |
| probe_cmd = [ | |
| 'ffprobe', '-v', 'error', '-show_entries', | |
| 'format=duration', '-of', | |
| 'default=noprint_wrappers=1:nokey=1', video_path | |
| ] | |
| try: | |
| result = subprocess.run(probe_cmd, capture_output=True, text=True, timeout=10) | |
| video_duration = float(result.stdout.strip()) | |
| fade_start = max(0, video_duration - fadeout_duration) | |
| audio_filters.append(f"afade=t=out:st={fade_start}:d={fadeout_duration}") | |
| except: | |
| # If can't get duration, use default fade | |
| audio_filters.append(f"afade=t=out:d={fadeout_duration}") | |
| # Combine filters | |
| filter_str = ",".join(audio_filters) if audio_filters else None | |
| cmd = [ | |
| 'ffmpeg', '-y', '-hide_banner', '-loglevel', 'error', | |
| '-i', video_path, | |
| '-ss', str(start_time), # Start audio from this time | |
| '-i', audio_path, | |
| '-c:v', 'copy', # Copy video without re-encoding | |
| '-c:a', 'aac', | |
| '-b:a', '192k', | |
| ] | |
| if filter_str: | |
| cmd.extend(['-af', filter_str]) | |
| cmd.extend(['-shortest', output_path]) | |
| result = subprocess.run(cmd, capture_output=True, text=True, timeout=300) | |
| if result.returncode != 0: | |
| st.warning(f"Audio mixing warning: {result.stderr}") | |
| return False | |
| return True | |
| except FileNotFoundError: | |
| st.error("FFmpeg not found. Please install FFmpeg to add audio.") | |
| return False | |
| except subprocess.TimeoutExpired: | |
| st.error("Audio processing timeout. Try a shorter audio file.") | |
| return False | |
| except Exception as e: | |
| st.error(f"Audio error: {str(e)}") | |
| return False | |
| def create_video(frames, fps, output_path, aspect_ratio): | |
| """Create video from frames""" | |
| if not frames: | |
| return False | |
| try: | |
| # Get dimensions from first frame | |
| sample_frame = resize_to_ratio(frames[0], aspect_ratio) | |
| height, width = sample_frame.shape[:2] | |
| # Initialize video writer with better codec | |
| fourcc = cv2.VideoWriter_fourcc(*'mp4v') | |
| out = cv2.VideoWriter(output_path, fourcc, fps, (width, height)) | |
| if not out.isOpened(): | |
| st.error("Could not open video writer") | |
| return False | |
| # Write frames | |
| for frame in frames: | |
| resized_frame = resize_to_ratio(frame, aspect_ratio) | |
| if resized_frame.shape[:2] != (height, width): | |
| resized_frame = cv2.resize(resized_frame, (width, height)) | |
| out.write(resized_frame) | |
| out.release() | |
| gc.collect() | |
| return True | |
| except Exception as e: | |
| st.error(f"Video creation error: {str(e)}") | |
| return False | |
| # Main App | |
| st.markdown("<h1>π¨ Turn your Chat GPT neon images into live drawing videos</h1>", unsafe_allow_html=True) | |
| # Initialize session state | |
| if 'video_generated' not in st.session_state: | |
| st.session_state.video_generated = False | |
| if 'video_path' not in st.session_state: | |
| st.session_state.video_path = None | |
| # Layout | |
| col1, col2 = st.columns([1, 1]) | |
| with col1: | |
| st.markdown("<div class='upload-section'>", unsafe_allow_html=True) | |
| st.subheader("π€ Upload Image") | |
| uploaded_file = st.file_uploader("Choose an image", type=['png', 'jpg', 'jpeg']) | |
| if uploaded_file: | |
| image = Image.open(uploaded_file) | |
| st.image(image, caption="Original Image", use_column_width="always") | |
| # Auto-detect best aspect ratio | |
| image_array = np.array(image) | |
| image_cv = cv2.cvtColor(image_array, cv2.COLOR_RGB2BGR) | |
| best_ratio, current_ratio = detect_best_aspect_ratio(image_cv) | |
| st.success(f"π **Recommended Aspect Ratio:** {best_ratio}") | |
| st.info(f"βΉοΈ Current image ratio: {current_ratio:.2f}:1") | |
| st.markdown("</div>", unsafe_allow_html=True) | |
| with col2: | |
| st.markdown("<div class='upload-section'>", unsafe_allow_html=True) | |
| st.subheader("βοΈ Settings") | |
| # Simple settings | |
| duration = st.slider("Animation Duration (seconds)", 5, 60, 10) | |
| col_hold1, col_hold2 = st.columns(2) | |
| with col_hold1: | |
| hold_drawn = st.slider("Hold Drawn Image (sec)", 0, 10, 3) | |
| with col_hold2: | |
| hold_final = st.slider("Hold Final Image (sec)", 0, 10, 2) | |
| st.markdown("</div>", unsafe_allow_html=True) | |
| # Auto-set these values (no user input needed) | |
| edge_method = 'canny' | |
| auto_color = True | |
| glow_intensity = 20 | |
| bg_darkness = 0 | |
| bg_color = (0, 0, 0) # Pure black background | |
| # Video Settings | |
| st.markdown("<div class='upload-section'>", unsafe_allow_html=True) | |
| st.subheader("π¬ Video Settings") | |
| col6, col7 = st.columns(2) | |
| with col6: | |
| aspect_ratio = st.selectbox("Aspect Ratio", ["16:9", "9:16", "4:5", "1:1"]) | |
| st.markdown("---") | |
| st.subheader("π΅ Background Audio") | |
| audio_file = st.file_uploader("Upload Audio (Optional)", type=['mp3', 'wav', 'ogg', 'm4a']) | |
| if audio_file: | |
| # Audio preview | |
| st.audio(audio_file, format=f'audio/{audio_file.name.split(".")[-1]}') | |
| # Audio controls | |
| col_audio1, col_audio2 = st.columns(2) | |
| with col_audio1: | |
| audio_start_time = st.number_input( | |
| "Start Time (seconds)", | |
| min_value=0.0, | |
| max_value=300.0, | |
| value=0.0, | |
| step=0.5, | |
| help="Audio will start from this time" | |
| ) | |
| with col_audio2: | |
| audio_fadeout = st.number_input( | |
| "Fade Out Duration (sec)", | |
| min_value=0.0, | |
| max_value=10.0, | |
| value=2.0, | |
| step=0.5, | |
| help="Smooth fade out at the end" | |
| ) | |
| with col7: | |
| fps = st.slider("Frame Rate (FPS)", 24, 60, 30) | |
| max_resolution = st.selectbox("Output Resolution", | |
| ["1080p (1920x1080)", "720p (1280x720)", "4K (3840x2160)"], | |
| index=1) | |
| st.markdown("</div>", unsafe_allow_html=True) | |
| # Outro settings | |
| st.markdown("<div class='upload-section'>", unsafe_allow_html=True) | |
| st.subheader("π¬ Outro Settings (Optional)") | |
| col8, col9 = st.columns([2, 1]) | |
| with col8: | |
| outro_text = st.text_area("Outro Text", | |
| "Thank you for watching!\nSubscribe for more!") | |
| with col9: | |
| outro_logo = st.file_uploader("Logo (Optional)", type=['png', 'jpg', 'jpeg']) | |
| outro_duration = st.slider("Outro Duration (sec)", 2, 10, 5) | |
| st.markdown("</div>", unsafe_allow_html=True) | |
| # Generate button | |
| if st.button("π¬ Generate Neon Drawing Video", type="primary"): | |
| if not uploaded_file: | |
| st.error("β οΈ Please upload an image first!") | |
| else: | |
| with st.spinner("π¨ Creating your neon masterpiece..."): | |
| try: | |
| # Convert uploaded image | |
| image_array = np.array(image) | |
| image_cv = cv2.cvtColor(image_array, cv2.COLOR_RGB2BGR) | |
| # Parse resolution | |
| if "1080p" in max_resolution: | |
| max_width, max_height = 1920, 1080 | |
| elif "720p" in max_resolution: | |
| max_width, max_height = 1280, 720 | |
| else: # 4K | |
| max_width, max_height = 3840, 2160 | |
| # Smart resize | |
| image_cv = resize_image_smart(image_cv, max_width, max_height) | |
| # Progress tracking | |
| progress_bar = st.progress(0) | |
| status_text = st.empty() | |
| # Calculate frames | |
| num_frames = int(duration * fps) | |
| # Step 1: Extract colors | |
| status_text.text("π¨ Step 1/6: Analyzing image colors...") | |
| progress_bar.progress(10) | |
| if auto_color: | |
| neon_colors = extract_dominant_colors(image_cv, n_colors=3) | |
| st.info(f"β¨ Auto-detected neon colors: {len(neon_colors)} vibrant tones") | |
| else: | |
| neon_colors = [(255, 150, 0)] # Default orange | |
| # Step 2: Edge detection | |
| status_text.text("β‘ Step 2/6: Detecting edges...") | |
| progress_bar.progress(25) | |
| edges = edge_detection_improved(image_cv, edge_method) | |
| # Step 3: Find drawing strokes | |
| status_text.text("βοΈ Step 3/6: Planning drawing strokes...") | |
| progress_bar.progress(40) | |
| strokes = find_contour_drawing_order(edges) | |
| if strokes: | |
| st.info(f"π Found {len(strokes)} drawing strokes for natural animation") | |
| # Step 4: Generate animation | |
| status_text.text("β¨ Step 4/6: Creating human-like drawing animation...") | |
| progress_bar.progress(55) | |
| hold_drawn_frames = int(hold_drawn * fps) | |
| hold_final_frames = int(hold_final * fps) | |
| frames = create_human_like_drawing( | |
| image_cv, edges, strokes, num_frames, | |
| colors=neon_colors, glow_size=glow_intensity, | |
| bg_color=bg_color, hold_drawn_frames=hold_drawn_frames, | |
| hold_final_frames=hold_final_frames | |
| ) | |
| if not frames: | |
| st.error("Failed to generate frames") | |
| st.stop() | |
| progress_bar.progress(70) | |
| # Step 5: Add outro | |
| status_text.text("π¬ Step 5/6: Adding outro...") | |
| sample_frame = resize_to_ratio(frames[0], aspect_ratio) | |
| height, width = sample_frame.shape[:2] | |
| outro_frame = create_outro_frame( | |
| outro_text, width, height, | |
| bg_color=bg_color, | |
| text_color=(255, 255, 255), | |
| logo_image=outro_logo | |
| ) | |
| outro_frames = [outro_frame] * (outro_duration * fps) | |
| all_frames = frames + outro_frames | |
| progress_bar.progress(80) | |
| # Step 6: Create video | |
| status_text.text("π₯ Step 6/6: Rendering video...") | |
| temp_video = tempfile.NamedTemporaryFile(delete=False, suffix='.mp4') | |
| video_path = temp_video.name | |
| temp_video.close() | |
| success = create_video(all_frames, fps, video_path, aspect_ratio) | |
| # Clear frames from memory | |
| del frames, all_frames, outro_frames | |
| gc.collect() | |
| if not success: | |
| st.error("β Failed to create video") | |
| st.stop() | |
| progress_bar.progress(90) | |
| # Add audio if provided | |
| final_video_path = video_path | |
| if audio_file: | |
| status_text.text("π΅ Adding audio...") | |
| temp_audio = tempfile.NamedTemporaryFile(delete=False, | |
| suffix=os.path.splitext(audio_file.name)[1]) | |
| temp_audio.write(audio_file.read()) | |
| temp_audio.close() | |
| final_video = tempfile.NamedTemporaryFile(delete=False, suffix='.mp4') | |
| final_video.close() | |
| if add_audio_to_video(video_path, temp_audio.name, final_video.name, | |
| start_time=audio_start_time, | |
| fadeout_duration=audio_fadeout): | |
| final_video_path = final_video.name | |
| try: | |
| os.unlink(video_path) | |
| except: | |
| pass | |
| try: | |
| os.unlink(temp_audio.name) | |
| except: | |
| pass | |
| status_text.text("β Video created successfully!") | |
| progress_bar.progress(100) | |
| # Display video | |
| st.success("π Your neon drawing video is ready!") | |
| st.video(final_video_path) | |
| # Download button | |
| with open(final_video_path, 'rb') as f: | |
| video_bytes = f.read() | |
| st.download_button( | |
| label="β¬οΈ Download Video", | |
| data=video_bytes, | |
| file_name=f"neon_drawing_{aspect_ratio.replace(':', 'x')}.mp4", | |
| mime="video/mp4", | |
| type="primary" | |
| ) | |
| # Store in session state | |
| st.session_state.video_generated = True | |
| st.session_state.video_path = final_video_path | |
| st.balloons() | |
| except MemoryError: | |
| st.error("β οΈ Memory error! Try:\n- Lower resolution\n- Shorter duration") | |
| except Exception as e: | |
| st.error(f"β Error: {str(e)}") | |
| import traceback | |
| with st.expander("Show error details"): | |
| st.code(traceback.format_exc()) | |
| # Footer | |
| st.markdown("---") | |
| st.markdown(""" | |
| <div style='text-align: center; color: #c7b8ea; padding: 20px;'> | |
| <h3 style='color: #e0e0ff; font-weight: 700;'>π¨ Live Drawing Studio - Professional Edition</h3> | |
| <p style='font-size: 1.1rem; margin-top: 10px;'>Transform images into stunning drawing animations</p> | |
| <p style='margin-top: 15px;'><b>β¨ Features:</b> Auto-color detection β’ Human-like drawing β’ Smart sizing β’ Professional output</p> | |
| </div> | |
| """, unsafe_allow_html=True) | |