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(""" """, 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', '
') formatted_text = text.replace('\n', '      ').replace(' ', '  ') if anim == "Static": return formatted_text elif anim == "Flash/Glow": return f'
{formatted_text}
' elif anim == "Slide & Hold": return f'
{formatted_text}
' elif anim == "Scroll Left": return f'{formatted_text}' elif anim == "Scroll Right": return f'{formatted_text}' elif anim == "Scroll Up": return f'{formatted_text}' elif anim == "Scroll Down": return f'{formatted_text}' elif anim == "Bounce": return f'{formatted_text}' elif anim == "Slide": return f'{formatted_text}' 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'{char}' logo_preview_html = f'
{span_html}
' else: logo_preview_html = f'
{logo_text}
' 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"""
{tl_content}
{tr_content}
{logo_preview_html}
{bottom_text}
""" 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}")