Mrsadeeqnextgenhub's picture
Rename app.py to app.py55
7c398da verified
Raw
History Blame Contribute Delete
26.7 kB
import streamlit as st
import requests
import edge_tts
import asyncio
import os
import subprocess
import re
import nest_asyncio
import json
import shlex
import time
import html
from typing import List, Optional
nest_asyncio.apply()
# CONFIG
st.set_page_config(
page_title="Islamic Shorts Creator",
page_icon="🕌",
layout="centered",
initial_sidebar_state="collapsed"
)
CHANNEL_NAME = "Abubakar Daily Islamic Shorts"
LOGO_FILE = "logo.png"
ANTHROPIC_KEY = os.getenv("ANTHROPIC_API_KEY", "").strip()
PEXELS_KEY = os.getenv("PEXELS_API_KEY", "").strip()
if "final_video_path" not in st.session_state:
st.session_state.final_video_path = None
if "script_text" not in st.session_state:
st.session_state.script_text = ""
if "metadata" not in st.session_state:
st.session_state.metadata = {}
if "last_metadata_file" not in st.session_state:
st.session_state.last_metadata_file = ""
# UI styling
st.markdown("""
<style>
@import url('https://fonts.googleapis.com/css2?family=Cinzel:wght@400;600;900&family=Crimson+Pro:ital,wght@0,300;0,400,1,300&family=Amiri:wght@400;700&display=swap');
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
.stApp { background: radial-gradient(ellipse at top, #0d1b2a 0%, #050e18 60%, #000 100%); min-height: 100vh; font-family: 'Crimson Pro', serif; }
.stApp::before { content: ''; position: fixed; inset: 0; background-image: radial-gradient(1px 1px at 15% 20%, rgba(212,175,55,0.4) 0%, transparent 100%), radial-gradient(1px 1px at 80% 10%, rgba(255,255,255,0.3) 0%, transparent 100%), radial-gradient(1px 1px at 45% 70%, rgba(212,175,55,0.2) 0%, transparent 100%), radial-gradient(2px 2px at 90% 80%, rgba(255,255,255,0.15) 0%, transparent 100%), radial-gradient(1px 1px at 5% 90%, rgba(212,175,55,0.3) 0%, transparent 100%); pointer-events: none; z-index: 0; }
.hero-header { text-align: center; padding: 2.5rem 1rem 1.5rem; position: relative; }
.hero-bismillah { font-family: 'Amiri', serif; font-size: 2.2rem; color: #d4af37; letter-spacing: 0.05em; text-shadow: 0 0 30px rgba(212,175,55,0.5); display: block; margin-bottom: 0.5rem; }
.hero-title { font-family: 'Cinzel', serif; font-size: clamp(1.5rem, 4vw, 2.4rem); font-weight: 900; background: linear-gradient(135deg, #d4af37 0%, #f5e07a 50%, #b8942c 100%); -webkit-background-clip: text; -webkit-text-fill-color: transparent; background-clip: text; text-transform: uppercase; letter-spacing: 0.15em; line-height: 1.2; }
.hero-sub { font-family: 'Crimson Pro', serif; font-size: 1rem; color: rgba(212,175,55,0.6); letter-spacing: 0.3em; text-transform: uppercase; margin-top: 0.4rem; }
.hero-divider { width: 200px; height: 1px; background: linear-gradient(90deg, transparent, #d4af37, transparent); margin: 1rem auto 0; }
.panel { background: linear-gradient(135deg, rgba(212,175,55,0.06) 0%, rgba(13,27,42,0.9) 40%, rgba(5,14,24,0.95) 100%); border: 1px solid rgba(212,175,55,0.18); border-radius: 16px; padding: 1.8rem 2rem; margin: 1rem 0; backdrop-filter: blur(10px); box-shadow: 0 8px 32px rgba(0,0,0,0.6), inset 0 1px 0 rgba(212,175,55,0.1); position: relative; }
.panel::before { content: ''; position: absolute; top: -1px; left: 20%; right: 20%; height: 2px; background: linear-gradient(90deg, transparent, #d4af37, transparent); border-radius: 2px; }
.section-label { font-family: 'Cinzel', serif; font-size: 0.7rem; color: #d4af37; letter-spacing: 0.3em; text-transform: uppercase; margin-bottom: 1rem; display: flex; align-items: center; gap: 0.6rem; }
.section-label::after { content: ''; flex: 1; height: 1px; background: linear-gradient(90deg, rgba(212,175,55,0.3), transparent); }
div[data-testid="stSelectbox"] label, div[data-testid="stTextInput"] label, div[data-testid="stTextArea"] label { font-family: 'Cinzel', serif !important; font-size: 0.75rem !important; color: rgba(212,175,55,0.8) !important; letter-spacing: 0.2em !important; text-transform: uppercase !important; }
div[data-testid="stSelectbox"] > div > div, div[data-testid="stTextInput"] > div > div > input, div[data-testid="stTextArea"] > div > div > textarea { background: rgba(5,14,24,0.8) !important; border: 1px solid rgba(212,175,55,0.25) !important; border-radius: 8px !important; color: #f0e8d0 !important; font-family: 'Crimson Pro', serif !important; font-size: 1.05rem !important; }
div[data-testid="stSelectbox"] > div > div:focus-within, div[data-testid="stTextInput"] > div > div > input:focus, div[data-testid="stTextArea"] > div > div > textarea:focus { border-color: rgba(212,175,55,0.6) !important; box-shadow: 0 0 15px rgba(212,175,55,0.12) !important; }
div[data-testid="stButton"] > button[kind="primary"], div[data-testid="stButton"] > button { width: 100% !important; background: linear-gradient(135deg, #b8942c 0%, #d4af37 50%, #f0c840 100%) !important; color: #050e18 !important; font-family: 'Cinzel', serif !important; font-size: 0.85rem !important; font-weight: 700 !important; letter-spacing: 0.25em !important; text-transform: uppercase !important; border: none !important; border-radius: 10px !important; padding: 0.85rem 2rem !important; cursor: pointer !important; transition: all 0.3s ease !important; box-shadow: 0 4px 20px rgba(212,175,55,0.3), inset 0 1px 0 rgba(255,255,255,0.2) !important; }
div[data-testid="stButton"] > button:hover { transform: translateY(-2px) !important; box-shadow: 0 8px 30px rgba(212,175,55,0.45) !important; }
.script-box { background: rgba(0,0,0,0.4); border: 1px solid rgba(212,175,55,0.15); border-left: 3px solid #d4af37; border-radius: 8px; padding: 1.2rem 1.5rem; font-family: 'Crimson Pro', serif; font-size: 1.05rem; color: #e8dfc0; line-height: 1.8; white-space: pre-wrap; margin: 0.5rem 0 1rem; }
div[data-testid="stDownloadButton"] > button { background: transparent !important; border: 1px solid rgba(212,175,55,0.4) !important; color: #d4af37 !important; font-family: 'Cinzel', serif !important; font-size: 0.75rem !important; letter-spacing: 0.2em !important; border-radius: 8px !important; width: 100% !important; padding: 0.7rem !important; transition: all 0.3s !important; }
div[data-testid="stDownloadButton"] > button:hover { background: rgba(212,175,55,0.1) !important; border-color: #d4af37 !important; box-shadow: 0 0 20px rgba(212,175,55,0.2) !important; }
.footer { text-align: center; padding: 2rem 1rem; font-family: 'Amiri', serif; color: rgba(212,175,55,0.35); font-size: 1rem; letter-spacing: 0.1em; }
</style>""", unsafe_allow_html=True)
# HERO
st.markdown("""
<div class="hero-header">
<span class="hero-bismillah">بِسْمِ اللَّهِ الرَّحْمَنِ الرَّحِيمِ</span>
<div class="hero-title">Islamic Shorts Creator</div>
<div class="hero-sub">Abubakar Daily Islamic Channel</div>
<div class="hero-divider"></div>
</div>
""", unsafe_allow_html=True)
# CORE FUNCTIONS
def call_claude(topic, lang):
prompt = (
f"Write a 35-second spiritual Islamic script in {lang} about '{topic}'. "
"Output ONLY the spoken text. No titles, no hashtags, no stage directions. "
"Make sure the script ends with a short, thought-provoking question to encourage comments."
)
try:
r = requests.post(
"https://api.anthropic.com/v1/messages",
headers={
"x-api-key": ANTHROPIC_KEY,
"anthropic-version": "2023-06-01",
"content-type": "application/json"
},
json={
"model": "claude-sonnet-4-5-20251001",
"max_tokens": 1024,
"messages": [{"role": "user", "content": prompt}]
},
timeout=30
)
return r.json()['content'][0]['text'].strip()
except Exception as e:
return f"Error: {e}"
def call_claude_metadata(topic, lang, script_text):
prompt = (
"You are a metadata assistant. Given the following short Islamic spoken script, "
"produce a JSON object with keys: title (max 60 chars), description (50-150 words), "
"hashtags (an array of 5 trending hashtags, include the # symbol). "
"Output ONLY valid JSON and nothing else.\n\n"
f"Language: {lang}\nTopic: {topic}\n\nScript:\n{script_text}\n"
)
try:
r = requests.post(
"https://api.anthropic.com/v1/messages",
headers={
"x-api-key": ANTHROPIC_KEY,
"anthropic-version": "2023-06-01",
"content-type": "application/json"
},
json={
"model": "claude-sonnet-4-5-20251001",
"max_tokens": 1024,
"messages": [{"role": "user", "content": prompt}]
},
timeout=30
)
raw = r.json()['content'][0]['text'].strip()
try:
return json.loads(raw)
except Exception:
jstart = raw.find("{")
jend = raw.rfind("}")
if jstart != -1 and jend != -1:
try:
return json.loads(raw[jstart:jend+1])
except Exception:
return {"title": "", "description": raw, "hashtags": []}
return {"title": "", "description": raw, "hashtags": []}
except Exception as e:
return {"title": "", "description": f"Error: {e}", "hashtags": []}
async def tts(text, voice, path):
clean = re.sub(r'[#*()<>]', '', text)
comm = edge_tts.Communicate(clean, voice)
await comm.save(path)
_tts = tts
def get_tts_duration(path: str) -> float:
try:
result = subprocess.run(
["ffprobe", "-v", "error", "-show_entries", "format=duration",
"-of", "default=noprint_wrappers=1:nokey=1", path],
capture_output=True, text=True
)
return float(result.stdout.strip())
except:
return 35.0
def escape_ffmpeg_text(text: str) -> str:
"""Escape text safely for ffmpeg drawtext filter"""
# Remove characters that break ffmpeg filter syntax
text = re.sub(r"[':=\\,\[\]@{}()]", " ", text)
# Collapse multiple spaces
text = re.sub(r" +", " ", text).strip()
return text
def build_professional_captions(words, audio_dur, tw, th, aspect):
"""
Professional caption style:
- Bottom third positioning (like YouTube/Netflix)
- Large bold white text with strong black outline (no semi-transparent box)
- 4-5 words per line max
- Two lines at a time
- Smooth word-timed display
"""
if aspect == "9:16":
fontsize = 62
y_pos = int(th * 0.72) # 72% from top = lower third for portrait
chunk_size = 4
else:
fontsize = 52
y_pos = int(th * 0.78) # lower third for landscape
chunk_size = 5
total_words = len(words)
sec_per_word = audio_dur / max(total_words, 1)
drawtext_filters = []
for i in range(0, total_words, chunk_size):
chunk = words[i: i + chunk_size]
half = (len(chunk) + 1) // 2
line1 = escape_ffmpeg_text(" ".join(chunk[:half]))
line2 = escape_ffmpeg_text(" ".join(chunk[half:])) if len(chunk) > half else ""
t_start = i * sec_per_word
t_end = t_start + (len(chunk) * sec_per_word)
enable = f"between(t,{t_start:.3f},{t_end:.3f})"
# LINE 1 — outline via borderw (professional look, no box)
df1 = (
f"drawtext=text='{line1}':"
f"fontcolor=white:fontsize={fontsize}:font=Arial:"
f"borderw=4:bordercolor=black@0.95:"
f"x=(w-text_w)/2:y={y_pos - fontsize - 10}:"
f"enable='{enable}'"
)
drawtext_filters.append(df1)
# LINE 2 (if exists)
if line2:
df2 = (
f"drawtext=text='{line2}':"
f"fontcolor=white:fontsize={fontsize}:font=Arial:"
f"borderw=4:bordercolor=black@0.95:"
f"x=(w-text_w)/2:y={y_pos}:"
f"enable='{enable}'"
)
drawtext_filters.append(df2)
return drawtext_filters
def create_video_centered(clip_paths: List[str], voice_path: str, out_path: str, script: str, aspect: str) -> bool:
try:
audio_dur = get_tts_duration(voice_path)
if aspect == "9:16":
tw, th = 720, 1280
watermark_fs = 30
else:
tw, th = 1280, 720
watermark_fs = 24
# ── Step 1: Scale & Crop clips
scaled = []
for i, src in enumerate(clip_paths):
dst = f"/tmp/scaled_{i}.mp4"
scale_cmd = [
"ffmpeg", "-y",
"-i", src,
"-vf", f"scale={tw}:{th}:force_original_aspect_ratio=increase,crop={tw}:{th}",
"-preset", "ultrafast",
"-an", dst
]
subprocess.run(scale_cmd, capture_output=True, text=True)
if os.path.exists(dst):
scaled.append(dst)
if not scaled:
return False
# Concatenate
list_file = "/tmp/concat_list.txt"
with open(list_file, "w") as lf:
for p in scaled:
lf.write(f"file '{p}'\n")
joined = "/tmp/joined_raw.mp4"
subprocess.run(["ffmpeg", "-y", "-f", "concat", "-safe", "0",
"-i", list_file, "-c", "copy", joined], capture_output=True, text=True)
# ── Step 2: Loop/trim to audio duration
looped = "/tmp/looped_bg.mp4"
epsilon = 0.05
subprocess.run([
"ffmpeg", "-y", "-stream_loop", "-1", "-i", joined,
"-t", f"{audio_dur + epsilon:.3f}", "-c", "copy", looped
], capture_output=True, text=True)
if not os.path.exists(looped):
looped = joined
# ── Step 3: Build professional captions
clean_script = re.sub(r'[^\w\s]', '', script)
words = clean_script.split()
caption_filters = build_professional_captions(words, audio_dur, tw, th, aspect)
# ── Step 4: Watermark at bottom (smaller, elegant)
watermark_text = "Abubakar Daily Islamic Shorts"
wm_y = th - 36
watermark_draw = (
f"drawtext=text='{watermark_text}':"
f"fontcolor=white@0.7:fontsize={watermark_fs}:font=Arial:"
f"borderw=2:bordercolor=black@0.8:"
f"x=(w-text_w)/2:y={wm_y}:"
f"enable='between(t,0,{audio_dur:.3f})'"
)
# Combine all drawtext filters
all_text_filters = caption_filters + [watermark_draw]
combined_text = ",".join(all_text_filters)
# ── Step 5: Build ffmpeg command
if os.path.exists(LOGO_FILE):
filter_complex = (
f"[2:v]scale=110:110[logo];"
f"[0:v][logo]overlay=W-w-16:16[tmp];"
f"[tmp]{combined_text}[vout]"
)
ff_cmd = [
"ffmpeg", "-y",
"-i", looped,
"-i", voice_path,
"-i", LOGO_FILE,
"-filter_complex", filter_complex,
"-map", "[vout]",
"-map", "1:a",
"-c:v", "libx264", "-preset", "ultrafast",
"-shortest", "-t", f"{audio_dur:.3f}",
out_path
]
else:
filter_complex = f"[0:v]{combined_text}[vout]"
ff_cmd = [
"ffmpeg", "-y",
"-i", looped,
"-i", voice_path,
"-filter_complex", filter_complex,
"-map", "[vout]",
"-map", "1:a",
"-c:v", "libx264", "-preset", "ultrafast",
"-shortest", "-t", f"{audio_dur:.3f}",
out_path
]
proc = subprocess.run(ff_cmd, capture_output=True, text=True)
if proc.returncode != 0:
print("ffmpeg stderr:", proc.stderr[-3000:])
return False
# Trim to exact duration if needed
if os.path.exists(out_path):
try:
pr = subprocess.run(
["ffprobe", "-v", "error", "-show_entries", "format=duration",
"-of", "default=noprint_wrappers=1:nokey=1", out_path],
capture_output=True, text=True
)
vdur = float(pr.stdout.strip())
except:
vdur = None
if vdur and abs(vdur - audio_dur) > 0.05:
trimmed = out_path + ".trim.mp4"
subprocess.run(["ffmpeg", "-y", "-i", out_path,
"-t", f"{audio_dur:.3f}", "-c", "copy", trimmed],
capture_output=True, text=True)
if os.path.exists(trimmed):
os.replace(trimmed, out_path)
return os.path.exists(out_path)
except Exception as e:
print(f"Error in create_video_centered: {e}")
return False
# INPUT PANEL
st.markdown('<div class="panel">', unsafe_allow_html=True)
st.markdown('<div class="section-label">🌙 Zaɓuɓɓuka — Settings</div>', unsafe_allow_html=True)
col1, col2 = st.columns([1, 1])
with col1:
lang = st.selectbox("Harshe / Language", ["Hausa 🇳🇬", "Larabci 🇸🇦", "English 🇺🇸"], key="lang_select")
with col2:
voice_map = {"Hausa 🇳🇬": "ha-NG-AbdullahNeural", "Larabci 🇸🇦": "ar-SA-HamedNeural", "English 🇺🇸": "en-US-AndrewNeural"}
selected_voice = voice_map[lang]
st.text_input("Muryar da za a yi amfani", value=selected_voice, disabled=True)
topic = st.text_input("Maudu'i — Topic", placeholder="e.g. Tuba, Tawakkul, Ƙaunar Allah...", key="topic_input")
st.markdown("<br>", unsafe_allow_html=True)
manual_script = st.text_area(
"Manual Script (paste here to bypass AI generation)",
value="",
placeholder="Paste your own spoken script here. If filled, AI generation will be skipped.",
height=150,
key="manual_script"
)
video_size = st.selectbox("Video Size / Aspect Ratio", ["Shorts/Reels (9:16)", "Long Video (16:9)"], key="video_size_select")
aspect_map = {"Shorts/Reels (9:16)": "9:16", "Long Video (16:9)": "16:9"}
selected_aspect = aspect_map[video_size]
st.markdown('</div>', unsafe_allow_html=True)
# GENERATE BUTTON
st.markdown('<div class="panel">', unsafe_allow_html=True)
st.markdown('<div class="section-label">⚙️ Ƙirƙirar Bidiyo</div>', unsafe_allow_html=True)
if st.button("🚀 GENERATE ISLAMIC VIDEO", use_container_width=True):
if not topic.strip():
st.warning("⚠️ Rubuta maudu'i da farko.")
else:
if manual_script and manual_script.strip():
script = manual_script.strip()
else:
with st.spinner("✦ Ana rubuta rubutu..."):
script = call_claude(topic, lang.split()[0])
st.session_state.script_text = script
st.markdown("**📜 Rubutun da aka ƙirƙira / Used Script:**")
st.markdown(f'<div class="script-box">{html.escape(script)}</div>', unsafe_allow_html=True)
with st.spinner("✦ Ana ƙirƙirar metadata..."):
metadata = call_claude_metadata(topic, lang.split()[0], script)
st.session_state.metadata = metadata
with st.spinner("✦ Ana ƙirƙirar murya (TTS)..."):
voice_path = "/tmp/tts_audio.mp3"
asyncio.run(_tts(script, selected_voice, voice_path))
with st.spinner("✦ Ana neman bidiyon bango masu dacewa..."):
headers = {"Authorization": PEXELS_KEY}
preferred_queries = ["beautiful mosques", "flowing river water", "deep green forest", "majestic wildlife"]
clip_paths = []
for q in preferred_queries:
if len(clip_paths) >= 4:
break
try:
resp = requests.get(
f"https://api.pexels.com/videos/search?query={requests.utils.quote(q)}&per_page=3&orientation=portrait",
headers=headers, timeout=20
)
r = resp.json()
for v in r.get('videos', []):
chosen = None
for f in v.get('video_files', []):
if f.get('file_type', '').lower() == 'video/mp4' and f.get('width', 0) >= 720:
chosen = f
break
if chosen:
raw_path = f"/tmp/r_{len(clip_paths)}.mp4"
dl = requests.get(chosen['link'], stream=True, timeout=60)
with open(raw_path, "wb") as fh:
for chunk in dl.iter_content(chunk_size=8192):
if chunk:
fh.write(chunk)
clip_paths.append(raw_path)
break
except Exception as e:
print(f"Pexels fetch error for query '{q}': {e}")
if not clip_paths:
st.error("❌ Ba a sami bidiyo ba daga Pexels.")
else:
with st.spinner(f"✦ Ana haɗa bidiyo gaba daya..."):
out_path = "final_islamic_video.mp4"
success = create_video_centered(clip_paths, voice_path, out_path, script, selected_aspect)
if success:
st.session_state.final_video_path = out_path
st.success("✅ An gama! Bidiyon yana ƙasa.")
ts = int(time.time())
metadata_filename = f"/tmp/metadata_{ts}.json"
try:
with open(metadata_filename, "w", encoding="utf-8") as mf:
json.dump(st.session_state.metadata, mf, ensure_ascii=False, indent=2)
st.session_state.last_metadata_file = metadata_filename
except Exception as e:
st.session_state.last_metadata_file = ""
md = st.session_state.metadata or {}
uid = str(int(time.time() * 1000))
title_val = html.escape(md.get("title", ""))
desc_val = html.escape(md.get("description", ""))
tags_list = md.get("hashtags", [])
tags_val = " ".join(tags_list) if isinstance(tags_list, list) else str(tags_list)
tags_val = html.escape(tags_val)
meta_html = f"""
<div style="display:flex;flex-direction:column;gap:10px;">
<label style="font-weight:700;color:#d4af37">Suggested Title</label>
<textarea id="title_{uid}" style="width:100%;height:48px;border-radius:8px;padding:8px;">{title_val}</textarea>
<div style="display:flex;gap:8px;margin-top:6px;">
<button onclick="navigator.clipboard.writeText(document.getElementById('title_{uid}').value).then(()=>{{alert('Title copied')}})" style="background:#d4af37;color:#050e18;padding:8px;border-radius:8px;border:none;cursor:pointer">Copy Title</button>
</div>
<label style="font-weight:700;color:#d4af37;margin-top:8px;">Suggested Description</label>
<textarea id="desc_{uid}" style="width:100%;height:160px;border-radius:8px;padding:8px;">{desc_val}</textarea>
<div style="display:flex;gap:8px;margin-top:6px;">
<button onclick="navigator.clipboard.writeText(document.getElementById('desc_{uid}').value).then(()=>{{alert('Description copied')}})" style="background:#d4af37;color:#050e18;padding:8px;border-radius:8px;border:none;cursor:pointer">Copy Description</button>
</div>
<label style="font-weight:700;color:#d4af37;margin-top:8px;">Suggested Hashtags</label>
<textarea id="tags_{uid}" style="width:100%;height:48px;border-radius:8px;padding:8px;">{tags_val}</textarea>
<div style="display:flex;gap:8px;margin-top:6px;">
<button onclick="navigator.clipboard.writeText(document.getElementById('tags_{uid}').value).then(()=>{{alert('Hashtags copied')}})" style="background:#d4af37;color:#050e18;padding:8px;border-radius:8px;border:none;cursor:pointer">Copy Hashtags</button>
</div>
<div style="display:flex;gap:8px;margin-top:12px;">
<button onclick="(function(){{var combined = document.getElementById('title_{uid}').value + '\\n\\n' + document.getElementById('desc_{uid}').value + '\\n\\n' + document.getElementById('tags_{uid}').value; navigator.clipboard.writeText(combined).then(()=>{{alert('All metadata copied (Title + Description + Hashtags)')}})}})()" style="background:#b8942c;color:#050e18;padding:10px;border-radius:10px;border:none;cursor:pointer">Copy All (YouTube Ready)</button>
</div>
</div>
"""
st.markdown('<div class="panel">', unsafe_allow_html=True)
st.markdown('<div class="section-label">📝 Generated Title, Description & Hashtags</div>', unsafe_allow_html=True)
st.markdown(meta_html, unsafe_allow_html=True)
try:
st.download_button(
label="📥 Download metadata JSON",
data=json.dumps(md, ensure_ascii=False, indent=2),
file_name=f"metadata_{ts}.json",
mime="application/json",
use_container_width=True
)
if st.session_state.last_metadata_file:
st.markdown(f"<small style='color:#d4af37'>Server copy: {st.session_state.last_metadata_file}</small>", unsafe_allow_html=True)
except Exception as e:
st.write("Failed to create metadata download:", e)
st.markdown('</div>', unsafe_allow_html=True)
else:
st.error("❌ Kuskure wajen haɗa bidiyo. (Sake gwadawa)")
st.markdown('</div>', unsafe_allow_html=True)
# VIDEO OUTPUT PANEL
if st.session_state.final_video_path and os.path.exists(st.session_state.final_video_path):
st.markdown('<div class="panel">', unsafe_allow_html=True)
st.markdown('<div class="section-label">🎬 Bidiyon da aka Ƙirƙira</div>', unsafe_allow_html=True)
st.video(st.session_state.final_video_path)
st.markdown("<br>", unsafe_allow_html=True)
with open(st.session_state.final_video_path, "rb") as fh:
st.download_button(
label="📥 SAUKE BIDIYO — Download Video",
data=fh,
file_name="Abubakar_Islamic_Short.mp4",
mime="video/mp4",
use_container_width=True
)
st.markdown('</div>', unsafe_allow_html=True)
# FOOTER
st.markdown("""
<div class="footer">
&nbsp;&nbsp; Abubakar Daily Islamic Shorts &nbsp;&nbsp;
</div>
""", unsafe_allow_html=True)