Spaces:
Sleeping
Sleeping
| import streamlit as st | |
| import base64 | |
| import tempfile | |
| import os | |
| import math | |
| # --- FORCED FONT DOWNLOADER --- | |
| font_path = "/tmp/Hind-Bold.ttf" | |
| if not os.path.exists(font_path) or os.path.getsize(font_path) < 10000: | |
| os.system("wget -qO /tmp/Hind-Bold.ttf https://github.com/google/fonts/raw/main/ofl/hind/Hind-Bold.ttf") | |
| try: | |
| from PIL import Image, ImageDraw, ImageFont | |
| import numpy as np | |
| if not hasattr(Image, 'ANTIALIAS'): | |
| Image.ANTIALIAS = Image.Resampling.LANCZOS | |
| except ImportError: | |
| pass | |
| def hex_to_rgb(hex_color): | |
| hex_color = hex_color.lstrip('#') | |
| return tuple(int(hex_color[i:i+2], 16) for i in (0, 2, 4)) | |
| # --- MASTER TEXT GENERATOR (Standard) --- | |
| def get_text_clip(text, fontsize, color_hex, duration, anim_type=""): | |
| import moviepy.editor as mpy | |
| if not text.strip(): | |
| text = " " | |
| if anim_type in ["Scroll Left", "Scroll Right"]: | |
| text = text.replace('\n', ' ') | |
| text = (text.rstrip() + " ") * 5 | |
| elif anim_type in ["Scroll Up", "Scroll Down"]: | |
| text = (text.strip() + "\n\n\n\n\n") * 15 | |
| else: | |
| text = text.strip() | |
| actual_font_size = int(fontsize) | |
| font = None | |
| try: font = ImageFont.truetype(font_path, actual_font_size) | |
| except: font = ImageFont.load_default() | |
| dummy_img = Image.new('RGBA', (1, 1), (0, 0, 0, 0)) | |
| draw = ImageDraw.Draw(dummy_img) | |
| lines = text.split('\n') | |
| max_w, total_h = 0, 0 | |
| line_heights, line_bboxes = [], [] | |
| for line in lines: | |
| bbox = draw.textbbox((0, 0), line, font=font) | |
| line_bboxes.append(bbox) | |
| lw, lh = bbox[2] - bbox[0], bbox[3] - bbox[1] | |
| if lw > max_w: max_w = lw | |
| total_h += lh | |
| line_heights.append(lh) | |
| total_h += max(0, len(lines) - 1) * 15 | |
| img_w = max(1, int(max_w) + 40) | |
| img_h = max(1, int(total_h) + 40) | |
| img = Image.new('RGBA', (img_w, img_h), (0, 0, 0, 0)) | |
| draw = ImageDraw.Draw(img) | |
| txt_rgb = hex_to_rgb(color_hex) | |
| y_offset = 20 | |
| try: | |
| from pilmoji import Pilmoji | |
| with Pilmoji(img) as pilmoji: | |
| for i, line in enumerate(lines): | |
| bbox = line_bboxes[i] | |
| x_pos = (img_w - (bbox[2] - bbox[0])) / 2 | |
| pilmoji.text((x_pos, y_offset - bbox[1]), line, font=font, fill=txt_rgb + (255,)) | |
| y_offset += line_heights[i] + 15 | |
| except ImportError: | |
| for i, line in enumerate(lines): | |
| bbox = line_bboxes[i] | |
| x_pos = (img_w - (bbox[2] - bbox[0])) / 2 | |
| draw.text((x_pos, y_offset - bbox[1]), line, font=font, fill=txt_rgb + (255,)) | |
| y_offset += line_heights[i] + 15 | |
| arr = np.array(img) | |
| clip = mpy.ImageClip(arr[:, :, :3]).set_duration(duration) | |
| mask = mpy.ImageClip(arr[:, :, 3] / 255.0, ismask=True).set_duration(duration) | |
| return clip.set_mask(mask) | |
| # --- SINGLE CHAR GENERATOR (Fixes Beka-Dheka Text / Perfect Baseline) --- | |
| def get_char_clip(char, fontsize, color_hex, duration): | |
| import moviepy.editor as mpy | |
| font = ImageFont.truetype(font_path, int(fontsize)) | |
| dummy = Image.new('RGBA', (1, 1), (0, 0, 0, 0)) | |
| draw = ImageDraw.Draw(dummy) | |
| ref_bbox = draw.textbbox((0, 0), "Ay", font=font) | |
| char_bbox = draw.textbbox((0, 0), char, font=font) | |
| lw = char_bbox[2] - char_bbox[0] | |
| lh = ref_bbox[3] - ref_bbox[1] | |
| img_w = max(1, int(lw) + 4) | |
| img_h = max(1, int(lh) + 20) | |
| img = Image.new('RGBA', (img_w, img_h), (0, 0, 0, 0)) | |
| d = ImageDraw.Draw(img) | |
| txt_rgb = hex_to_rgb(color_hex) | |
| x_pos = 2 | |
| y_pos = 10 - ref_bbox[1] | |
| try: | |
| from pilmoji import Pilmoji | |
| with Pilmoji(img) as pilmoji: | |
| pilmoji.text((x_pos, y_pos), char, font=font, fill=txt_rgb + (255,)) | |
| except ImportError: | |
| d.text((x_pos, y_pos), char, font=font, fill=txt_rgb + (255,)) | |
| arr = np.array(img) | |
| clip = mpy.ImageClip(arr[:, :, :3]).set_duration(duration) | |
| mask = mpy.ImageClip(arr[:, :, 3] / 255.0, ismask=True).set_duration(duration) | |
| return clip.set_mask(mask), img_w | |
| st.set_page_config(page_title="NeuzX - Broadcast Pro 720p 60FPS", layout="wide") | |
| st.markdown(""" | |
| <style> | |
| @import url('https://fonts.googleapis.com/css2?family=Mukta:wght@800&display=swap'); | |
| .main { background-color: #0d1117; color: #c9d1d9; font-family: 'Mukta', sans-serif; } | |
| div[data-testid="stExpander"] { background-color: #161b22 !important; border: 1px solid #30363d !important; border-radius: 8px; } | |
| .stButton>button { background-color: #238636 !important; color: white !important; font-weight: bold; width: 100%; border-radius: 6px; border: none; height: 45px; } | |
| .green_screen_bg { background-color: #00FF00; } | |
| @keyframes brightScan { 0% { left: -100%; opacity: 0; } 10% { opacity: 0.9; } 30% { left: 100%; opacity: 0; } 100% { left: 100%; opacity: 0; } } | |
| .ticker_gfx_sweep { overflow: hidden; position: relative; } | |
| .ticker_gfx_sweep::before { content: ""; position: absolute; top: 0; left: -100%; width: 80%; height: 100%; background: linear-gradient(90deg, transparent 0%, rgba(255, 255, 255, 0.9) 50%, transparent 100%); z-index: 2; pointer-events: none; animation: brightScan 5s infinite linear; } | |
| @keyframes flashGlow { 0% { opacity: 0.5; filter: brightness(1); text-shadow: none; } 50% { opacity: 1.0; filter: brightness(1.5); text-shadow: 0 0 15px rgba(255,255,255,1); } 100% { opacity: 0.5; filter: brightness(1); text-shadow: none; } } | |
| @keyframes slideHold { 0% { transform: translateX(100%); } 15% { transform: translateX(0%); } 85% { transform: translateX(0%); } 100% { transform: translateX(-100%); } } | |
| /* PREVIEW LOGO ANIMATION */ | |
| @keyframes logoEntryLoop { 0% { transform: translateX(-30px); opacity: 0; text-shadow: none; } 15% { transform: translateX(0); opacity: 1; text-shadow: none; } 50% { text-shadow: 0 0 12px rgba(255,255,255,0.9); } 85% { transform: translateX(0); opacity: 1; text-shadow: none; } 100% { transform: translateX(30px); opacity: 0; text-shadow: none; } } | |
| .preview-logo-box span { display: inline-block; animation: logoEntryLoop 10s infinite both; } | |
| @keyframes pulse3D { 0% { transform: scale(1); } 100% { transform: scale(1.15); } } | |
| @keyframes float3D { 0% { transform: translateY(-3px) translateX(-3px); } 100% { transform: translateY(3px) translateX(3px); } } | |
| @keyframes shake3D { 0% { transform: translate(1px, 1px) rotate(0deg); } 25% { transform: translate(-1px, -1px) rotate(-2deg); } 50% { transform: translate(1px, -1px) rotate(2deg); } 75% { transform: translate(-1px, 1px) rotate(0deg); } 100% { transform: translate(1px, 1px) rotate(0deg); } } | |
| @keyframes flip3D { 0% { transform: perspective(400px) rotateY(0deg); } 100% { transform: perspective(400px) rotateY(360deg); } } | |
| </style> | |
| """, unsafe_allow_html=True) | |
| st.title("🎬 NeuzX - Broadcast Pro 720p 60FPS") | |
| col_workspace, col_property_panel = st.columns([6, 5]) | |
| anim_options = ["Static", "Scroll Left", "Scroll Right", "Scroll Up", "Scroll Down", "Bounce", "Slide", "Flash/Glow", "Slide & Hold"] | |
| logo_anim_options = ["Static", "3D Pulse (Zoom)", "3D Float", "3D Shake", "3D Flip", "Reverse Letter Slide"] | |
| def get_marquee(text, anim, speed): | |
| if anim in ["Scroll Up", "Scroll Down"]: return text.replace('\n', '<br>') | |
| formatted_text = text.replace('\n', ' ').replace(' ', ' ') | |
| if anim == "Static": return formatted_text | |
| elif anim == "Flash/Glow": return f'<div style="animation: flashGlow 3s infinite; width: 100%; text-align: center;">{formatted_text}</div>' | |
| elif anim == "Slide & Hold": return f'<div style="animation: slideHold 10s infinite; width: 100%; text-align: center;">{formatted_text}</div>' | |
| elif anim == "Scroll Left": return f'<marquee scrollamount="{speed}" direction="left" style="white-space: nowrap; width: 100%;">{formatted_text}</marquee>' | |
| elif anim == "Scroll Right": return f'<marquee scrollamount="{speed}" direction="right" style="white-space: nowrap; width: 100%;">{formatted_text}</marquee>' | |
| elif anim == "Scroll Up": return f'<marquee scrollamount="{speed}" direction="up" style="text-align: center; width: 100%; height: 100%; display: block;">{formatted_text}</marquee>' | |
| elif anim == "Scroll Down": return f'<marquee scrollamount="{speed}" direction="down" style="text-align: center; width: 100%; height: 100%; display: block;">{formatted_text}</marquee>' | |
| elif anim == "Bounce": return f'<marquee scrollamount="{speed}" behavior="alternate" style="text-align: center; width: 100%;">{formatted_text}</marquee>' | |
| elif anim == "Slide": return f'<marquee scrollamount="{speed}" behavior="slide" style="white-space: nowrap; text-align: center; width: 100%;">{formatted_text}</marquee>' | |
| return formatted_text | |
| with col_property_panel: | |
| sliders_locked = st.toggle("🔒 Lock Sliders & Sizes (Prevent Accidental Changes)", value=False) | |
| st.subheader("🛠️ Customization (720p)") | |
| with st.expander("⬆️ Upper Ticker Settings", expanded=True): | |
| top_speed = st.slider("Upper Speed", 1, 20, 6, disabled=sliders_locked) | |
| col1, col2 = st.columns(2) | |
| top_left_bg = col1.color_picker("Top Left BG", "#FF0000") | |
| top_left_color = col1.color_picker("Top Left Text Color", "#FFFF00") | |
| top_left_size = col1.slider("Top Left Font Size", 10, 250, 45, disabled=sliders_locked) | |
| top_left_text = col1.text_area("Top Left Text", "आज की 20 बड़ी खबरें") | |
| top_left_anim = col1.selectbox("Left Animation", anim_options, index=0) | |
| top_right_bg = col2.color_picker("Top Right BG", "#0000FF") | |
| top_right_color = col2.color_picker("Top Right Text Color", "#FFFFFF") | |
| top_right_size = col2.slider("Top Right Font Size", 10, 250, 45, disabled=sliders_locked) | |
| top_right_text = col2.text_area("Top Right Text", "FAST NEWS | 20 बड़ी खबरें") | |
| top_right_anim = col2.selectbox("Right Animation", anim_options, index=8) # Set Slide & Hold as default | |
| with st.expander("⬇️ Lower Ticker Settings", expanded=True): | |
| col7, col8 = st.columns(2) | |
| logo_bg = col7.color_picker("Logo Background", "#FFFF00") | |
| logo_color = col7.color_picker("Logo Text Color", "#000000") | |
| logo_size = col7.slider("Logo Font Size", 10, 250, 60, disabled=sliders_locked) | |
| logo_text = col7.text_input("Logo Text", "NeuzX") | |
| logo_anim = col7.selectbox("Logo Animation", logo_anim_options, index=5) | |
| logo_anim_speed = col7.slider("Logo Anim Speed", 1, 20, 3, disabled=sliders_locked) | |
| bottom_bg = col8.color_picker("Bottom Bar BG", "#0000FF") | |
| bottom_color = col8.color_picker("Bottom Text Color", "#FFFFFF") | |
| bottom_size = col8.slider("Bottom Font Size", 10, 250, 50, disabled=sliders_locked) | |
| default_bottom_txt = "🌐 देश-विदेश की तमाम बड़ी खबरों के लिए हमारे साथ जुड़े रहें... 🎯 सच्ची, सटीक और निष्पक्ष खबरों का एकमात्र ठिकाना - Neuz X 📢 देशभर की सुबह की 20 सबसे बड़ी खबरें सीधे आप... 👍 अगर जानकारी अच्छी लगी तो वीडियो को एक लाइक जरूर करें..." | |
| bottom_text = col8.text_input("Bottom Ticker", default_bottom_txt) | |
| bottom_speed = st.slider("Lower Speed", 1, 20, 3, disabled=sliders_locked) | |
| tl_content = get_marquee(top_left_text, top_left_anim, top_speed) | |
| tr_content = get_marquee(top_right_text, top_right_anim, top_speed) | |
| logo_css = "" | |
| css_speed = max(0.2, 5.0 / max(1, logo_anim_speed)) | |
| if logo_anim == "3D Pulse (Zoom)": logo_css = f"animation: pulse3D {css_speed}s infinite alternate ease-in-out;" | |
| elif logo_anim == "3D Float": logo_css = f"animation: float3D {css_speed}s infinite alternate ease-in-out;" | |
| elif logo_anim == "3D Shake": logo_css = f"animation: shake3D {css_speed/2}s infinite linear;" | |
| elif logo_anim == "3D Flip": logo_css = f"animation: flip3D {css_speed * 1.5}s infinite linear;" | |
| if logo_anim == "Reverse Letter Slide": | |
| span_html = "" | |
| word_len = len(logo_text) | |
| for i, char in enumerate(logo_text): | |
| delay = (word_len - 1 - i) * 0.3 | |
| span_html += f'<span style="animation-delay: {delay}s;">{char}</span>' | |
| logo_preview_html = f'<div class="preview-logo-box" style="font-size: {logo_size * 0.078125:.3f}cqw; line-height: 1; display: flex; align-items: center; justify-content: center;">{span_html}</div>' | |
| else: | |
| logo_preview_html = f'<div style="font-size: {logo_size * 0.078125:.3f}cqw; line-height: 1; display: flex; align-items: center; justify-content: center; {logo_css}">{logo_text}</div>' | |
| cqw_factor = 0.078125 | |
| tl_fz = f"{top_left_size * cqw_factor:.3f}cqw" | |
| tr_fz = f"{top_right_size * cqw_factor:.3f}cqw" | |
| bot_fz = f"{bottom_size * cqw_factor:.3f}cqw" | |
| with col_workspace: | |
| st.subheader("📺 1:1 Pixel-Perfect Monitor") | |
| html_preview = f"""<div class="green_screen_bg" style="container-type: inline-size; width: 100%; aspect-ratio: 16/9; position: relative; overflow: hidden; border: 2px solid white;"><div style="position: absolute; top: 0; left: 0; width: 100%; height: 8%; display: flex; font-weight: bold; z-index: 10;"><div class="ticker_gfx_sweep" style="width: 50%; height: 100%; background: {top_left_bg}; color: {top_left_color}; font-size: {tl_fz}; line-height: 1; display: flex; align-items: center; justify-content: center; overflow: hidden; padding: 0 5px;">{tl_content}</div><div class="ticker_gfx_sweep" style="width: 50%; height: 100%; background: {top_right_bg}; color: {top_right_color}; font-size: {tr_fz}; line-height: 1; display: flex; align-items: center; justify-content: center; overflow: hidden; padding: 0 5px;">{tr_content}</div></div><div style="position: absolute; bottom: 0; left: 0; width: 100%; height: 10%; display: flex; font-weight: bold; z-index: 10;"><div style="width: 20%; background: {logo_bg}; color: {logo_color}; display: flex; align-items: center; justify-content: center; overflow: hidden;">{logo_preview_html}</div><div class="ticker_gfx_sweep" style="width: 80%; background: {bottom_bg}; color: {bottom_color}; font-size: {bot_fz}; line-height: 1; display: flex; align-items: center; overflow: hidden;"><marquee scrollamount="{bottom_speed}">{bottom_text}</marquee></div></div></div>""" | |
| st.markdown(html_preview, unsafe_allow_html=True) | |
| st.markdown("---") | |
| st.info("💡 Render: 720p HD | 60 FPS. (Slide & Hold 10-Second Loop Fix Applied!)") | |
| def get_light_sweep_clip(w, h, duration): | |
| import moviepy.editor as mpy | |
| import numpy as np | |
| sweep_w = max(100, int(w * 0.6)) | |
| arr = np.ones((h, sweep_w, 3), dtype=np.uint8) * 255 | |
| alpha = np.zeros((h, sweep_w), dtype=np.float32) | |
| for i in range(sweep_w): | |
| dist = abs(i - sweep_w/2.0)/(sweep_w/2.0) | |
| alpha[:, i] = 0.75 * (1.0 - (dist ** 2)) | |
| sweep = mpy.ImageClip(arr).set_mask(mpy.ImageClip(alpha, ismask=True)).set_duration(duration) | |
| def sweep_pos(t): | |
| t_c = t % 5.0 | |
| if t_c < 1.5: return (-sweep_w + (w + sweep_w) * (t_c / 1.5), 0) | |
| return (-w*2, 0) | |
| return sweep.set_position(sweep_pos) | |
| def apply_animation(txt_clip, anim_name, speed, box_w, box_h): | |
| txt_w, txt_h = txt_clip.size | |
| if anim_name == "Flash/Glow": | |
| import numpy as np | |
| def change_opacity(get_frame, t): | |
| frame = get_frame(t) | |
| op = 0.5 + 0.5 * np.abs(np.sin(speed * 0.8 * t)) | |
| return frame * op | |
| txt_clip = txt_clip.set_position(('center', 'center')) | |
| if txt_clip.mask is not None: | |
| txt_clip.mask = txt_clip.mask.fl(change_opacity) | |
| return txt_clip | |
| elif anim_name == "Scroll Left": return txt_clip.set_position(lambda t: (box_w - (speed*80.0)*t, 'center')) | |
| elif anim_name == "Scroll Right": return txt_clip.set_position(lambda t: (-txt_w + (speed*80.0)*t, 'center')) | |
| elif anim_name == "Scroll Up": return txt_clip.set_position(lambda t: ('center', box_h - (speed*60.0)*t)) | |
| elif anim_name == "Scroll Down": return txt_clip.set_position(lambda t: ('center', -txt_h + (speed*60.0)*t)) | |
| elif anim_name == "Bounce": | |
| import numpy as np | |
| mid_x = (box_w - txt_w) / 2.0 | |
| return txt_clip.set_position(lambda t: (mid_x - mid_x * np.cos(speed * 0.8 * t), 'center')) | |
| elif anim_name == "Slide": return txt_clip.set_position(lambda t: (min((box_w - txt_w)/2, -txt_w + (speed*80.0)*t), 'center')) | |
| else: return txt_clip.set_position(('center', 'center')) | |
| def process_text_box(text, size, color, bg_color, anim_name, speed, box_w, box_h, duration): | |
| import moviepy.editor as mpy | |
| bg = mpy.ColorClip(size=(box_w, box_h), color=hex_to_rgb(bg_color)).set_duration(duration) | |
| sweep = get_light_sweep_clip(box_w, box_h, duration) | |
| if anim_name == "Slide & Hold": | |
| lines = [l.strip() for l in text.split('\n') if l.strip()] | |
| if not lines: lines = [" "] | |
| # --- FIX: Single Line Repeater for 10-second looping --- | |
| if len(lines) == 1: | |
| lines = [lines[0]] * max(1, int(duration / 10.0)) | |
| line_dur = duration / len(lines) | |
| clips_to_composite = [bg] | |
| for i, line in enumerate(lines): | |
| s_txt = get_text_clip(line, size, color, line_dur, anim_type="Static") | |
| txt_w, txt_h = s_txt.size | |
| mid_x = (box_w - txt_w) / 2.0 | |
| def make_pos_func(w_box, w_txt, m_x, dur, spd): | |
| def pos(t): | |
| slide_time = max(0.2, 3.0 / max(1, spd)) | |
| if slide_time * 2 > dur: slide_time = dur / 2.0 | |
| if t < slide_time: return (w_box - (w_box - m_x) * (t / slide_time), 'center') | |
| elif t > dur - slide_time: | |
| out_t = t - (dur - slide_time) | |
| return (m_x - (m_x + w_txt) * (out_t / slide_time), 'center') | |
| else: return (m_x, 'center') | |
| return pos | |
| s_txt = s_txt.set_position(make_pos_func(box_w, txt_w, mid_x, line_dur, speed)).set_start(i * line_dur) | |
| clips_to_composite.append(s_txt) | |
| clips_to_composite.append(sweep) | |
| return mpy.CompositeVideoClip(clips_to_composite, size=(box_w, box_h)).set_duration(duration) | |
| else: | |
| txt_final = get_text_clip(text, size, color, duration, anim_type=anim_name) | |
| txt_final = apply_animation(txt_final, anim_name, speed, box_w, box_h) | |
| return mpy.CompositeVideoClip([bg, txt_final, sweep], size=(box_w, box_h)) | |
| if st.button("🚀 COMPILE BROADCAST VIDEO (720p 60FPS)", use_container_width=True): | |
| with st.spinner("Rendering Slide & Hold Loop..."): | |
| try: | |
| import moviepy.editor as mpy | |
| import numpy as np | |
| W, H = 1280, 720 | |
| duration = 30.0 | |
| bg = mpy.ColorClip(size=(W, H), color=(0, 255, 0)).set_duration(duration) | |
| clips = [bg] | |
| def tl_pos(t): | |
| if t < 1.0: return (-W//2 + (W//2) * t, 0) | |
| if t > 29.0: return (-W//2 + (W//2) * (30.0 - t), 0) | |
| return (0, 0) | |
| def tr_pos(t): | |
| if t < 1.0: return (W - (W//2) * t, 0) | |
| if t > 29.0: return (W//2 + (W//2) * (t - 29.0), 0) | |
| return (W//2, 0) | |
| def logo_pos(t): | |
| if t < 1.0: return (0, H - int(H*0.1) * t) | |
| if t > 29.0: return (0, int(H*0.9) + int(H*0.1) * (t - 29.0)) | |
| return (0, int(H*0.9)) | |
| def bot_pos(t): | |
| if t < 1.0: return (W - int(W*0.8) * t, int(H*0.9)) | |
| if t > 29.0: return (int(W*0.2) + int(W*0.8) * (t - 29.0), int(H*0.9)) | |
| return (int(W*0.2), int(H*0.9)) | |
| tl_box_w, tl_box_h = W//2, int(H*0.08) | |
| tl_final = process_text_box(top_left_text, top_left_size, top_left_color, top_left_bg, top_left_anim, top_speed, tl_box_w, tl_box_h, duration) | |
| clips.append(tl_final.set_position(tl_pos)) | |
| tr_box_w, tr_box_h = W//2, int(H*0.08) | |
| tr_final = process_text_box(top_right_text, top_right_size, top_right_color, top_right_bg, top_right_anim, top_speed, tr_box_w, tr_box_h, duration) | |
| clips.append(tr_final.set_position(tr_pos)) | |
| logo_box_w, logo_box_h = int(W*0.2), int(H*0.1) | |
| l_bg = mpy.ColorClip(size=(logo_box_w, logo_box_h), color=hex_to_rgb(logo_bg)).set_duration(duration) | |
| if logo_anim == "Reverse Letter Slide": | |
| word = logo_text if logo_text.strip() else " " | |
| letter_clips = [] | |
| char_data = [] | |
| total_w = 0 | |
| for char in word: | |
| c_clip, c_w = get_char_clip(char, logo_size, logo_color, duration) | |
| char_data.append((char, c_clip, c_w)) | |
| total_w += c_w | |
| start_x = (logo_box_w - total_w) / 2 | |
| current_x = start_x | |
| for i, (char, c_clip, c_w) in enumerate(char_data): | |
| target_x = current_x | |
| current_x += c_w | |
| delay = (len(word) - 1 - i) * 0.25 | |
| def make_char_pos(tx, dly, index): | |
| def cpos(t): | |
| tc = t % 10.0 | |
| if tc < dly: return (-200, 0) | |
| if tc < dly + 0.6: | |
| progress = (tc - dly) / 0.6 | |
| return (tx - 100 + 100 * progress, 'center') | |
| if tc > 8.0 + (index*0.1): | |
| p_out = (tc - (8.0 + (index*0.1))) / 0.5 | |
| return (tx + 150 * p_out, 'center') | |
| return (tx, 'center') | |
| return cpos | |
| def glow_fl(get_frame, t): | |
| frame = get_frame(t) | |
| tc = t % 10.0 | |
| if 2.0 < tc < 8.0: | |
| pulse = 1.0 + 0.4 * abs(math.sin(tc * (logo_anim_speed/2.0))) | |
| return (frame * pulse).clip(0, 255) | |
| return frame | |
| c_clip = c_clip.set_position(make_char_pos(target_x, delay, i)).fl(glow_fl) | |
| letter_clips.append(c_clip) | |
| logo_final = mpy.CompositeVideoClip([l_bg] + letter_clips, size=(logo_box_w, logo_box_h)) | |
| else: | |
| logo_txt_clip = get_text_clip(logo_text, logo_size, logo_color, duration, anim_type="Static") | |
| if logo_anim == "3D Pulse (Zoom)": logo_txt_clip = logo_txt_clip.resize(lambda t: 1.0 + 0.1 * np.sin(logo_anim_speed * 1.5 * t)).set_position(('center', 'center')) | |
| elif logo_anim == "3D Float": logo_txt_clip = logo_txt_clip.set_position(lambda t: ((logo_box_w - logo_txt_clip.w)/2 + 10 * np.sin(logo_anim_speed * 1.5 * t), (logo_box_h - logo_txt_clip.h)/2 + 5 * np.cos(logo_anim_speed * 1.5 * t))) | |
| elif logo_anim == "3D Shake": logo_txt_clip = logo_txt_clip.set_position(lambda t: ((logo_box_w - logo_txt_clip.w)/2 + 5 * np.sin(logo_anim_speed * 5 * t), (logo_box_h - logo_txt_clip.h)/2 + 3 * np.cos(logo_anim_speed * 8 * t))) | |
| elif logo_anim == "3D Flip": | |
| orig_w, orig_h = logo_txt_clip.size | |
| def flip_size(t): return (max(1, int(orig_w * abs(np.cos(logo_anim_speed * 1.2 * t)))), orig_h) | |
| logo_txt_clip = logo_txt_clip.resize(flip_size).set_position(('center', 'center')) | |
| else: logo_txt_clip = logo_txt_clip.set_position(('center', 'center')) | |
| logo_final = mpy.CompositeVideoClip([l_bg, logo_txt_clip], size=(logo_box_w, logo_box_h)) | |
| clips.append(logo_final.set_position(logo_pos)) | |
| bot_box_w, bot_box_h = int(W*0.8), int(H*0.1) | |
| bot_final = process_text_box(bottom_text, bottom_size, bottom_color, bottom_bg, "Scroll Left", bottom_speed, bot_box_w, bot_box_h, duration) | |
| clips.append(bot_final.set_position(bot_pos)) | |
| final = mpy.CompositeVideoClip(clips) | |
| out = "NeuzX_Broadcast_Pro_720p_60FPS.mp4" | |
| final.write_videofile(out, fps=60, codec="libx264", preset="ultrafast", threads=2, audio=False, logger=None) | |
| st.success("🎉 Single Line Auto-Loop Fix Applied! Video is Ready.") | |
| with open(out, "rb") as f: | |
| st.download_button("⬇️ DOWNLOAD VIDEO", f, out, "video/mp4") | |
| except Exception as e: | |
| st.error(f"Render Error: {e}") | |