import os import re import subprocess import gradio as gr CURRENT_DIR = os.path.dirname(os.path.abspath(__file__)) WORKING_DIR = os.path.dirname(CURRENT_DIR) # ViralCutter root import sys sys.path.append(WORKING_DIR) from i18n.i18n import I18nAuto i18n = I18nAuto() # Subtitle Presets SUBTITLE_PRESETS = { "MrBeast Clean Hook": { "font_name": "Montserrat-ExtraBold", "font_size": 32, "base_color": "#FFFFFF", "highlight_color": "#FFD700", "outline_color": "#000000", "outline_thickness": 3, "shadow_color": "#000000", "shadow_size": 2, "bold": True, "italic": False, "uppercase": True, "highlight_size": 38, "words_per_block": 3, "gap_limit": 0.25, "mode": "highlight", "underline": False, "strikeout": False, "border_style": 1, "vertical_position": 180, "alignment": 2, "remove_punctuation": True }, "Hormozi (Classic)": { "font_name": "Montserrat-ExtraBold", "font_size": 30, "base_color": "#FFFFFF", "highlight_color": "#00FF00", "outline_color": "#000000", "outline_thickness": 3, "shadow_color": "#000000", "shadow_size": 0, "bold": True, "italic": False, "uppercase": True, "highlight_size": 35, "words_per_block": 2, "gap_limit": 0.5, "mode": "highlight", "underline": False, "strikeout": False, "border_style": 1, "vertical_position": 200, "alignment": 2, "remove_punctuation": True }, "Beasty (Loud)": { "font_name": "Arial", "font_size": 34, "base_color": "#FFFFFF", "highlight_color": "#FF0000", "outline_color": "#000000", "outline_thickness": 3, "shadow_color": "#000000", "shadow_size": 3, "bold": True, "italic": False, "uppercase": True, "highlight_size": 40, "words_per_block": 3, "gap_limit": 0.4, "mode": "highlight", "underline": False, "strikeout": False, "border_style": 1, "vertical_position": 190, "alignment": 2, "remove_punctuation": True }, "Word Killer (TikTok)": { "font_name": "Impact", "font_size": 38, "base_color": "#FF0000", "highlight_color": "#FF0000", "outline_color": "#000000", "outline_thickness": 3, "shadow_color": "#000000", "shadow_size": 3, "bold": True, "italic": False, "uppercase": True, "highlight_size": 45, "words_per_block": 1, "gap_limit": 0.2, "mode": "word_by_word", "underline": False, "strikeout": False, "border_style": 1, "vertical_position": 210, "alignment": 2, "remove_punctuation": True }, "Rapid Fire (Sprint)": { "font_name": "Impact", "font_size": 36, "base_color": "#FFFF00", "highlight_color": "#FFFF00", "outline_color": "#000000", "outline_thickness": 2, "shadow_color": "#000000", "shadow_size": 2, "bold": True, "italic": True, "uppercase": True, "highlight_size": 42, "words_per_block": 1, "gap_limit": 0.3, "mode": "word_by_word", "underline": False, "strikeout": False, "border_style": 1, "vertical_position": 210, "alignment": 2, "remove_punctuation": True }, "Educational Fast": { "font_name": "Roboto-Bold", "font_size": 28, "base_color": "#FFFFFF", "highlight_color": "#00BFFF", "outline_color": "#000000", "outline_thickness": 2, "shadow_color": "#000000", "shadow_size": 1, "bold": True, "italic": False, "uppercase": False, "highlight_size": 34, "words_per_block": 3, "gap_limit": 0.45, "mode": "highlight", "underline": False, "strikeout": False, "border_style": 1, "vertical_position": 220, "alignment": 2, "remove_punctuation": False }, "Podcast Viral (Centered)": { "font_name": "Arial", "font_size": 26, "base_color": "#FFFFFF", "highlight_color": "#00FFAA", "outline_color": "#000000", "outline_thickness": 2, "shadow_color": "#000000", "shadow_size": 1, "bold": True, "italic": False, "uppercase": False, "highlight_size": 30, "words_per_block": 4, "gap_limit": 0.55, "mode": "highlight", "underline": False, "strikeout": False, "border_style": 1, "vertical_position": 240, "alignment": 2, "remove_punctuation": True }, "Drama Emocional": { "font_name": "Arial", "font_size": 28, "base_color": "#EAEAEA", "highlight_color": "#FF5555", "outline_color": "#000000", "outline_thickness": 2, "shadow_color": "#000000", "shadow_size": 2, "bold": True, "italic": False, "uppercase": False, "highlight_size": 34, "words_per_block": 2, "gap_limit": 0.6, "mode": "highlight", "underline": False, "strikeout": False, "border_style": 1, "vertical_position": 235, "alignment": 2, "remove_punctuation": True }, "Story Subtitle (Netflix Style)": { "font_name": "Arial", "font_size": 24, "base_color": "#FFFFFF", "highlight_color": "#FFFFFF", "outline_color": "#000000", "outline_thickness": 0, "shadow_color": "#000000", "shadow_size": 1, "bold": True, "italic": False, "uppercase": False, "highlight_size": 24, "words_per_block": 7, "gap_limit": 0.7, "mode": "no_highlight", "underline": False, "strikeout": False, "border_style": 3, "vertical_position": 250, "alignment": 2, "remove_punctuation": False }, "Neon Cyber": { "font_name": "Arial", "font_size": 30, "base_color": "#FF00FF", "highlight_color": "#00FFFF", "outline_color": "#FFFFFF", "outline_thickness": 1, "shadow_color": "#000000", "shadow_size": 3, "bold": True, "italic": False, "uppercase": True, "highlight_size": 36, "words_per_block": 2, "gap_limit": 0.5, "mode": "highlight", "underline": True, "strikeout": False, "border_style": 1, "vertical_position": 205, "alignment": 2, "remove_punctuation": True }, "Retro Pixel": { "font_name": "Consolas", "font_size": 26, "base_color": "#00FF00", "highlight_color": "#00FF00", "outline_color": "#000000", "outline_thickness": 2, "shadow_color": "#000000", "shadow_size": 0, "bold": False, "italic": False, "uppercase": True, "highlight_size": 26, "words_per_block": 1, "gap_limit": 0.5, "mode": "word_by_word", "underline": False, "strikeout": False, "border_style": 3, "vertical_position": 215, "alignment": 2 } } def generate_preview_html(font, size, color, highlight, outline, outline_thick, shadow, shadow_sz, bold, italic, upper, h_size, w_block, gap, mode, under, strike, border_s, vert_pos, align, remove_punc): # Debug inputs #print(f"DEBUG_HTML: Inputs - Color: {color}, Highlight: {highlight}, Outline: {outline}") def sanitize_color(c): if not c: return "#FFFFFF" clean = c.lstrip('#').strip() # Handle RGB/RGBA if clean.lower().startswith("rgb"): try: nums = re.findall(r"[\d\.]+", clean) if len(nums) >= 3: r = int(float(nums[0])) g = int(float(nums[1])) b = int(float(nums[2])) r = max(0, min(255, r)) g = max(0, min(255, g)) b = max(0, min(255, b)) ret = f"#{r:02X}{g:02X}{b:02X}" # print(f"DEBUG_HTML: Sanitized {c} -> {ret}") return ret except Exception as e: print(f"DEBUG_HTML: Sanitize Error: {e}") pass # Ensure # prefix for standard hex if missing if not c.startswith("#") and not c.startswith("rgb"): return f"#{c}" return c color = sanitize_color(color) highlight = sanitize_color(highlight) outline = sanitize_color(outline) shadow = sanitize_color(shadow) #print(f"DEBUG_HTML: Final Colors - Color: {color}, Highlight: {highlight}") weight = "bold" if bold else "normal" style = "italic" if italic else "normal" transform = "uppercase" if upper else "none" decorations = [] if under: decorations.append("underline") if strike: decorations.append("line-through") decoration = " ".join(decorations) if decorations else "none" # Force larger preview size regardless of input size # We maintain ratio between highlight and base base_preview_px = 40 ratio = 1.0 if size > 0: ratio = h_size / size highlight_preview_px = base_preview_px * ratio # Avoid extreme ratios in preview if highlight_preview_px > base_preview_px * 2: highlight_preview_px = base_preview_px * 2 # Border Style 3 is Opaque Box usually in ASS, here we can simulate background bg_style = "background-color: rgba(0,0,0,0.6); padding: 5px 10px; border-radius: 4px;" if border_s == 3 else "" # Handle Content based on Mode # Handle Content based on Mode content_html = "" preview_word = i18n("PREVIEW") if mode == "word_by_word": # Only show the active word content_html = f'{preview_word}' elif mode == "no_highlight": # No highlight difference span_html = f'{preview_word}' content_html = i18n("This is a {} of your subtitles").format(span_html) else: # Default Highlight mode span_html = f'{preview_word}' content_html = i18n("This is a {} of your subtitles").format(span_html) html = f"""
{content_html}
""" return html def apply_preset(preset): if preset in SUBTITLE_PRESETS: p = SUBTITLE_PRESETS[preset] return ( p["font_name"], p["font_size"], p["base_color"], p["highlight_color"], p["outline_color"], p["outline_thickness"], p["shadow_color"], p["shadow_size"], p["bold"], p["italic"], p["uppercase"], p["highlight_size"], p["words_per_block"], p["gap_limit"], p["mode"], p["underline"], p["strikeout"], p["border_style"], p.get("vertical_position", 210), p.get("alignment", 2), p.get("remove_punctuation", True) ) return (gr.skip(),) * 21 import scripts.adjust_subtitles as adjust def render_preview_video(font, size, color, highlight, outline, outline_thick, shadow, shadow_sz, bold, italic, upper, h_size, w_block, gap, mode, under, strike, border_s, vert_pos, align, remove_punc): # Helper to convert HEX to ASS color &HBBGGRR& def hex_to_ass(h): try: with open("debug_preview.log", "a") as f: f.write(f"PREVIEW INPUT: '{h}'\n") except: pass if not h: return "&H00FFFFFF&" hex_clean = h.lstrip('#').strip() # Handle rgb/rgba if hex_clean.lower().startswith("rgb"): try: # regex to capture numbers including floats nums = re.findall(r"[\d\.]+", hex_clean) if len(nums) >= 3: r = int(float(nums[0])) g = int(float(nums[1])) b = int(float(nums[2])) # Clamp just in case r = max(0, min(255, r)) g = max(0, min(255, g)) b = max(0, min(255, b)) return f"&H00{b:02X}{g:02X}{r:02X}&".upper() except: pass if len(hex_clean) == 3: hex_clean = "".join([c*2 for c in hex_clean]) if len(hex_clean) == 6: return f"&H00{hex_clean[4:6]}{hex_clean[2:4]}{hex_clean[0:2]}&".upper() return "&H00FFFFFF&" base_c = hex_to_ass(color) high_c = hex_to_ass(highlight) out_c = hex_to_ass(outline) shad_c = hex_to_ass(shadow) # Paths preview_dir = os.path.join(CURRENT_DIR, "PREVIEW") os.makedirs(preview_dir, exist_ok=True) json_template = os.path.join(CURRENT_DIR, "preview.json") if not os.path.exists(json_template): print(f"Error: {json_template} not found.") return None ass_path = os.path.join(preview_dir, "preview.ass") out_vid_path = os.path.join(preview_dir, "preview_render.mp4") # Prepare ASS Bool Values (-1=True, 0=False) bold_val = "-1" if bold else "0" italic_val = "-1" if italic else "0" under_val = "-1" if under else "0" strike_val = "-1" if strike else "0" try: # Generate ASS from JSON using the shared script logic # this ensures consistency with the actual video generation adjust.generate_ass_from_file( input_path=json_template, output_path=ass_path, project_folder=preview_dir, # Dummy folder base_color=base_c, base_size=size, highlight_size=h_size, highlight_color=high_c, words_per_block=int(w_block), gap_limit=gap, mode=mode, vertical_position=vert_pos, alignment=align, font=font, outline_color=out_c, shadow_color=shad_c, bold=bold_val, italic=italic_val, underline=under_val, strikeout=strike_val, border_style=border_s, outline_thickness=outline_thick, shadow_size=shadow_sz, uppercase=upper, face_modes={}, remove_punctuation=remove_punc ) # Prepare safe path for ffmpeg filter: escape windows backslashes and colon safe_ass_path = ass_path.replace('\\', '/').replace(':', '\\:') # Render with ffmpeg # Background color #333333 to match UI roughly. # Resolution 480x854 (9:16) cmd = [ "ffmpeg", "-y", "-f", "lavfi", "-i", "color=c=0x333333:s=480x854:d=2.4", "-vf", f"ass='{safe_ass_path}'", "-c:v", "libx264", "-preset", "ultrafast", "-pix_fmt", "yuv420p", "-an", out_vid_path ] subprocess.run(cmd, cwd=WORKING_DIR, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL) if os.path.exists(out_vid_path): import shutil # Create a timestamped copy to force browser cache refresh import time timestamp = int(time.time()) cache_bust_path = os.path.join(preview_dir, f"preview_render_{timestamp}.mp4") shutil.copy(out_vid_path, cache_bust_path) # Clean old files try: for f in os.listdir(preview_dir): if f.startswith("preview_render_") and f.endswith(".mp4") and f != os.path.basename(cache_bust_path): try: os.remove(os.path.join(preview_dir, f)) except: pass except: pass return gr.update(value=cache_bust_path, autoplay=True) except Exception as e: print(f"Preview Gen Error: {e}") import traceback traceback.print_exc() return None