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''
elif anim == "Scroll Right": return f''
elif anim == "Scroll Up": return f''
elif anim == "Scroll Down": return f''
elif anim == "Bounce": return f''
elif anim == "Slide": return f''
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"""{logo_preview_html}
"""
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}")