""" Director's Cut - HuggingFace Space Frontend ============================================ This version uses Modal backend for all video processing. Preserves the exact UI and structure from GitHub, only replacing local processing with Modal API calls. Modal backend handles: - YouTube downloads (with Webshare residential proxies) - Video processing (Scout, Verifier, Director, Hands, Showrunner) - All heavy compute operations Frontend (this file) handles: - Gradio UI - MCP server - User interactions - Display/download of results """ # Copy exact imports from GitHub version import gradio as gr import os import tempfile import shutil import logging import json import time import re import base64 import requests from typing import List, Dict, Any, Tuple, Optional from dotenv import load_dotenv # Load environment variables load_dotenv() # Configure Logging logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s') logger = logging.getLogger(__name__) # ============================================================================== # MODAL BACKEND CONFIGURATION # ============================================================================== # Modal backend URL - deployed as "directors-cut" app # CORRECT username is tayyabkhn343 (not tayyab415) MODAL_BASE_URL = os.getenv( "MODAL_BASE_URL", "https://tayyabkhn343--directors-cut") # Available Modal endpoints (8 max for free tier): # - health (GET) - Health check # - video_info (POST) - Get video metadata using Webshare proxies # - transcript (POST) - Get transcript via Supadata # - process (POST) - Download video/audio with proxies # - outputs (GET) - List output files # - state (GET) - Get workflow state # - step (POST) - Full pipeline: steps 1-6 # - download (GET) - Download rendered video by job_id def get_modal_endpoint(name: str) -> str: """Build Modal endpoint URL. Modal converts underscores to hyphens.""" return f"{MODAL_BASE_URL}-{name.replace('_', '-')}.modal.run" # Modal API helper def call_modal(endpoint: str, method: str = "POST", data: dict = None, timeout: int = 1800) -> dict: """Call Modal backend endpoint.""" url = get_modal_endpoint(endpoint) logger.info(f"Calling Modal: {method} {url}") try: if method == "GET": response = requests.get(url, timeout=timeout) else: response = requests.post(url, json=data, timeout=timeout) response.raise_for_status() return response.json() except requests.exceptions.Timeout: logger.error(f"Modal timeout: {endpoint}") return {"error": "Request timed out"} except requests.exceptions.HTTPError as e: logger.error( f"Modal HTTP error: {e.response.status_code} - {e.response.text}") return {"error": f"{e.response.status_code} {e.response.reason} for url: {url}"} except Exception as e: logger.error(f"Modal error: {e}") return {"error": str(e)} # Output directory OUTPUT_DIR = os.getenv("OUTPUT_DIR", "/tmp/directors-cut/output") os.makedirs(OUTPUT_DIR, exist_ok=True) # ============================================================================== # HELPER FUNCTIONS (Preserved from GitHub version) # ============================================================================== def classify_video(video_info: Dict) -> str: """Classify video as 'podcast' or 'generic' - preserved from #file:app.py""" title = video_info.get('title', '').lower() uploader = video_info.get('uploader', '').lower() channel = video_info.get('channel', '').lower() duration = video_info.get('duration', 0) # Known podcast channels podcast_channels = [ 'joe rogan', 'powerfuljre', 'jre clips', 'lex fridman', 'lex clips', 'huberman lab', 'andrew huberman', 'all-in podcast', 'all-in pod', 'diary of a ceo', 'impact theory', 'tim ferriss', 'smartless', 'ted talks', 'ted', 'flagrant', 'flagrant 2' ] for pc in podcast_channels: if pc in uploader or pc in channel: logger.info(f"Classified as PODCAST via channel: {uploader}") return "podcast" # Podcast keywords podcast_keywords = ['podcast', 'interview', 'talk show', 'conversation', 'episode'] generic_keywords = ['tutorial', 'how to', 'guide', 'demo', 'review'] for keyword in generic_keywords: if keyword in title: logger.info(f"Classified as GENERIC via keyword: {keyword}") return "generic" podcast_score = sum(1 for kw in podcast_keywords if kw in title) if duration > 900 and podcast_score >= 2: logger.info("Classified as PODCAST via long duration + keywords") return "podcast" if podcast_score >= 3: logger.info("Classified as PODCAST via strong signals") return "podcast" logger.info(f"Classified as GENERIC (default)") return "generic" # ============================================================================== # GLOBAL STATE (Preserved from GitHub version) # ============================================================================== workflow_state = { 'video_url': None, 'video_info': None, 'category': None, 'temp_dir': None, 'hotspots': [], 'verified_hotspots': [], 'clips_metadata': [], 'edit_plan': [], 'final_plan': [], 'final_video_path': None, 'num_hotspots': 5, 'job_id': None } manual_state = { 'video_url': None, 'video_info': None, 'temp_dir': None, 'transcript_text': None, 'topics': [], 'selected_indices': [], 'clips_metadata': [], 'verified_clips': [], 'final_video_path': None } # ============================================================================== # STEP-BY-STEP WORKFLOW (Modal Backend Integration) # ============================================================================== def step1_analyze_video(url: str): """Step 1: Analyze video via Modal step endpoint - creates job_id and syncs state.""" try: workflow_state['video_url'] = url workflow_state['temp_dir'] = tempfile.mkdtemp() logger.info(f"Step 1: Analyzing video via Modal step endpoint: {url}") # Call Modal step endpoint (creates job_id, uses Webshare proxies) response = call_modal( "step", data={"step": 1, "url": url}, timeout=180) if response.get("error"): return f"❌ Error: {response['error']}", gr.update(interactive=False) # Modal returns job_id on success (no "success" field, just check for job_id) if not response.get("job_id"): return f"❌ Failed to analyze video: {response.get('error', 'No job_id returned')}", gr.update(interactive=False) # Store job_id for later steps workflow_state['job_id'] = response.get('job_id') # Store results workflow_state['video_info'] = { 'title': response.get('title', 'Unknown'), 'duration': response.get('duration', 0), 'uploader': response.get('channel', 'Unknown'), 'channel': response.get('channel', 'Unknown'), 'description': '', 'thumbnail': '', } # Classify video - use category from Modal or classify locally workflow_state['category'] = response.get('category') or classify_video( workflow_state['video_info']) video_info = workflow_state['video_info'] category = workflow_state['category'] duration = video_info.get('duration', 0) or 0 job_id = workflow_state.get('job_id', 'unknown') has_transcript = response.get('has_transcript', False) info_text = f""" ## ✅ Video Analyzed **Job ID:** `{job_id}` **Video Info:** - **Title:** {video_info.get('title')} - **Duration:** {duration:.0f}s ({duration/60:.1f} min) - **Channel:** {video_info.get('channel')} - **Classification:** **{category.upper()}** - **Transcript:** {'✅ Available' if has_transcript else '❌ Not available'} **Pipeline:** {'🎙️ Podcast Mode' if category == 'podcast' else '🎬 Generic Mode'} ✅ Ready for Step 2: Scout Hotspots """ return info_text, gr.update(interactive=True) except Exception as e: logger.error(f"Step 1 failed: {e}") return f"❌ Error: {e}", gr.update(interactive=False) def step2_scout_hotspots(url: str, num_hotspots: int = 5): """Step 2: Scout via Modal - preserved structure from #file:app.py""" try: if not workflow_state['video_info']: return "❌ Run Step 1 first!", gr.update(interactive=False) workflow_state['num_hotspots'] = int(num_hotspots) logger.info(f"Step 2: Scouting {num_hotspots} hotspots via Modal") response = call_modal( "step", data={"step": 2, "num_hotspots": num_hotspots}, timeout=600) if response.get("error"): return f"❌ Error: {response['error']}", gr.update(interactive=False) hotspots = response.get('hotspots', []) workflow_state['hotspots'] = hotspots result_text = f"""## 🎯 Hotspots Found **Total:** {response.get('total_found', len(hotspots))} **Top {num_hotspots}:** """ for i, h in enumerate(hotspots[:num_hotspots], 1): start_fmt = f"{int(h['start'] // 60)}:{int(h['start'] % 60):02d}" end_fmt = f"{int(h['end'] // 60)}:{int(h['end'] % 60):02d}" result_text += f"\n{i}. **{start_fmt}-{end_fmt}** | Score: {h.get('score', 0):.2f}" return result_text, gr.update(interactive=True) except Exception as e: logger.error(f"Step 2 failed: {e}") return f"❌ Error: {e}", gr.update(interactive=False) def step3_verify_hotspots(url: str): """Step 3: Verify via Modal - preserved structure from #file:app.py""" try: if not workflow_state['hotspots']: return "❌ Run Step 2 first!", gr.update(interactive=False) logger.info("Step 3: Verifying via Modal") response = call_modal("step", data={"step": 3}, timeout=900) if response.get("error"): return f"❌ Error: {response['error']}", gr.update(interactive=False) verified_clips = response.get('clips', []) workflow_state['verified_hotspots'] = [c['hotspot'] for c in verified_clips if c.get('verification', {}).get('verified')] workflow_state['clips_metadata'] = verified_clips result_text = f"""## 🔍 Verification Results **Downloaded:** {response.get('downloaded', 0)} **Verified:** {response.get('verified', 0)} """ for clip in verified_clips: v = clip.get('verification', {}) score = v.get('score', 5) passed = v.get('verified', score >= 5) status = "✅" if passed else "❌" result_text += f"\n{status} Score: {score}/10" verified_count = len(workflow_state['verified_hotspots']) if verified_count == 0: return result_text + "\n\n⚠️ No clips passed!", gr.update(interactive=False) return result_text, gr.update(interactive=True) except Exception as e: logger.error(f"Step 3 failed: {e}") return f"❌ Error: {e}", gr.update(interactive=False) def step4_create_plan(): """Step 4: Plan via Modal - preserved structure from #file:app.py""" try: if not workflow_state.get('verified_hotspots'): return "❌ Run Step 3 first!", gr.update(interactive=False), "" logger.info("Step 4: Creating plan via Modal") response = call_modal("step", data={"step": 4}, timeout=300) if response.get("error"): return f"❌ Error: {response['error']}", gr.update(interactive=False), "" final_plan = response.get('plan', []) workflow_state['final_plan'] = final_plan result_text = f"**Edit Plan ({len(final_plan)} clips):**\n\n" total_duration = 0 for i, item in enumerate(final_plan, 1): duration = item.get('end', 0) - item.get('start', 0) total_duration += duration result_text += f"{i}. {item.get('start', 0):.1f}s-{item.get('end', 0):.1f}s ({duration:.1f}s)\n" result_text += f"\n**Total: {total_duration:.1f}s**" plan_json = json.dumps(final_plan, indent=2) return result_text, gr.update(interactive=True), plan_json except Exception as e: logger.error(f"Step 4 failed: {e}") return f"❌ Error: {e}", gr.update(interactive=False), "" def step5_render_video(): """Step 5: Render via Modal - preserved structure from #file:app.py""" try: if not workflow_state.get('final_plan'): return "❌ Run Step 4 first!", None logger.info("Step 5: Rendering via Modal") response = call_modal("step", data={"step": 5}, timeout=600) if response.get("error"): return f"❌ Error: {response['error']}", None # Download video from Modal job_id = response.get('job_id') or workflow_state.get('job_id') if job_id: download_url = get_modal_endpoint( "download") + f"?job_id={job_id}&type=render" video_path = os.path.join(OUTPUT_DIR, f"render_{job_id}.mp4") try: resp = requests.get(download_url, stream=True, timeout=300) resp.raise_for_status() with open(video_path, 'wb') as f: for chunk in resp.iter_content(chunk_size=8192): f.write(chunk) workflow_state['final_video_path'] = video_path return f"✅ Success! Video: `{video_path}`", video_path except Exception as e: logger.error(f"Download failed: {e}") return f"❌ Download failed: {e}", None else: return "❌ No job_id returned", None except Exception as e: logger.error(f"Step 5 failed: {e}") return f"❌ Error: {e}", None def reset_workflow(): """Reset workflow - clears both local and Modal backend state.""" # Clear Modal backend state first try: logger.info("Resetting Modal backend state...") response = call_modal("reset", method="POST", data={}, timeout=30) if response.get("success"): logger.info("Modal backend reset successful") else: logger.warning( f"Modal reset warning: {response.get('error', 'Unknown')}") except Exception as e: logger.warning(f"Modal reset failed (continuing): {e}") # Clear local temp directory if workflow_state.get('temp_dir') and os.path.exists(workflow_state['temp_dir']): try: shutil.rmtree(workflow_state['temp_dir']) except: pass # Clear local state for key in workflow_state: if key == 'temp_dir': workflow_state[key] = None elif isinstance(workflow_state[key], list): workflow_state[key] = [] else: workflow_state[key] = None return ( "", "", "", "", "", "", None, gr.update(interactive=False), gr.update(interactive=False), gr.update(interactive=False), gr.update(interactive=False), ) # ============================================================================== # PRODUCTION STUDIO (Modal Backend Integration) # ============================================================================== def add_production_wrapper(video_file, mood_override, enable_smart_crop, add_intro_image, add_subtitles, progress=gr.Progress()): """Production via Modal - uploads video and processes with fresh job_id""" # Debug: Log what we received from Gradio logger.info(f"video_file type: {type(video_file)}") logger.info(f"video_file value: {video_file}") actual_video_path = None # Handle different Gradio 6 input formats if video_file is None: if workflow_state.get('final_video_path'): actual_video_path = workflow_state['final_video_path'] yield "🔄 Using last render", None else: yield "❌ No video uploaded. Please upload a video file.", None return elif isinstance(video_file, str): # Direct string path actual_video_path = video_file elif isinstance(video_file, dict): # Gradio 6 may return dict with 'path' or 'name' key actual_video_path = video_file.get('path') or video_file.get( 'name') or video_file.get('video') logger.info(f"Extracted path from dict: {actual_video_path}") elif hasattr(video_file, 'name'): # File-like object actual_video_path = video_file.name else: yield f"❌ Unexpected video input type: {type(video_file)}", None return if not actual_video_path: yield "❌ Could not determine video file path", None return # Check if file exists if not os.path.exists(actual_video_path): logger.error(f"File not found: {actual_video_path}") # Try to list the directory to debug parent_dir = os.path.dirname(actual_video_path) if os.path.exists(parent_dir): contents = os.listdir(parent_dir) logger.info(f"Directory {parent_dir} contents: {contents[:10]}") yield f"❌ File not found: {actual_video_path}\n\nThe uploaded file may have been cleaned up. Please try uploading again.", None return try: progress(0.1, desc="Preparing video...") yield "📤 Preparing video for processing...", None # Copy file to our temp directory to prevent Gradio cleanup issues import shutil import uuid temp_dir = os.path.join(os.getcwd(), "temp") os.makedirs(temp_dir, exist_ok=True) # Generate a unique filename temp_filename = f"upload_{uuid.uuid4().hex[:8]}_{os.path.basename(actual_video_path)}" local_video_path = os.path.join(temp_dir, temp_filename) logger.info(f"Copying uploaded file to: {local_video_path}") shutil.copy2(actual_video_path, local_video_path) # Get the file size for logging file_size = os.path.getsize(local_video_path) / 1024 / 1024 logger.info( f"Processing video: {local_video_path} ({file_size:.1f} MB)") # Read video as base64 for transfer to Modal progress(0.2, desc="Reading video file...") yield "📦 Reading video file...", None with open(local_video_path, 'rb') as f: video_bytes = f.read() video_base64 = base64.b64encode(video_bytes).decode('utf-8') logger.info( f"Video encoded: {len(video_base64) / 1024 / 1024:.1f} MB base64") progress(0.3, desc="Processing on Modal...") yield "🎬 Processing on Modal (this may take a few minutes)...", None # Send video data directly to Modal for processing response = call_modal("step", data={ "step": 6, "video_base64": video_base64, "video_filename": os.path.basename(local_video_path), "enable_smart_crop": enable_smart_crop, "add_intro": add_intro_image, "add_subtitles": add_subtitles, "mood": mood_override }, timeout=1800) # 30 min timeout for large videos if response.get("error"): yield f"❌ Error: {response['error']}", None return progress(0.8, desc="Downloading result...") yield "📥 Downloading polished video...", None # Get job_id from response job_id = response.get('job_id') if not job_id: yield "❌ Error: No job_id in response", None return logger.info(f"Downloading production video for job_id: {job_id}") download_url = get_modal_endpoint( "download") + f"?job_id={job_id}&type=production" output_path = os.path.join(OUTPUT_DIR, f"production_{job_id}.mp4") logger.info(f"Download URL: {download_url}") resp = requests.get(download_url, stream=True, timeout=300) # Check if response is an error JSON content_type = resp.headers.get('content-type', '') if 'application/json' in content_type: error_data = resp.json() yield f"❌ Download error: {error_data.get('error', 'Unknown error')}", None return resp.raise_for_status() with open(output_path, 'wb') as f: for chunk in resp.iter_content(chunk_size=8192): f.write(chunk) # Verify file was downloaded if os.path.exists(output_path) and os.path.getsize(output_path) > 1000: workflow_state['final_video_path'] = output_path progress(1.0, desc="Complete!") yield f"✅ Complete! Saved to: {output_path}", output_path else: yield f"❌ Download failed: File empty or not found", None except Exception as e: logger.error(f"Production error: {e}") yield f"❌ Error: {e}", None def load_last_render_into_production(): """Load last render - preserved from #file:app.py""" path = workflow_state.get('final_video_path') if not path: return gr.update(value=None), "❌ No video available" return gr.update(value=path), f"✅ Loaded: {os.path.basename(path)}" # ============================================================================== # MCP TOOLS (Preserved from GitHub version) # ============================================================================== @gr.mcp.tool() def process_video(url: str) -> str: """Process video via Modal - preserved from #file:app.py""" try: logger.info(f"Processing via Modal: {url}") response = call_modal("process", data={ "url": url, "num_hotspots": 5, "enable_smart_crop": True, "add_intro": True, "add_subtitles": True, "mood": "auto" }, timeout=1800) if response.get("error"): return f"Error: {response['error']}" if response.get("success"): job_id = response.get('job_id') stats = response.get('stats', {}) return f"Success!\n\nJob ID: {job_id}\nCategory: {stats.get('video_category')}\nMood: {stats.get('mood')}" else: return f"Failed: {response.get('error', 'Unknown error')}" except Exception as e: return f"Error: {str(e)}" @gr.mcp.tool() def step1_analyze_video_mcp(youtube_url: str) -> str: """Step 1 MCP tool - preserved from #file:app.py""" try: response = call_modal("step", data={"step": 1, "url": youtube_url}) if response.get("error"): return f"Error: {response['error']}" workflow_state['job_id'] = response.get('job_id') workflow_state['video_info'] = { 'title': response.get('title'), 'duration': response.get('duration'), } workflow_state['category'] = response.get('category') return f"Step 1 Complete!\n\nVideo: {response.get('title')}\nCategory: {response.get('category')}\nJob ID: {response.get('job_id')}" except Exception as e: return f"Error: {str(e)}" @gr.mcp.tool() def get_workflow_state_mcp() -> str: """Get workflow state - preserved from #file:app.py""" try: response = call_modal("state", method="GET") return json.dumps(response, indent=2) except Exception as e: return f"Error: {str(e)}" # ============================================================================== # CHATGPT APPS SDK - MCP TOOLS & WIDGETS # ============================================================================== @gr.mcp.tool( _meta={ "openai/outputTemplate": "ui://widget/production.html", "openai/resultCanProduceWidget": True, "openai/widgetAccessible": True, } ) def add_production_to_video( video_url: str, mood: str = "auto", enable_smart_crop: bool = True, add_intro: bool = True, add_subtitles: bool = True ) -> str: """ 🎬 MAIN VIDEO PROCESSING TOOL - Transform any video into viral-ready content! ⚠️ IMPORTANT: This tool requires a WEB URL (http:// or https://), NOT a local file path! - ✅ YouTube URLs work: https://youtube.com/watch?v=... - ✅ Direct video URLs work: https://example.com/video.mp4 - ❌ Local paths do NOT work: /mnt/data/file.mp4 If the user uploads a file, tell them to: 1. Upload the video to YouTube (unlisted) and provide the URL, OR 2. Use a cloud storage link (Google Drive public link, Dropbox, etc.) This is the PRIMARY tool for video editing. Use this tool when the user wants to: - Process a YouTube video - Add professional production value to a video - Create short-form vertical content for TikTok/Reels/Shorts - Add intros, subtitles, smart crop, or background music The tool automatically: 1. Downloads the video from the URL 2. Applies AI-powered 9:16 smart crop for mobile viewing 3. Generates a custom AI intro with voiceover and title card 4. Adds auto-generated subtitles using Whisper 5. Adds mood-matched background music 6. Returns a download link for the finished video Parameters: video_url: The WEB URL of the video (must start with http:// or https://) - YouTube URL: https://youtube.com/watch?v=VIDEO_ID - Direct video URL: https://example.com/video.mp4 mood: Video mood/style - 'hype' (energetic), 'chill' (relaxed), 'suspense' (dramatic), or 'auto' (AI detects) enable_smart_crop: If True, crops video to 9:16 vertical format for mobile add_intro: If True, generates AI intro with voiceover and title card add_subtitles: If True, adds auto-generated subtitles Returns: Processing result with a download URL for the produced video """ try: # Validate URL - must be a web URL, not a local path if not video_url: return "❌ Error: No video URL provided" # Check for local file paths (reject these) if video_url.startswith('/mnt/data/') or video_url.startswith('/tmp/') or video_url.startswith('C:\\'): return f"❌ Error: Local file path detected.\n\n⚠️ This tool requires a web URL, not a local file.\n\n**Options:**\n1. Use the Video Studio widget to upload files directly\n2. Upload to YouTube (unlisted) and share the URL\n3. Use a cloud storage link (Google Drive, Dropbox)" if not video_url.startswith(('http://', 'https://')): return f"❌ Error: Invalid URL '{video_url[:50]}...'\n\n⚠️ This tool requires a web URL (http:// or https://), not a local file path.\n\n**Please provide:**\n- A YouTube URL: https://youtube.com/watch?v=...\n- Or a direct video URL: https://example.com/video.mp4\n\nIf you uploaded a file, please upload it to YouTube (unlisted) first and share that URL." logger.info( f"🎬 Production pipeline starting for: {video_url[:100]}...") # Call Modal Step 6 with video_url for standalone mode response = call_modal("step", data={ "step": 6, "video_url": video_url, "mood": mood, "enable_smart_crop": enable_smart_crop, "add_intro": add_intro, "add_subtitles": add_subtitles, }, timeout=2400) # 40 min timeout for large videos if response.get("error"): return f"❌ Error: {response['error']}" if response.get("success"): job_id = response.get('job_id') duration = response.get('duration', 0) detected_mood = response.get('mood', mood) # Build download URL download_url = f"https://tayyabkhn343--directors-cut-download.modal.run?job_id={job_id}" result = f"""✅ Video produced successfully! 📊 **Details:** - Job ID: {job_id} - Duration: {duration:.1f}s - Mood: {detected_mood} - Smart Crop: {'✓' if enable_smart_crop else '✗'} - AI Intro: {'✓' if response.get('has_intro') else '✗'} - Subtitles: {'✓' if response.get('has_subtitles') else '✗'} - Background Music: {'✓' if response.get('has_music') else '✗'} 🔗 **Download:** {download_url}""" return result else: return f"❌ Processing failed: {response.get('error', 'Unknown error')}" except Exception as e: logger.error(f"Production pipeline error: {e}") return f"❌ Error: {str(e)}" @gr.mcp.resource("ui://widget/production.html", mime_type="text/html+skybridge") def production_widget_html(): """ChatGPT widget for displaying video production results with download button.""" return """
""" @gr.mcp.resource("ui://widget/video-studio.html", mime_type="text/html+skybridge") def video_studio_widget_html(): """Interactive Video Studio widget for ChatGPT - upload and process videos directly.""" return """ """ @gr.mcp.tool( _meta={ "openai/outputTemplate": "ui://widget/video-studio.html", "openai/resultCanProduceWidget": True, "openai/widgetAccessible": True, } ) def open_video_studio() -> str: """ 🎬 Open the Director's Cut Video Studio interface. Use this tool when the user wants to: - Process or edit a video - Upload a video file - Add production value to any video - Create viral short-form content This opens an interactive studio where users can: - Paste a YouTube URL OR upload a video file directly - Choose mood (hype, chill, suspense, or auto-detect) - Enable/disable smart crop, AI intro, and subtitles - Process the video and download the result Returns: str: Confirmation that the studio is ready """ return "🎬 Director's Cut Video Studio is ready! You can paste a YouTube URL or upload a video file, then click 'Process Video' to transform it into viral content." # ============================================================================== # GRADIO INTERFACE (Preserved exact structure from GitHub) # ============================================================================== with gr.Blocks(title="Director's Cut") as app: gr.Markdown("# 🎬 Director's Cut - Autonomous Video Editor") gr.Markdown("**Powered by Modal Backend** with Webshare proxies") with gr.Tabs(): # ==================== README TAB ==================== with gr.Tab("📖 About"): # Read and display README content readme_path = os.path.join(os.path.dirname(__file__), "README.md") if os.path.exists(readme_path): with open(readme_path, "r") as f: readme_content = f.read() # Remove YAML frontmatter if readme_content.startswith("---"): end_idx = readme_content.find("---", 3) if end_idx != -1: readme_content = readme_content[end_idx + 3:].strip() # Convert relative image paths to absolute HuggingFace URLs readme_content = readme_content.replace( "./resources/", "https://huggingface.co/spaces/tyb343/directors-cut/resolve/main/resources/" ) gr.Markdown(readme_content) else: gr.Markdown("README not found") # ==================== AUTO MODE TAB ==================== with gr.Tab("📹 Create Clip"): gr.Markdown("**Step-by-Step Editor** - Processing on Modal") with gr.Row(): url_input = gr.Textbox( label="YouTube URL", placeholder="https://youtube.com/watch?v=...", scale=4) reset_btn = gr.Button("🔄 Reset", scale=1, variant="secondary") # Step 1 with gr.Group(): gr.Markdown("### Step 1: Analyze Video") gr.Markdown( "*Downloads video & extracts transcript (~3-4 mins)*", elem_classes=["step-hint"]) step1_btn = gr.Button( "1️⃣ Analyze & Classify", variant="primary") step1_output = gr.Markdown() # Step 2 with gr.Group(): gr.Markdown("### Step 2: Scout Hotspots") num_hotspots_slider = gr.Slider( minimum=3, maximum=10, value=5, step=1, label="Number of Hotspots") step2_btn = gr.Button("2️⃣ Scout", interactive=False) step2_output = gr.Markdown() # Step 3 with gr.Group(): gr.Markdown("### Step 3: Verify") step3_btn = gr.Button("3️⃣ Verify Clips", interactive=False) step3_output = gr.Markdown() # Step 4 with gr.Group(): gr.Markdown("### Step 4: Create Plan") step4_btn = gr.Button("4️⃣ Generate Plan", interactive=False) step4_output = gr.Markdown() plan_json = gr.Code(label="Edit Plan (JSON)", language="json") # Step 5 with gr.Group(): gr.Markdown("### Step 5: Render") step5_btn = gr.Button("5️⃣ Render Video", interactive=False, variant="primary") step5_output = gr.Markdown() video_output = gr.Video(label="Final Edit") # Event handlers step1_btn.click(fn=step1_analyze_video, inputs=[ url_input], outputs=[step1_output, step2_btn]) step2_btn.click(fn=step2_scout_hotspots, inputs=[ url_input, num_hotspots_slider], outputs=[step2_output, step3_btn]) step3_btn.click(fn=step3_verify_hotspots, inputs=[ url_input], outputs=[step3_output, step4_btn]) step4_btn.click(fn=step4_create_plan, inputs=[], outputs=[ step4_output, step5_btn, plan_json]) step5_btn.click(fn=step5_render_video, inputs=[], outputs=[step5_output, video_output]) reset_btn.click(fn=reset_workflow, inputs=[], outputs=[step1_output, step2_output, step3_output, step4_output, plan_json, step5_output, video_output, step2_btn, step3_btn, step4_btn, step5_btn]) # ==================== PRODUCTION STUDIO TAB ==================== with gr.Tab("🎙️ Production Studio"): gr.Markdown( "## Professional Video Production\n**Processing on Modal backend**") video_input_2 = gr.Video(label="Upload Video") with gr.Row(): load_render_btn = gr.Button( "⬇️ Load Last Render", variant="secondary") load_render_status = gr.Markdown() with gr.Row(): mood_override = gr.Dropdown( choices=["auto", "hype", "suspense", "chill"], value="auto", label="Mood") with gr.Row(): enable_smart_crop = gr.Checkbox( label="🎯 Smart Crop", value=True) add_intro_image = gr.Checkbox( label="🖼️ Intro Image", value=True) add_subtitles = gr.Checkbox(label="📝 Subtitles", value=True) produce_btn = gr.Button( "✨ Add Production Value", variant="primary", size="lg") progress_2 = gr.Textbox( label="Progress", lines=8, interactive=False) video_output_2 = gr.Video(label="Polished Video", height=500) produce_btn.click(fn=add_production_wrapper, inputs=[ video_input_2, mood_override, enable_smart_crop, add_intro_image, add_subtitles], outputs=[progress_2, video_output_2]) load_render_btn.click(fn=load_last_render_into_production, inputs=[ ], outputs=[video_input_2, load_render_status]) # ==================== CHATGPT MCP TAB (for MCP tool/resource binding) ==================== with gr.Tab("🤖 ChatGPT Integration", visible=False): # This tab binds MCP tools and resources to Gradio events # Required by Gradio MCP to expose them to ChatGPT # Bind the add_production_to_video tool chatgpt_url_input = gr.Textbox(label="Video URL") chatgpt_mood = gr.Dropdown( choices=["auto", "hype", "suspense", "chill"], value="auto") chatgpt_crop = gr.Checkbox(value=True) chatgpt_intro = gr.Checkbox(value=True) chatgpt_subs = gr.Checkbox(value=True) chatgpt_output = gr.Textbox(label="Result") chatgpt_btn = gr.Button("Process Video") chatgpt_btn.click( add_production_to_video, inputs=[chatgpt_url_input, chatgpt_mood, chatgpt_crop, chatgpt_intro, chatgpt_subs], outputs=chatgpt_output ) # Bind the production widget resource widget_code = gr.Code(label="Widget HTML", language="html", max_lines=5) widget_btn = gr.Button("Show Widget") widget_btn.click(production_widget_html, outputs=widget_code) # Bind the Video Studio tool studio_output = gr.Textbox(label="Studio Status") studio_btn = gr.Button("Open Video Studio") studio_btn.click(open_video_studio, outputs=studio_output) # Bind the Video Studio widget resource studio_widget_code = gr.Code( label="Video Studio Widget", language="html", max_lines=5) studio_widget_btn = gr.Button("Get Video Studio Widget") studio_widget_btn.click( video_studio_widget_html, outputs=studio_widget_code) if __name__ == "__main__": print("🚀 Starting Director's Cut HuggingFace Space...") print(f"📡 Modal Backend: {MODAL_BASE_URL}") app.launch( server_name="0.0.0.0", server_port=7860, mcp_server=True, share=False )