File size: 9,783 Bytes
e2b6d23 c43864f |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 |
import os
import re
import json
import io
import datetime as dt
import pandas as pd
import streamlit as st
from dotenv import load_dotenv
import google.generativeai as genai
# --- Setup ---
load_dotenv()
DEFAULT_MODEL = "gemini-1.5-flash"
def configure_gemini(api_key: str):
"""Initializes the Gemini client with the provided API key."""
genai.configure(api_key=api_key)
# --- Utilities ---
def extract_json(text: str) -> dict:
"""
Pulls a JSON object from a string, even if it's wrapped in markdown code fences.
Returns a Python dictionary or raises an error if parsing fails.
"""
if not text:
raise ValueError("Received an empty response from the model.")
# Look for JSON within ```json ... ``` markdown block
match = re.search(r"```json\s*(.*?)\s*```", text, re.DOTALL)
if match:
json_str = match.group(1)
else:
# Fallback: find the first '{' and last '}'
start = text.find('{')
end = text.rfind('}')
if start == -1 or end == -1:
raise json.JSONDecodeError("No JSON object found in the response text.", text, 0)
json_str = text[start:end+1]
return json.loads(json_str)
def seconds_to_ts(s: int) -> str:
"""Converts an integer of seconds to a MM:SS timestamp string."""
m, sec = divmod(int(s), 60)
return f"{m:02d}:{sec:02d}"
def make_prompt(topic: str, idea_count: int, total_seconds: int, scene_count: int) -> str:
"""Creates the detailed, structured prompt for the generative model."""
return f"""
You are a YouTube Shorts producer for "Contentmaniacs" (nature, cosmos, paradoxes, AI).
Goal: Create viral, factual, poetic-science Shorts with clear visuals.
Generate EXACTLY {idea_count} ideas for topic: "{topic}".
Return ONLY a single, valid JSON object (no markdown). The root object must contain one key, "ideas", which is a list of idea objects.
The schema for each idea object in the list is:
{{
"title": "string (<= 60 chars, no quotes)",
"keywords": ["kw1","kw2","kw3"],
"description": "1–2 lines, SEO-rich, natural language",
"hashtags": ["Shorts","YouTubeShorts","Contentmaniacs","<up to 7 topical>"],
"thumbnail_prompt": "clear 9:16 visual brief (no text)",
"video_plan": {{
"duration_seconds": {total_seconds},
"scenes_count": {scene_count},
"scenes": [
{{
"scene_no": 1,
"start_sec": 0,
"end_sec": 0,
"voiceover": "1–2 punchy lines, simple language",
"on_screen_text": "few words, optional, no hashtags",
"visual_direction": "what to show (subject, motion, environment, mood, lighting)",
"shot_type": "macro | wide | medium | timelapse | drone | slow-mo | infographic",
"prompt": "text-to-video/image prompt for Canva/Runway (no text overlay)",
"broll_ideas": ["alt idea 1","alt idea 2"],
"sfx_music": "sound design notes (subtle, cinematic, ambient, etc.)"
}}
]
}},
"full_transcript": "Combine all voiceover lines into a clean 45–60s transcript."
}}
RULES:
- Factual, inspiring, no clickbait lies.
- Keep each scene's voiceover short (<= 18 words).
- Distribute time evenly across scenes so end_sec of last scene == duration_seconds.
- Output MUST be a single, valid JSON object only.
"""
def idea_json_to_overview_rows(topic: str, idea: dict) -> dict:
"""Creates a dictionary for the overview DataFrame from a single idea JSON."""
return {
"Topic": topic,
"Title": (idea.get("title") or "").strip(),
"Keywords": ", ".join(idea.get("keywords") or []),
"Description": (idea.get("description") or "").strip(),
"Hashtags": " ".join(("#" + h.lstrip("#")) for h in (idea.get("hashtags") or [])),
"ThumbnailPrompt": (idea.get("thumbnail_prompt") or "").strip(),
"DurationSec": idea.get("video_plan", {}).get("duration_seconds", "")
}
def idea_json_to_scenes_df(topic: str, idea: dict) -> pd.DataFrame:
"""Creates a DataFrame for the scene-by-scene shot list."""
scenes = idea.get("video_plan", {}).get("scenes", []) or []
rows = []
for sc in scenes:
rows.append({
"Topic": topic,
"Title": idea.get("title", ""),
"SceneNo": sc.get("scene_no", ""),
"Start": seconds_to_ts(sc.get("start_sec", 0)),
"End": seconds_to_ts(sc.get("end_sec", 0)),
"Voiceover": (sc.get("voiceover") or "").strip(),
"OnScreenText": (sc.get("on_screen_text") or "").strip(),
"VisualDirection": (sc.get("visual_direction") or "").strip(),
"ShotType": (sc.get("shot_type") or "").strip(),
"Prompt": (sc.get("prompt") or "").strip(),
"BrollIdeas": ", ".join(sc.get("broll_ideas") or []),
"SFX_Music": (sc.get("sfx_music") or "").strip()
})
return pd.DataFrame(rows)
def df_to_csv_bytes(df: pd.DataFrame) -> bytes:
"""Converts a DataFrame to UTF-8 encoded CSV bytes for downloading."""
return df.to_csv(index=False).encode("utf-8")
def transcript_bytes(title: str, transcript: str) -> bytes:
"""Creates bytes for a simple text file containing the title and transcript."""
content = f"TITLE\n{title}\n\nFULL TRANSCRIPT\n{transcript}\n"
return content.encode("utf-8")
# --- Streamlit UI ---
st.set_page_config(page_title="Contentmaniacs Producer", page_icon="🎬", layout="wide")
st.title("🎬 Contentmaniacs — Shorts Producer")
st.caption("Generate ideas → transcript → scene prompts with one click.")
# Load Gemini API key from Hugging Face Secrets or local .env file
try:
# This is for deployed apps on Hugging Face
api_key = st.secrets["GEMINI_API_KEY"]
except (KeyError, FileNotFoundError):
# This is for local development
api_key = os.getenv("GEMINI_API_KEY", "")
if not api_key:
st.error("⚠️ Gemini API key is missing! Please set it in your .env file locally, or in the Hugging Face Space secrets.")
st.stop()
# Inputs
c1, c2, c3, c4 = st.columns([2, 1, 1, 1])
with c1:
topic = st.text_input("Topic", placeholder="Cosmic paradoxes, Deep ocean mysteries, AI vs Humans…")
with c2:
idea_count = st.number_input("Ideas", min_value=1, max_value=5, value=1, step=1)
with c3:
total_seconds = st.number_input("Video length (sec)", min_value=30, max_value=90, value=60, step=5)
with c4:
scene_count = st.number_input("Scenes", min_value=3, max_value=10, value=6, step=1)
model_name = st.selectbox("Model", [DEFAULT_MODEL, "gemini-1.5-pro"], index=0)
go = st.button("✨ Generate")
if go:
if not api_key:
st.error("Please paste your Gemini API key.")
st.stop()
if not topic.strip():
st.error("Please enter a topic.")
st.stop()
try:
configure_gemini(api_key)
model = genai.GenerativeModel(model_name)
prompt = make_prompt(topic.strip(), int(idea_count), int(total_seconds), int(scene_count))
with st.spinner("Producing ideas, transcript and scenes…"):
resp = model.generate_content(prompt)
data = extract_json(resp.text)
ideas = data.get("ideas", [])
if not ideas:
st.warning("No ideas returned. Try again with a simpler topic or check the model's response format.")
st.stop()
# Display each idea in its own tab
tab_names = [f"Idea {i+1}" for i in range(len(ideas))]
tabs = st.tabs(tab_names)
ts = dt.datetime.now().strftime("%Y%m%d_%H%M%S")
for i, (tab, idea) in enumerate(zip(tabs, ideas), start=1):
with tab:
overview_row = idea_json_to_overview_rows(topic.strip(), idea)
scenes_df = idea_json_to_scenes_df(topic.strip(), idea)
transcript = idea.get("full_transcript", "").strip()
title = overview_row["Title"]
st.subheader("Overview")
st.dataframe(pd.DataFrame([overview_row]), use_container_width=True)
st.subheader("Scenes / Shot List")
st.dataframe(scenes_df, use_container_width=True)
# Download buttons
colA, colB, colC = st.columns(3)
file_prefix = f"idea{i}_{ts}"
with colA:
st.download_button(
"⬇️ Download Scenes CSV",
data=df_to_csv_bytes(scenes_df),
file_name=f"scenes_{file_prefix}.csv",
mime="text/csv"
)
with colB:
st.download_button(
"⬇️ Download Transcript TXT",
data=transcript_bytes(title, transcript),
file_name=f"transcript_{file_prefix}.txt",
mime="text/plain"
)
with colC:
st.download_button(
"⬇️ Download Overview CSV",
data=df_to_csv_bytes(pd.DataFrame([overview_row])),
file_name=f"overview_{file_prefix}.csv",
mime="text/csv"
)
with st.expander("👀 Quick copy: Transcript"):
st.code(transcript or "No transcript returned.", language="markdown")
with st.expander("🎯 Thumbnail Prompt"):
st.markdown(overview_row["ThumbnailPrompt"] or "_No prompt returned._")
except json.JSONDecodeError:
st.error("The model response wasn’t valid JSON. Click 'Generate' again or try a simpler topic.")
st.code(resp.text) # Show the faulty text
except Exception as e:
st.error(f"An unexpected error occurred: {e}") |