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" FREEMODEL_KEY = os.getenv("FREEMODEL_API_KEY", "").strip() PEXELS_KEY = os.getenv("PEXELS_API_KEY", "").strip() # freemodel.dev config FREEMODEL_BASE = "https://api.freemodel.dev/v1" FREEMODEL_MODEL = "gpt-5.5" 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 = "" if "thumbnail_path" not in st.session_state: st.session_state.thumbnail_path = None if "topic_suggestions" not in st.session_state: st.session_state.topic_suggestions = [] if "nasheed_path" not in st.session_state: st.session_state.nasheed_path = None if "video_history" not in st.session_state: # load_history defined later โ€” safe inline load here try: import json as _json _hf = "/tmp/video_history.json" if os.path.exists(_hf): with open(_hf, "r", encoding="utf-8") as _f: st.session_state.video_history = _json.load(_f) else: st.session_state.video_history = [] except: st.session_state.video_history = [] if "export_paths" not in st.session_state: st.session_state.export_paths = {} if "batch_results" not in st.session_state: st.session_state.batch_results = [] if "yt_status" not in st.session_state: st.session_state.yt_status = "" # UI styling st.markdown(""" """, unsafe_allow_html=True) # HERO st.markdown("""
ุจูุณู’ู…ู ุงู„ู„ูŽู‘ู‡ู ุงู„ุฑูŽู‘ุญู’ู…ูŽู†ู ุงู„ุฑูŽู‘ุญููŠู…ู
Islamic Shorts Creator
Abubakar Daily Islamic Channel
""", unsafe_allow_html=True) # CORE FUNCTIONS def call_freemodel(topic, lang, duration_sec: int = 35): lang_instruction = { "Hausa": "Write ONLY in Hausa language.", "Larabci": "ุงูƒุชุจ ุงู„ู†ุต ุจุงู„ู„ุบุฉ ุงู„ุนุฑุจูŠุฉ ุงู„ูุตุญู‰ ูู‚ุท. ู„ุง ุชุณุชุฎุฏู… ุฃูŠ ู„ุบุฉ ุฃุฎุฑู‰.", "Arabic": "ุงูƒุชุจ ุงู„ู†ุต ุจุงู„ู„ุบุฉ ุงู„ุนุฑุจูŠุฉ ุงู„ูุตุญู‰ ูู‚ุท. ู„ุง ุชุณุชุฎุฏู… ุฃูŠ ู„ุบุฉ ุฃุฎุฑู‰.", "English": "Write ONLY in English language.", }.get(lang, f"Write ONLY in {lang} language.") prompt = ( f"{lang_instruction} " f"Write a {duration_sec}-second spiritual Islamic script about '{topic}'. " "Write like an imam giving a heartfelt short reminder. " "Use vivid imagery and short powerful sentences. " "Pause naturally between ideas. " "Output ONLY the spoken text. No titles, no hashtags, no stage directions, no translation. " "End with a short thought-provoking question to encourage comments." ) try: r = requests.post( f"{FREEMODEL_BASE}/chat/completions", headers={ "Authorization": f"Bearer {FREEMODEL_KEY}", "Content-Type": "application/json" }, json={ "model": FREEMODEL_MODEL, "messages": [{"role": "user", "content": prompt}], "max_tokens": 1024 }, timeout=60 ) return r.json()['choices'][0]['message']['content'].strip() except Exception as e: return f"Error: {e}" def call_freemodel_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( f"{FREEMODEL_BASE}/chat/completions", headers={ "Authorization": f"Bearer {FREEMODEL_KEY}", "Content-Type": "application/json" }, json={ "model": FREEMODEL_MODEL, "messages": [{"role": "user", "content": prompt}], "max_tokens": 1024 }, timeout=60 ) raw = r.json()['choices'][0]['message']['content'].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": []} # โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ• # FEATURE 1: AI Topic Suggestions # โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ• def get_topic_suggestions(lang: str) -> list: prompt = ( f"Give me 10 trending Islamic YouTube Shorts topic ideas in {lang}. " "Each topic should be short (3-6 words), spiritually engaging, and suitable for a 30-40 second video. " "Output ONLY a JSON array of 10 strings. No explanation, no numbering, no extra text." ) try: r = requests.post( f"{FREEMODEL_BASE}/chat/completions", headers={"Authorization": f"Bearer {FREEMODEL_KEY}", "Content-Type": "application/json"}, json={"model": FREEMODEL_MODEL, "messages": [{"role": "user", "content": prompt}], "max_tokens": 512}, timeout=30 ) raw = r.json()['choices'][0]['message']['content'].strip() raw = re.sub(r'^```json|^```|```$', '', raw, flags=re.MULTILINE).strip() return json.loads(raw) except Exception as e: print(f"Topic suggestion error: {e}") return [] # โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ• # FEATURE 2: Thumbnail Generator # โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ• def generate_thumbnail(title: str, aspect: str, out_path: str = "/tmp/thumbnail.jpg") -> bool: try: from PIL import Image, ImageDraw, ImageFont import textwrap if aspect == "9:16": w, h = 720, 1280 else: w, h = 1280, 720 # Background gradient (dark blue โ†’ black) img = Image.new("RGB", (w, h), (5, 14, 24)) draw = ImageDraw.Draw(img) # Gold gradient overlay at top for y in range(h // 3): alpha = int(80 * (1 - y / (h / 3))) draw.line([(0, y), (w, y)], fill=(212, 175, 55, alpha)) # Decorative border border = 18 draw.rectangle([border, border, w - border, h - border], outline=(212, 175, 55), width=3) draw.rectangle([border + 8, border + 8, w - border - 8, h - border - 8], outline=(212, 175, 55, 80), width=1) # Bismillah Arabic text at top try: font_ar = ImageFont.truetype("/usr/share/fonts/truetype/dejavu/DejaVuSans.ttf", 48) font_title = ImageFont.truetype("/usr/share/fonts/truetype/dejavu/DejaVuSans-Bold.ttf", 64 if aspect == "9:16" else 54) font_sub = ImageFont.truetype("/usr/share/fonts/truetype/dejavu/DejaVuSans.ttf", 32) except: font_ar = ImageFont.load_default() font_title = font_ar font_sub = font_ar # Bismillah bism = "Abubakar Daily Islamic Shorts" bbox = draw.textbbox((0, 0), bism, font=font_sub) bw = bbox[2] - bbox[0] draw.text(((w - bw) // 2, border + 30), bism, font=font_sub, fill=(212, 175, 55)) # Gold divider line draw.line([(w // 4, border + 80), (3 * w // 4, border + 80)], fill=(212, 175, 55), width=2) # Main title โ€” wrapped max_chars = 20 if aspect == "9:16" else 35 lines = textwrap.wrap(title.upper(), width=max_chars) total_h = len(lines) * 80 y_start = (h - total_h) // 2 - 40 for i, line in enumerate(lines): bbox = draw.textbbox((0, 0), line, font=font_title) lw = bbox[2] - bbox[0] x = (w - lw) // 2 y = y_start + i * 80 # Shadow draw.text((x + 3, y + 3), line, font=font_title, fill=(0, 0, 0)) # Gold text draw.text((x, y), line, font=font_title, fill=(212, 175, 55)) # Bottom gold bar draw.rectangle([0, h - 80, w, h], fill=(212, 175, 55, 40)) channel = "๐Ÿ•Œ Islamic Shorts" bbox = draw.textbbox((0, 0), channel, font=font_sub) cw = bbox[2] - bbox[0] draw.text(((w - cw) // 2, h - 58), channel, font=font_sub, fill=(255, 255, 255)) img.save(out_path, "JPEG", quality=95) return os.path.exists(out_path) except Exception as e: print(f"Thumbnail error: {e}") return False # โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ• # SOUND LIBRARY โ€” Nasheeds + Quran (free, archive.org) # โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ• SOUND_LIBRARY = { "๐Ÿ•Œ Quran โ€” Al-Fatihah (Mishary)": "https://archive.org/download/mishary-al-afasy-quran/001.mp3", "๐Ÿ•Œ Quran โ€” Al-Baqarah opener (Mishary)": "https://archive.org/download/mishary-al-afasy-quran/002.mp3", "๐ŸŽต Nasheed โ€” Tala Al Badru (Instrumental)": "https://archive.org/download/NasheedCollection_201904/Tala_Al_Badru.mp3", "๐ŸŽต Nasheed โ€” Ya Taiba (Instrumental)": "https://archive.org/download/NasheedCollection_201904/Ya_Taiba.mp3", "๐ŸŽต Nasheed โ€” Allahu Allah (Soft)": "https://archive.org/download/NasheedCollection_201904/Allahu_Allah.mp3", "๐ŸŽต Islamic Instrumental โ€” Oud Calm": "https://archive.org/download/free-islamic-music/oud_calm_islamic.mp3", "๐ŸŽต Islamic Instrumental โ€” Flute Peaceful": "https://archive.org/download/free-islamic-music/flute_peaceful.mp3", "๐Ÿ”‡ Babu Music (murya kawai)": "", } # Fallback URLs idan ษ—aya ya kasa FALLBACK_URLS = [ "https://archive.org/download/Mishari_Rashid_Al-Afasy_Quran/001.mp3", "https://download.quranicaudio.com/quran/mishaari_raashid_al_3afaasee/001.mp3", "https://archive.org/download/quran-recitation-mishary/001.mp3", ] def download_sound(url: str, out_path: str) -> str: """Download sound from URL. Returns path if success, empty string if fail.""" if not url: return "" if os.path.exists(out_path) and os.path.getsize(out_path) > 10000: return out_path try: r = requests.get(url, timeout=30, stream=True) if r.status_code == 200: with open(out_path, "wb") as f: for chunk in r.iter_content(8192): if chunk: f.write(chunk) if os.path.exists(out_path) and os.path.getsize(out_path) > 10000: return out_path except Exception as e: print(f"Sound download failed ({url}): {e}") # Try fallbacks for fb_url in FALLBACK_URLS: try: r = requests.get(fb_url, timeout=20, stream=True) if r.status_code == 200: with open(out_path, "wb") as f: for chunk in r.iter_content(8192): if chunk: f.write(chunk) if os.path.exists(out_path) and os.path.getsize(out_path) > 10000: return out_path except: pass return "" def download_nasheed(out_path: str = "/tmp/nasheed.mp3") -> str: """Legacy wrapper โ€” download default Quran recitation.""" return download_sound(list(SOUND_LIBRARY.values())[0], out_path) def mix_background_nasheed(voice_path: str, nasheed_path: str, out_path: str = "/tmp/mixed_nasheed.mp3") -> str: """Mix voice with nasheed at low volume, loop nasheed to match voice length.""" try: dur = get_tts_duration(voice_path) if not nasheed_path or not os.path.exists(nasheed_path): # Fallback: soft sine ambient return mix_background_music(voice_path, out_path) proc = subprocess.run([ "ffmpeg", "-y", "-i", voice_path, "-stream_loop", "-1", "-i", nasheed_path, "-filter_complex", "[0:a]volume=1.0[voice];[1:a]volume=0.12,atrim=0:{dur}[bg];[voice][bg]amix=inputs=2:duration=first:dropout_transition=2[aout]".format(dur=f"{dur:.3f}"), "-map", "[aout]", "-ar", "44100", "-t", f"{dur:.3f}", out_path ], capture_output=True, text=True) if proc.returncode == 0 and os.path.exists(out_path) and os.path.getsize(out_path) > 1000: return out_path else: print("Nasheed mix stderr:", proc.stderr[-500:]) return mix_background_music(voice_path, out_path) except Exception as e: print(f"Nasheed mix error: {e}") return voice_path # โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ• # FEATURE: Video Preview Card # โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ• def render_preview_card(script: str, metadata: dict, aspect: str): """Show script + metadata preview before generating video.""" title = metadata.get("title", "") if metadata else "" desc = metadata.get("description", "") if metadata else "" tags = metadata.get("hashtags", []) if metadata else [] tags_str = " ".join(tags) if isinstance(tags, list) else str(tags) aspect_icon = "๐Ÿ“ฑ" if aspect == "9:16" else "๐Ÿ–ฅ๏ธ" st.markdown(f"""
{aspect_icon} VIDEO PREVIEW โ€” {aspect}
{html.escape(title)}
{html.escape(script[:200])}{'...' if len(script) > 200 else ''}
{html.escape(tags_str)}
""", unsafe_allow_html=True) # โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ• # FEATURE: History / Archive # โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ• HISTORY_FILE = "/tmp/video_history.json" def load_history() -> list: try: if os.path.exists(HISTORY_FILE): with open(HISTORY_FILE, "r", encoding="utf-8") as f: return json.load(f) except: pass return [] def save_to_history(topic: str, lang: str, metadata: dict, video_path: str, thumb_path: str): history = load_history() entry = { "id": int(time.time()), "date": time.strftime("%Y-%m-%d %H:%M"), "topic": topic, "lang": lang, "title": metadata.get("title", topic) if metadata else topic, "video_path": video_path, "thumb_path": thumb_path, } history.insert(0, entry) history = history[:20] # keep last 20 only try: with open(HISTORY_FILE, "w", encoding="utf-8") as f: json.dump(history, f, ensure_ascii=False, indent=2) except Exception as e: print(f"History save error: {e}") # โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ• # FEATURE: Multi-Platform Export # โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ• EXPORT_FORMATS = { "YouTube Shorts / TikTok / Reels (9:16 720x1280)": ("720", "1280", "9x16"), "Instagram Square (1:1 1080x1080)": ("1080", "1080", "1x1"), "Facebook / YouTube (16:9 1280x720)": ("1280", "720", "16x9"), "Instagram Story (9:16 1080x1920)": ("1080", "1920", "9x16_hd"), } def export_video_format(src_path: str, width: str, height: str, suffix: str) -> str: out = f"/tmp/export_{suffix}.mp4" subprocess.run([ "ffmpeg", "-y", "-i", src_path, "-vf", f"scale={width}:{height}:force_original_aspect_ratio=decrease," f"pad={width}:{height}:(ow-iw)/2:(oh-ih)/2:black", "-c:v", "libx264", "-preset", "ultrafast", "-c:a", "copy", out ], capture_output=True, text=True) return out if os.path.exists(out) else "" async def tts(text, voice, path): clean = re.sub(r'[#*()<>]', '', text) # Slower rate + slightly lower pitch = more imam-like delivery comm = edge_tts.Communicate(clean, voice, rate="-10%", pitch="-5Hz") await comm.save(path) _tts = tts def mix_background_music(voice_path: str, out_path: str, music_volume: float = 0.18) -> str: try: dur = get_tts_duration(voice_path) bg_audio = "/tmp/bg_ambient.mp3" # Soft 432Hz ambient tone with echo โ€” calming Islamic atmosphere subprocess.run([ "ffmpeg", "-y", "-f", "lavfi", "-i", f"sine=frequency=432:duration={dur:.3f}", "-af", "volume=0.06,aecho=0.8:0.88:60:0.4", bg_audio ], capture_output=True, text=True) if not os.path.exists(bg_audio): return voice_path mixed = "/tmp/mixed_audio.mp3" subprocess.run([ "ffmpeg", "-y", "-i", voice_path, "-i", bg_audio, "-filter_complex", f"[1:a]volume={music_volume}[bg];[0:a][bg]amix=inputs=2:duration=first[aout]", "-map", "[aout]", "-t", f"{dur:.3f}", mixed ], capture_output=True, text=True) return mixed if os.path.exists(mixed) else voice_path except Exception as e: print(f"Music mix error: {e}") return voice_path 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 apply_zoom_to_clip(src: str, dst: str, tw: int, th: int, zoom_dir: str, duration: float) -> bool: """ Reliable Ken Burns effect using scale expression. zoom_dir: 'in' = slowly zoom in, 'out' = slowly zoom out """ fps = 25 n_frames = int(duration * fps) # Scale slightly larger than target so zoom has room sw = int(tw * 1.3) sh = int(th * 1.3) cx = sw // 2 cy = sh // 2 if zoom_dir == "in": # Pan from slightly zoomed-out to center โ€” zooming in vf = ( f"scale={sw}:{sh}:force_original_aspect_ratio=increase," f"crop={sw}:{sh}," f"crop=w='if(lte(t,0),{tw},{tw}+({sw}-{tw})*max(0,1-t/{duration:.3f}))'" f":h='if(lte(t,0),{th},{th}+({sh}-{th})*max(0,1-t/{duration:.3f}))'" f":x='({sw}-out_w)/2':y='({sh}-out_h)/2'," f"scale={tw}:{th}" ) else: # Zoom out: start tight, slowly reveal more vf = ( f"scale={sw}:{sh}:force_original_aspect_ratio=increase," f"crop={sw}:{sh}," f"crop=w='if(lte(t,0),{tw},{tw}+({sw}-{tw})*min(1,t/{duration:.3f}))'" f":h='if(lte(t,0),{th},{th}+({sh}-{th})*min(1,t/{duration:.3f}))'" f":x='({sw}-out_w)/2':y='({sh}-out_h)/2'," f"scale={tw}:{th}" ) cmd = [ "ffmpeg", "-y", "-i", src, "-vf", vf, "-t", f"{duration:.3f}", "-r", str(fps), "-c:v", "libx264", "-preset", "ultrafast", "-an", dst ] proc = subprocess.run(cmd, capture_output=True, text=True) if not (os.path.exists(dst) and os.path.getsize(dst) > 0): # Fallback: simple scale crop no zoom fb_cmd = [ "ffmpeg", "-y", "-i", src, "-vf", f"scale={tw}:{th}:force_original_aspect_ratio=increase,crop={tw}:{th}", "-t", f"{duration:.3f}", "-r", str(fps), "-c:v", "libx264", "-preset", "ultrafast", "-an", dst ] subprocess.run(fb_cmd, capture_output=True, text=True) return os.path.exists(dst) and os.path.getsize(dst) > 0 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 n_clips = len(clip_paths) # Each clip gets equal share of audio duration dur_per_clip = audio_dur / n_clips # โ”€โ”€ Step 1: Apply zoom effect to each clip individually, trim to equal duration zoom_dirs = ["in", "out", "in", "out"] # alternate zoom direction processed = [] for i, src in enumerate(clip_paths): dst = f"/tmp/zoom_{i}.mp4" zdir = zoom_dirs[i % len(zoom_dirs)] ok = apply_zoom_to_clip(src, dst, tw, th, zdir, dur_per_clip) if ok: processed.append(dst) else: # fallback: simple scale+crop without zoom fb = f"/tmp/fb_{i}.mp4" subprocess.run([ "ffmpeg", "-y", "-i", src, "-vf", f"scale={tw}:{th}:force_original_aspect_ratio=increase,crop={tw}:{th}", "-t", f"{dur_per_clip:.3f}", "-preset", "ultrafast", "-an", fb ], capture_output=True, text=True) if os.path.exists(fb): processed.append(fb) if not processed: return False # โ”€โ”€ Step 2: Concatenate all processed clips into one background list_file = "/tmp/concat_list.txt" with open(list_file, "w") as lf: for p in processed: 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) if not os.path.exists(joined): return False # โ”€โ”€ Step 3: Trim/extend to exactly audio_dur looped = "/tmp/looped_bg.mp4" subprocess.run([ "ffmpeg", "-y", "-stream_loop", "-1", "-i", joined, "-t", f"{audio_dur:.3f}", "-c", "copy", looped ], capture_output=True, text=True) bg = looped if os.path.exists(looped) else joined # โ”€โ”€ Step 4: 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 5: 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 6: Build final ffmpeg command with captions + watermark + logo if os.path.exists(LOGO_FILE): # Circle logo: scale โ†’ geq mask โ†’ alphaextract โ†’ overlay filter_complex = ( f"[2:v]scale=110:110[logo_sq];" # Create circle mask via geq f"[logo_sq]format=rgba," f"geq=r='r(X,Y)':g='g(X,Y)':b='b(X,Y)'" f":a='if(lte(pow(X-55,2)+pow(Y-55,2),pow(54,2)),255,0)'[logo_circ];" f"[0:v][logo_circ]overlay=W-w-16:16[tmp];" f"[tmp]{combined_text}[vout]" ) ff_cmd = [ "ffmpeg", "-y", "-i", bg, "-i", voice_path, "-i", LOGO_FILE, "-filter_complex", filter_complex, "-map", "[vout]", "-map", "1:a", "-c:v", "libx264", "-preset", "ultrafast", "-t", f"{audio_dur:.3f}", out_path ] else: filter_complex = f"[0:v]{combined_text}[vout]" ff_cmd = [ "ffmpeg", "-y", "-i", bg, "-i", voice_path, "-filter_complex", filter_complex, "-map", "[vout]", "-map", "1:a", "-c:v", "libx264", "-preset", "ultrafast", "-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 # โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ• # FEATURE: YouTube Upload (via YouTube Data API v3) # โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ• YOUTUBE_API_KEY = os.getenv("YOUTUBE_API_KEY", "").strip() def upload_to_youtube(video_path: str, title: str, description: str, tags: list) -> dict: """ Upload video to YouTube using resumable upload. Requires YOUTUBE_API_KEY (OAuth token) in secrets. Returns {"success": True/False, "url": "...", "error": "..."} """ try: if not YOUTUBE_API_KEY: return {"success": False, "error": "YOUTUBE_API_KEY ba a saka ba a Secrets"} # Step 1: Initialize resumable upload metadata = { "snippet": { "title": title[:100], "description": description, "tags": tags[:10] if tags else [], "categoryId": "22" # People & Blogs }, "status": { "privacyStatus": "public", "selfDeclaredMadeForKids": False } } init_resp = requests.post( "https://www.googleapis.com/upload/youtube/v3/videos" "?uploadType=resumable&part=snippet,status", headers={ "Authorization": f"Bearer {YOUTUBE_API_KEY}", "Content-Type": "application/json", "X-Upload-Content-Type": "video/mp4", }, json=metadata, timeout=30 ) if init_resp.status_code not in (200, 201): return {"success": False, "error": f"Init failed: {init_resp.text[:200]}"} upload_url = init_resp.headers.get("Location", "") if not upload_url: return {"success": False, "error": "No upload URL returned"} # Step 2: Upload video bytes file_size = os.path.getsize(video_path) with open(video_path, "rb") as vf: upload_resp = requests.put( upload_url, data=vf, headers={ "Content-Type": "video/mp4", "Content-Length": str(file_size) }, timeout=300 ) if upload_resp.status_code in (200, 201): vid_id = upload_resp.json().get("id", "") return {"success": True, "url": f"https://youtube.com/shorts/{vid_id}", "id": vid_id} else: return {"success": False, "error": f"Upload failed: {upload_resp.text[:200]}"} except Exception as e: return {"success": False, "error": str(e)} # โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ• # FEATURE: Batch Video Generation # โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ• def generate_batch_video(topic: str, lang: str, voice: str, aspect: str, idx: int) -> dict: """Generate one complete video โ€” used in batch mode.""" result = {"topic": topic, "status": "โŒ Failed", "path": "", "metadata": {}} try: # Script script = call_freemodel(topic, lang) if script.startswith("Error"): result["status"] = f"โŒ Script error" return result # Metadata metadata = call_freemodel_metadata(topic, lang, script) # TTS vpath = f"/tmp/batch_voice_{idx}.mp3" asyncio.run(_tts(script, voice, vpath)) # Nasheed mix nasheed = download_nasheed(f"/tmp/nasheed_{idx}.mp3") if nasheed: vpath = mix_background_nasheed(vpath, nasheed, f"/tmp/batch_mixed_{idx}.mp3") # Pexels clips headers = {"Authorization": PEXELS_KEY} queries = ["islamic architecture golden hour", "desert sunset sand dunes", "ocean waves peaceful nature", "green forest light rays"] clip_paths = [] for q in 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=2&orientation=portrait", headers=headers, timeout=15 ) for v in resp.json().get("videos", []): for f in v.get("video_files", []): if f.get("file_type", "").lower() == "video/mp4" and f.get("width", 0) >= 720: rp = f"/tmp/batch_clip_{idx}_{len(clip_paths)}.mp4" dl = requests.get(f["link"], stream=True, timeout=60) with open(rp, "wb") as fh: for chunk in dl.iter_content(8192): if chunk: fh.write(chunk) clip_paths.append(rp) break if len(clip_paths) >= 4: break except: pass if not clip_paths: result["status"] = "โŒ No clips" return result # Assemble video out = f"/tmp/batch_video_{idx}.mp4" ok = create_video_centered(clip_paths, vpath, out, script, aspect) if ok: result["status"] = "โœ… Done" result["path"] = out result["metadata"] = metadata result["script"] = script save_to_history(topic, lang, metadata, out, "") else: result["status"] = "โŒ Assembly failed" except Exception as e: result["status"] = f"โŒ {str(e)[:60]}" return result # โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ• # FEATURE: Analytics Dashboard # โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ• def compute_analytics(history: list) -> dict: if not history: return {} total = len(history) lang_count = {} topic_words = {} dates = [] for e in history: lg = e.get("lang", "Unknown") lang_count[lg] = lang_count.get(lg, 0) + 1 for w in e.get("topic", "").split(): topic_words[w] = topic_words.get(w, 0) + 1 dates.append(e.get("date", "")[:10]) top_lang = max(lang_count, key=lang_count.get) if lang_count else "-" top_topics = sorted(topic_words.items(), key=lambda x: x[1], reverse=True)[:5] unique_days = len(set(dates)) return { "total": total, "lang_count": lang_count, "top_lang": top_lang, "top_topics": top_topics, "unique_days": unique_days, "avg_per_day": round(total / max(unique_days, 1), 1) } # TABS tab1, tab2, tab3 = st.tabs(["๐ŸŽฌ Generate Video", "๐Ÿ“ฆ Batch Mode", "๐Ÿ“ˆ Analytics"]) with tab1: # INPUT PANEL st.markdown('
', unsafe_allow_html=True) st.markdown('
๐ŸŒ™ Zaษ“uษ“ษ“uka โ€” Settings
', 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") # AI Topic Suggestions col_sug1, col_sug2 = st.columns([3, 1]) with col_sug2: if st.button("๐Ÿ’ก Get Topics", use_container_width=True): with st.spinner("AI na ba da shawarwari..."): st.session_state.topic_suggestions = get_topic_suggestions(lang.split()[0]) if st.session_state.topic_suggestions: st.markdown("
", unsafe_allow_html=True) st.markdown("๐Ÿ‘† Danna topic ษ—aya don zaษ“ar shi:", unsafe_allow_html=True) cols = st.columns(2) for i, sug in enumerate(st.session_state.topic_suggestions): with cols[i % 2]: if st.button(f"๐Ÿ“Œ {sug}", key=f"sug_{i}", use_container_width=True): st.session_state.selected_topic = sug st.rerun() st.markdown("
", unsafe_allow_html=True) if "selected_topic" in st.session_state and st.session_state.selected_topic: st.info(f"โœ… Topic da aka zaษ“a: **{st.session_state.selected_topic}**") st.markdown("
", 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] video_duration = st.slider( "โฑ๏ธ Video Duration (seconds)", min_value=20, max_value=90, value=35, step=5, key="video_duration_slider" ) st.markdown(f"Script zai kasance kusan {video_duration} seconds", unsafe_allow_html=True) # Sound Library selector st.markdown("
", unsafe_allow_html=True) selected_sound_name = st.selectbox( "๐ŸŽต Background Sound โ€” Zaษ“i Music", options=list(SOUND_LIBRARY.keys()), key="sound_library_select" ) selected_sound_url = SOUND_LIBRARY[selected_sound_name] st.markdown('
', unsafe_allow_html=True) # GENERATE BUTTON st.markdown('
', unsafe_allow_html=True) st.markdown('
โš™๏ธ ฦ˜irฦ™irar Bidiyo
', unsafe_allow_html=True) if st.button("๐Ÿš€ GENERATE ISLAMIC VIDEO", use_container_width=True): # Use selected topic from suggestions if available final_topic = st.session_state.get("selected_topic", "") or topic.strip() if not final_topic: st.warning("โš ๏ธ Rubuta maudu'i da farko ko zaษ“i daga suggestions.") else: topic = final_topic st.session_state.selected_topic = "" # reset after use if final_topic: if True: if manual_script and manual_script.strip(): script = manual_script.strip() else: with st.spinner("โœฆ Ana rubuta rubutu..."): script = call_freemodel(topic, lang.split()[0], duration_sec=video_duration) st.session_state.script_text = script st.markdown("**๐Ÿ“œ Rubutun da aka ฦ™irฦ™ira / Used Script:**") st.markdown(f'
{html.escape(script)}
', unsafe_allow_html=True) with st.spinner("โœฆ Ana ฦ™irฦ™irar metadata..."): metadata = call_freemodel_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)) # Download selected background sound and mix if selected_sound_url: with st.spinner(f"โœฆ Ana sauke {selected_sound_name}..."): sound_file = download_sound(selected_sound_url, "/tmp/selected_sound.mp3") if sound_file: voice_path = mix_background_nasheed(voice_path, sound_file, "/tmp/mixed_nasheed.mp3") st.success(f"โœ… {selected_sound_name} an haษ—a da murya!") else: st.warning("โš ๏ธ Ba a sami sound ba โ€” murya kawai.") st.session_state.nasheed_path = sound_file else: st.info("๐Ÿ”‡ Murya kawai โ€” babu background music.") st.session_state.nasheed_path = "" # โ”€โ”€ VIDEO PREVIEW CARD st.markdown('
๐Ÿ‘๏ธ Preview โ€” Tabbatar kafin haษ—a bidiyo
', unsafe_allow_html=True) render_preview_card(script, st.session_state.metadata, selected_aspect) with st.spinner("โœฆ Ana neman bidiyon bango masu dacewa..."): headers = {"Authorization": PEXELS_KEY} audio_dur_tmp = get_tts_duration(voice_path) n_clips_needed = max(4, int(audio_dur_tmp / 8)) # Use topic hash for random page โ†’ different clips every video even same topic import hashlib topic_seed = int(hashlib.md5((topic + str(time.time())).encode()).hexdigest()[:4], 16) % 8 + 1 all_queries = [ "islamic architecture golden hour", "mosque minaret sky", "desert sunset sand dunes", "ocean waves peaceful nature", "green forest light rays sunbeam", "mountain landscape sunrise fog", "night sky stars milky way", "waterfall nature peaceful", "flowers bloom nature spring", "river stream forest calm", "aerial city lights night", "clouds sky time lapse", ] import random random.seed(topic_seed) random.shuffle(all_queries) preferred_queries = all_queries[:n_clips_needed] clip_paths = [] for q in preferred_queries: if len(clip_paths) >= n_clips_needed: break try: page = random.randint(1, 4) resp = requests.get( f"https://api.pexels.com/videos/search?query={requests.utils.quote(q)}&per_page=5&page={page}&orientation=portrait", headers=headers, timeout=20 ) r = resp.json() videos = r.get('videos', []) random.shuffle(videos) for v in 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.") # Save to history save_to_history( topic, lang.split()[0], st.session_state.metadata, out_path, "/tmp/thumbnail.jpg" ) st.session_state.video_history = load_history() # Generate thumbnail with st.spinner("โœฆ Ana ฦ™irฦ™irar thumbnail..."): thumb_title = st.session_state.metadata.get("title", topic) if st.session_state.metadata else topic thumb_ok = generate_thumbnail(thumb_title, selected_aspect, "/tmp/thumbnail.jpg") if thumb_ok: st.session_state.thumbnail_path = "/tmp/thumbnail.jpg" 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"""
""" st.markdown('
', unsafe_allow_html=True) st.markdown('', 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"Server copy: {st.session_state.last_metadata_file}", unsafe_allow_html=True) except Exception as e: st.write("Failed to create metadata download:", e) st.markdown('
', unsafe_allow_html=True) else: st.error("โŒ Kuskure wajen haษ—a bidiyo. (Sake gwadawa)") st.markdown('
', 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('
', unsafe_allow_html=True) st.markdown('
๐ŸŽฌ Bidiyon da aka ฦ˜irฦ™ira
', unsafe_allow_html=True) st.video(st.session_state.final_video_path) st.markdown("
", 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('
', unsafe_allow_html=True) # THUMBNAIL OUTPUT PANEL if st.session_state.thumbnail_path and os.path.exists(st.session_state.thumbnail_path): st.markdown('
', unsafe_allow_html=True) st.markdown('
๐Ÿ–ผ๏ธ YouTube Thumbnail
', unsafe_allow_html=True) st.image(st.session_state.thumbnail_path, use_column_width=True) st.markdown("
", unsafe_allow_html=True) with open(st.session_state.thumbnail_path, "rb") as fh: st.download_button( label="๐Ÿ“ฅ SAUKE THUMBNAIL โ€” Download Thumbnail", data=fh, file_name="Abubakar_Islamic_Thumbnail.jpg", mime="image/jpeg", use_container_width=True ) st.markdown('
', unsafe_allow_html=True) # MULTI-PLATFORM EXPORT PANEL if st.session_state.final_video_path and os.path.exists(st.session_state.final_video_path): st.markdown('
', unsafe_allow_html=True) st.markdown('
๐Ÿ“ฑ Multi-Platform Export
', unsafe_allow_html=True) st.markdown("Zaษ“i platform โ†’ app zai canza girman bidiyo kai tsaye", unsafe_allow_html=True) st.markdown("
", unsafe_allow_html=True) for fmt_name, (fw, fh, fsuffix) in EXPORT_FORMATS.items(): col_a, col_b = st.columns([3, 1]) with col_a: st.markdown(f"๐Ÿ“ {fmt_name}", unsafe_allow_html=True) with col_b: btn_key = f"export_{fsuffix}" if st.button("Export", key=btn_key, use_container_width=True): with st.spinner(f"Ana export..."): ep = export_video_format(st.session_state.final_video_path, fw, fh, fsuffix) if ep: st.session_state.export_paths[fsuffix] = ep # Show download if exported if fsuffix in st.session_state.export_paths: ep = st.session_state.export_paths[fsuffix] if os.path.exists(ep): with open(ep, "rb") as ef: st.download_button( label=f"๐Ÿ“ฅ Download {fsuffix.upper()}", data=ef, file_name=f"Islamic_Short_{fsuffix}.mp4", mime="video/mp4", key=f"dl_{fsuffix}", use_container_width=True ) st.markdown("
", unsafe_allow_html=True) st.markdown('
', unsafe_allow_html=True) # HISTORY PANEL if st.session_state.video_history: st.markdown('
', unsafe_allow_html=True) st.markdown('
๐Ÿ’พ Tarihin Videos โ€” History
', unsafe_allow_html=True) for entry in st.session_state.video_history[:5]: col1, col2 = st.columns([4, 1]) with col1: st.markdown(f"""
{html.escape(entry.get('title',''))}
{entry.get('date','')} ยท {entry.get('lang','')} ยท {entry.get('topic','')}
""", unsafe_allow_html=True) with col2: vp = entry.get("video_path", "") if vp and os.path.exists(vp): with open(vp, "rb") as hf: st.download_button( "๐Ÿ“ฅ", data=hf, file_name=f"Islamic_{entry['id']}.mp4", mime="video/mp4", key=f"hist_{entry['id']}", use_container_width=True ) st.markdown('
', unsafe_allow_html=True) # โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ• # YOUTUBE UPLOAD PANEL (inside tab1 video output) # โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ• if st.session_state.final_video_path and os.path.exists(st.session_state.final_video_path): if YOUTUBE_API_KEY: st.markdown('
', unsafe_allow_html=True) st.markdown('
๐Ÿ“ค Loda YouTube kai tsaye
', unsafe_allow_html=True) md_yt = st.session_state.metadata or {} yt_title = md_yt.get("title", "Islamic Short") yt_desc = md_yt.get("description", "") yt_tags = md_yt.get("hashtags", []) col_yt1, col_yt2 = st.columns([3, 1]) with col_yt1: st.markdown(f"๐Ÿ“น {html.escape(yt_title)}", unsafe_allow_html=True) with col_yt2: if st.button("๐Ÿš€ Upload", key="yt_upload", use_container_width=True): with st.spinner("Ana loda YouTube..."): yt_result = upload_to_youtube( st.session_state.final_video_path, yt_title, yt_desc, [t.replace("#","") for t in yt_tags] ) if yt_result["success"]: st.session_state.yt_status = f"โœ… An loda! {yt_result.get('url','')}" else: st.session_state.yt_status = f"โŒ {yt_result.get('error','')}" if st.session_state.yt_status: st.info(st.session_state.yt_status) st.markdown('
', unsafe_allow_html=True) # โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ• # BATCH MODE TAB # โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ• with tab2: st.markdown('
', unsafe_allow_html=True) st.markdown('
๐Ÿ“ฆ Batch Mode โ€” ฦ˜irฦ™irar Videos Da Yawa
', unsafe_allow_html=True) batch_lang = st.selectbox("Harshe", ["Hausa", "Arabic", "English"], key="batch_lang") batch_voice_map = {"Hausa": "ha-NG-AbdullahNeural", "Arabic": "ar-SA-HamedNeural", "English": "en-US-AndrewNeural"} batch_aspect = st.selectbox("Aspect Ratio", ["9:16", "16:9"], key="batch_aspect") st.markdown("Rubuta topics ษ—aya a kowace layi (max 10)", unsafe_allow_html=True) batch_topics_raw = st.text_area( "Topics (ษ—aya a kowace layi)", placeholder="Tuba, Tawakkul, Fadar Annabi, Sabr da lada, Kaunar Allah", height=180, key="batch_topics" ) col_b1, col_b2 = st.columns(2) with col_b1: if st.button("๐Ÿ’ก Auto-fill Topics (AI)", use_container_width=True, key="batch_autofill"): with st.spinner("AI na ba da topics..."): sugs = get_topic_suggestions(batch_lang) if sugs: st.session_state["batch_autofilled"] = "\n".join(sugs[:8]) st.rerun() with col_b2: run_batch = st.button("๐Ÿš€ START BATCH", use_container_width=True, key="run_batch") if "batch_autofilled" in st.session_state: st.info("Topics sun shiga โ€” kopiya su saka a box sama.") st.code(st.session_state["batch_autofilled"]) if run_batch: topics_list = [t.strip() for t in batch_topics_raw.strip().splitlines() if t.strip()][:10] if not topics_list: st.warning("โš ๏ธ Rubuta topics da farko!") else: st.session_state.batch_results = [] progress = st.progress(0) status_box = st.empty() for i, tp in enumerate(topics_list): status_box.info(f"๐ŸŽฌ Ana ฦ™irฦ™irar {i+1}/{len(topics_list)}: **{tp}**") res = generate_batch_video(tp, batch_lang, batch_voice_map[batch_lang], batch_aspect, i) st.session_state.batch_results.append(res) progress.progress((i + 1) / len(topics_list)) status_box.success(f"โœ… An gama! Videos {len(topics_list)} sun shirya.") # Show batch results if st.session_state.batch_results: st.markdown('
๐Ÿ“‹ Sakamakon Batch
', unsafe_allow_html=True) for res in st.session_state.batch_results: col_r1, col_r2 = st.columns([4, 1]) with col_r1: st.markdown(f"""
{res['status']} {html.escape(res['topic'])}
{html.escape(res.get('metadata',{}).get('title',''))}
""", unsafe_allow_html=True) with col_r2: vp = res.get("path", "") if vp and os.path.exists(vp): with open(vp, "rb") as bf: st.download_button( "๐Ÿ“ฅ", data=bf, file_name=f"batch_{res['topic'][:20]}.mp4", mime="video/mp4", key=f"batch_dl_{res['topic'][:10]}_{id(res)}", use_container_width=True ) st.markdown('
', unsafe_allow_html=True) # โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ• # ANALYTICS TAB # โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ• with tab3: st.markdown('
', unsafe_allow_html=True) st.markdown('
๐Ÿ“ˆ Analytics Dashboard
', unsafe_allow_html=True) history_data = st.session_state.video_history analytics = compute_analytics(history_data) if not analytics: st.markdown("
Babu bayanan tarihi tukuna.
ฦ˜irฦ™iri videos da yawa sannan analytics zai bayyana.
", unsafe_allow_html=True) else: # KPI Cards col_k1, col_k2, col_k3 = st.columns(3) def kpi_card(label, value, icon): return f"""
{icon}
{value}
{label}
""" with col_k1: st.markdown(kpi_card("Jimillar Videos", analytics["total"], "๐ŸŽฌ"), unsafe_allow_html=True) with col_k2: st.markdown(kpi_card("Yawan Harshe", analytics["top_lang"], "๐ŸŒ"), unsafe_allow_html=True) with col_k3: st.markdown(kpi_card("Avg / Rana", analytics["avg_per_day"], "๐Ÿ“…"), unsafe_allow_html=True) st.markdown("
", unsafe_allow_html=True) # Language breakdown st.markdown("
๐ŸŒ Videos ta Harshe
", unsafe_allow_html=True) for lg, cnt in analytics["lang_count"].items(): pct = int(cnt / analytics["total"] * 100) st.markdown(f"""
{lg} {cnt} ({pct}%)
""", unsafe_allow_html=True) st.markdown("
", unsafe_allow_html=True) # Top topics if analytics["top_topics"]: st.markdown("
๐Ÿ”ฅ Topics da suka fi
", unsafe_allow_html=True) for word, cnt in analytics["top_topics"]: st.markdown(f"๐Ÿ“Œ {html.escape(word)} ร—{cnt}", unsafe_allow_html=True) st.markdown("
", unsafe_allow_html=True) # Recent history table st.markdown("
๐Ÿ• Videos na ฦ˜arshe
", unsafe_allow_html=True) for e in history_data[:8]: st.markdown(f"""
{html.escape(e.get('title','')[:40])} {e.get('date','')}
""", unsafe_allow_html=True) st.markdown('
', unsafe_allow_html=True) # FOOTER st.markdown(""" """, unsafe_allow_html=True)