yonagush commited on
Commit
2223dfd
·
verified ·
1 Parent(s): ad2c1d5

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +92 -160
app.py CHANGED
@@ -1,149 +1,103 @@
1
- """
2
- AI Reels Maker — Fixed Version
3
- - Single Pexels background video (looped)
4
- - Outro video appended at the end
5
- - No caption background/shadow
6
- - Voice selection works
7
- """
8
-
9
  import sys
10
  sys.stdout.reconfigure(line_buffering=True)
11
 
12
- import asyncio
13
  import os
14
  import re
15
  import tempfile
16
- import urllib.request
17
- from pathlib import Path
18
-
19
- import gradio as gr
20
- import numpy as np
21
  import requests
22
- from PIL import Image, ImageDraw, ImageFont, ImageFilter
23
-
24
- print("✓ imports done", flush=True)
25
-
26
- # Constants
27
- VIDEO_W = 720
28
- VIDEO_H = 1280
29
- FPS = 24
30
 
31
- FONT_BOLD_URL = "https://github.com/google/fonts/raw/main/ofl/montserrat/Montserrat-Bold.ttf"
32
- FONT_BOLD_PATH = "/tmp/Montserrat-Bold.ttf"
 
 
33
 
34
- # Voice configs
35
  EDGE_VOICES = {
36
  "Aria (US Female)": "en-US-AriaNeural",
37
  "Davis (US Male)": "en-US-DavisNeural",
38
  "Jenny (US Female)": "en-US-JennyNeural",
39
  "Guy (US Male)": "en-US-GuyNeural",
40
- "Sonia (UK Female)": "en-GB-SoniaNeural",
41
- "Ryan (UK Male)": "en-GB-RyanNeural",
42
  }
43
- DEFAULT_VOICE_KEY = "Aria (US Female)"
44
 
45
- # Prompts (factual + CTA)
46
- REEL_TYPES = ["Fact", "Custom Prompt", "Top 5", "Blog to Reel"]
47
- REEL_PROMPTS = {
48
- "Fact": (
49
- "Create a FACTUAL reel script based STRICTLY on the provided news article.\n"
50
- "Do NOT invent facts. Each line: 15-20 words.\n"
51
- "The LAST line MUST be a call-to-action with the source domain (e.g., 'Visit ChainStreet.io for the full story').\n"
52
- "Output exactly {n} lines, one per line."
53
- ),
54
- "Custom Prompt": (
55
- "You are a senior financial journalist. Write a FACTUAL script based ONLY on the provided news content.\n"
56
- "No speculation. Last line: call-to-action with source domain.\n"
57
- "Each line: 15-20 words. Output {n} lines."
58
- ),
59
- }
60
- DEFAULT_PROMPT = REEL_PROMPTS["Fact"]
61
-
62
- # Font loader
63
- _font = None
64
  def get_font(size=64):
65
- global _font
66
- if _font is None:
67
- try:
68
- if not os.path.exists(FONT_BOLD_PATH):
69
- urllib.request.urlretrieve(FONT_BOLD_URL, FONT_BOLD_PATH)
70
- _font = ImageFont.truetype(FONT_BOLD_PATH, size)
71
- except:
72
- _font = ImageFont.load_default()
73
- return _font
74
 
75
- # Scrape URL
76
  def scrape_url(url):
77
  try:
78
  import trafilatura
79
  text = trafilatura.extract(trafilatura.fetch_url(url))
80
  return text.strip() if text else ""
81
  except Exception as e:
82
- print(f"[scrape] {e}")
83
  return ""
84
 
85
- # Generate script with Groq
86
- def generate_script(content, groq_key, reel_type, num_points):
87
  from groq import Groq
88
  client = Groq(api_key=groq_key.strip())
89
- prompt = REEL_PROMPTS.get(reel_type, DEFAULT_PROMPT).format(n=num_points)
 
 
 
90
  resp = client.chat.completions.create(
91
  model="llama-3.3-70b-versatile",
92
- messages=[{"role": "system", "content": prompt},
93
- {"role": "user", "content": f"Content:\n{content[:4000]}"}],
94
- temperature=0.75,
 
 
95
  max_tokens=700,
96
  )
97
- lines = [l.strip() for l in resp.choices[0].message.content.strip().split("\n") if l.strip()]
98
  return lines[:num_points]
99
 
100
- # Edge TTS (with voice selection)
101
- async def _edge_save(text, voice, path):
102
  import edge_tts
103
  await edge_tts.Communicate(text, voice).save(path)
104
 
105
  def generate_audio(text, voice_key):
106
- if not text.strip():
107
- raise ValueError("Empty text")
108
- voice_id = EDGE_VOICES.get(voice_key, EDGE_VOICES[DEFAULT_VOICE_KEY])
109
- print(f"[TTS] Using voice: {voice_id} (key: {voice_key})")
110
  out = tempfile.mktemp(suffix=".mp3")
111
  loop = asyncio.new_event_loop()
112
  try:
113
- loop.run_until_complete(_edge_save(text, voice_id, out))
114
  finally:
115
  loop.close()
116
  return out
117
 
118
- # Fetch ONE background video from Pexels (loop it later)
119
  def fetch_pexels_video(query, api_key):
120
  if not api_key:
121
- print("[Pexels] No API key")
122
  return None
123
  try:
124
  resp = requests.get(
125
  "https://api.pexels.com/videos/search",
126
  headers={"Authorization": api_key},
127
  params={"query": query, "per_page": 5, "orientation": "portrait"},
128
- timeout=15,
129
  )
130
  data = resp.json()
131
  videos = data.get("videos", [])
132
- if not videos:
133
- print(f"[Pexels] No videos for '{query}'")
134
- return None
135
- # pick first portrait video
136
  for vid in videos:
137
- files = vid.get("video_files", [])
138
- for f in files:
139
- if f.get("height") == VIDEO_H and f.get("width") == VIDEO_W:
140
- url = f["link"]
141
  break
142
  else:
143
  # fallback to any portrait
144
- for f in files:
145
- if f.get("width", 0) < f.get("height", 0):
146
- url = f["link"]
147
  break
148
  else:
149
  continue
@@ -153,27 +107,25 @@ def fetch_pexels_video(query, api_key):
153
  with open(out, "wb") as f:
154
  for chunk in r.iter_content(32768):
155
  f.write(chunk)
156
- print(f"[Pexels] Downloaded: {out}")
157
  return out
158
  except Exception as e:
159
- print(f"[Pexels] Error: {e}")
160
  return None
161
 
162
- # Create main reel (single background video looped, then append outro)
163
- def create_reel(sentences, audio_path, bg_video_path, logo_path, accent_hex, outro_path):
164
  import moviepy.editor as mpe
165
- from moviepy.video.fx import loop, resize
166
 
167
  W, H = VIDEO_W, VIDEO_H
168
- # Load audio
169
  audio = mpe.AudioFileClip(audio_path)
170
  total_dur = audio.duration
171
- dur_each = total_dur / len(sentences)
172
 
173
- # Background video (loop if shorter)
174
  if bg_video_path and os.path.exists(bg_video_path):
175
  bg = mpe.VideoFileClip(bg_video_path)
176
- # Resize to fit 720x1280 (cover)
177
  if bg.w / bg.h > W / H:
178
  bg = bg.resize(height=H)
179
  else:
@@ -182,23 +134,23 @@ def create_reel(sentences, audio_path, bg_video_path, logo_path, accent_hex, out
182
  if bg.duration < total_dur:
183
  bg = mpe.concatenate_videoclips([bg] * int(np.ceil(total_dur / bg.duration)))
184
  bg = bg.subclip(0, total_dur)
185
- # Darken a bit
186
  dark = mpe.ColorClip((W, H), color=(0,0,0)).set_opacity(0.3).set_duration(total_dur)
187
  bg_layer = mpe.CompositeVideoClip([bg, dark])
188
  else:
189
- # Gradient fallback
190
  frame = np.zeros((H, W, 3), dtype=np.uint8)
191
  for i in range(H):
192
  frame[i] = [int(30 + i/H*100), int(20 + i/H*50), int(40 + i/H*80)]
193
  bg_layer = mpe.ImageClip(frame).set_duration(total_dur)
194
 
195
- # Text clips (no background, no shadow)
196
- def make_text_clip(sentence, start, duration):
197
  img = Image.new("RGBA", (W, H), (0,0,0,0))
198
  draw = ImageDraw.Draw(img)
199
  font = get_font(60)
200
- # word wrap
201
- words = sentence.split()
202
  lines = []
203
  cur = []
204
  for w in words:
@@ -216,26 +168,24 @@ def create_reel(sentences, audio_path, bg_video_path, logo_path, accent_hex, out
216
  for line in lines:
217
  bbox = draw.textbbox((0,0), line, font=font)
218
  x = (W - (bbox[2]-bbox[0]))//2
219
- draw.text((x, y), line, font=font, fill=(255,255,255,255))
220
  y += line_h
221
- # Load logo if any
222
  if logo_path and os.path.exists(logo_path):
223
  try:
224
  logo = Image.open(logo_path).convert("RGBA")
225
  logo = logo.resize((180, int(180*logo.height/logo.width)), Image.LANCZOS)
226
  img.paste(logo, (W-180-30, 30), logo)
227
  except Exception as e:
228
- print(f"[logo] {e}")
229
- clip = mpe.ImageClip(np.array(img)).set_start(start).set_duration(duration)
230
- return clip
231
 
232
- text_clips = [make_text_clip(s, i*dur_each, dur_each) for i, s in enumerate(sentences)]
233
  final = mpe.CompositeVideoClip([bg_layer] + text_clips).set_audio(audio)
234
 
235
- # Append outro if provided
236
  if outro_path and os.path.exists(outro_path):
237
  outro = mpe.VideoFileClip(outro_path)
238
- # resize to same dimensions
239
  if outro.w / outro.h > W / H:
240
  outro = outro.resize(height=H)
241
  else:
@@ -244,33 +194,31 @@ def create_reel(sentences, audio_path, bg_video_path, logo_path, accent_hex, out
244
  final = mpe.concatenate_videoclips([final, outro])
245
 
246
  out = tempfile.mktemp(suffix=".mp4")
247
- final.write_videofile(out, codec="libx264", audio_codec="aac", fps=FPS, preset="ultrafast", logger=None)
248
  return out
249
 
250
- # Gradio pipeline
251
- def generate_script_only(url_or_text, groq_key, reel_type, num_points, progress=gr.Progress()):
252
  if not groq_key:
253
  groq_key = os.getenv("GROQ_API_KEY", "")
254
  if not groq_key:
255
- return "", "❌ Groq API key missing"
256
  raw = url_or_text.strip()
257
  if raw.startswith("http"):
258
  content = scrape_url(raw)
259
  if not content:
260
- return "", "❌ Could not extract text from URL"
261
  else:
262
  content = raw
263
- if len(content) < 20:
264
- return "", "❌ Please enter a valid URL or topic"
265
- sentences = generate_script(content, groq_key, reel_type, int(num_points))
266
  if not sentences:
267
  return "", "❌ Script generation failed"
268
  script_text = "\n".join(sentences)
269
  md = "\n\n".join(f"**{i+1}.** {s}" for i,s in enumerate(sentences))
270
  return script_text, f"## Script Ready\n\n{md}"
271
 
272
- def create_video_from_script(script_text, groq_key, pexels_key, tts_engine, edge_voice, kokoro_voice,
273
- logo_file, logo_pos, accent_hex, show_caption_bg, outro_video, progress=gr.Progress()):
274
  if not groq_key:
275
  groq_key = os.getenv("GROQ_API_KEY", "")
276
  if not groq_key:
@@ -282,60 +230,44 @@ def create_video_from_script(script_text, groq_key, pexels_key, tts_engine, edge
282
  if not sentences:
283
  return None, "❌ No script"
284
  full_script = " ".join(sentences)
285
- # Generate audio
286
- if tts_engine == "Edge TTS":
287
- audio_path = generate_audio(full_script, edge_voice)
288
- else:
289
- # Kokoro fallback to Edge
290
- audio_path = generate_audio(full_script, edge_voice)
291
 
292
- # Get background video from Pexels
293
  bg_video = None
294
  if pexels_key:
295
- # Extract keyword from first sentence
296
  kw = re.sub(r'[^\w\s]', '', sentences[0]).split()[:3]
297
  kw = " ".join(kw) if kw else "news"
298
- print(f"[Pexels] Searching for: {kw}")
299
  bg_video = fetch_pexels_video(kw, pexels_key)
300
- if not bg_video:
301
- print("[Pexels] No video found, using gradient")
302
 
303
  logo_path = logo_file if isinstance(logo_file, str) else (logo_file.name if logo_file else None)
304
- outro_path = outro_video if isinstance(outro_video, str) else (outro_video.name if outro_video else None)
305
 
306
- video_path = create_reel(sentences, audio_path, bg_video, logo_path, accent_hex, outro_path)
307
  md = "\n\n".join(f"**{i+1}.** {s}" for i,s in enumerate(sentences))
308
  return video_path, f"## Reel Ready!\n\n{md}"
309
 
310
- # Gradio UI
311
  with gr.Blocks(title="AI Reels Maker") as demo:
312
- gr.Markdown("# 🎬 AI Reels Maker\nFactual news reels with background video and optional outro")
313
- with gr.Tab("Reel Generator"):
314
- with gr.Row():
315
- with gr.Column():
316
- url_input = gr.Textbox(label="URL or Topic", lines=3)
317
- reel_type = gr.Dropdown(choices=REEL_TYPES, value="Fact")
318
- groq_key = gr.Textbox(label="Groq API Key", type="password", placeholder="or set GROQ_API_KEY secret")
319
- pexels_key = gr.Textbox(label="Pexels API Key", type="password", placeholder="or set PEXELS_API_KEY secret")
320
- with gr.Accordion("Voice"):
321
- tts_engine = gr.Radio(["Edge TTS", "Kokoro TTS"], value="Edge TTS")
322
- edge_voice = gr.Dropdown(choices=list(EDGE_VOICES.keys()), value=DEFAULT_VOICE_KEY, label="Edge Voice")
323
- kokoro_voice = gr.Dropdown(choices=["Heart (US Female)"], value="Heart (US Female)", visible=False)
324
- with gr.Accordion("Branding"):
325
- logo_file = gr.File(label="Logo (PNG)", type="filepath")
326
- logo_pos = gr.Dropdown(["top-right","top-left","bottom-right","bottom-left"], value="top-right")
327
- accent_hex = gr.ColorPicker(value="#d4af37", label="Accent Color")
328
- outro_video = gr.File(label="Outro Video (optional)", type="filepath")
329
- num_points = gr.Slider(6, 10, value=8, step=1, label="Number of sentences")
330
- gen_btn = gr.Button("Generate Script")
331
- create_btn = gr.Button("Create Video")
332
- with gr.Column():
333
- script_editor = gr.TextArea(label="Edit Script", lines=10, interactive=True)
334
- video_out = gr.Video(label="Your Reel")
335
- status = gr.Markdown()
336
 
337
- tts_engine.change(lambda e: (gr.update(visible=e=="Edge TTS"), gr.update(visible=e=="Kokoro TTS")), inputs=tts_engine, outputs=[edge_voice, kokoro_voice])
338
- gen_btn.click(generate_script_only, inputs=[url_input, groq_key, reel_type, num_points], outputs=[script_editor, status])
339
- create_btn.click(create_video_from_script, inputs=[script_editor, groq_key, pexels_key, tts_engine, edge_voice, kokoro_voice, logo_file, logo_pos, accent_hex, gr.Checkbox(value=False, visible=False), outro_video], outputs=[video_out, status])
340
 
341
  demo.launch()
 
 
 
 
 
 
 
 
 
1
  import sys
2
  sys.stdout.reconfigure(line_buffering=True)
3
 
 
4
  import os
5
  import re
6
  import tempfile
7
+ import asyncio
 
 
 
 
8
  import requests
9
+ import numpy as np
10
+ from PIL import Image, ImageDraw, ImageFont
11
+ import gradio as gr
 
 
 
 
 
12
 
13
+ # ---------- Constants ----------
14
+ VIDEO_W, VIDEO_H, FPS = 720, 1280, 24
15
+ FONT_URL = "https://github.com/google/fonts/raw/main/ofl/montserrat/Montserrat-Bold.ttf"
16
+ FONT_PATH = "/tmp/Montserrat-Bold.ttf"
17
 
 
18
  EDGE_VOICES = {
19
  "Aria (US Female)": "en-US-AriaNeural",
20
  "Davis (US Male)": "en-US-DavisNeural",
21
  "Jenny (US Female)": "en-US-JennyNeural",
22
  "Guy (US Male)": "en-US-GuyNeural",
 
 
23
  }
24
+ DEFAULT_VOICE = "Aria (US Female)"
25
 
26
+ # ---------- Font ----------
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
27
  def get_font(size=64):
28
+ if not os.path.exists(FONT_PATH):
29
+ import urllib.request
30
+ urllib.request.urlretrieve(FONT_URL, FONT_PATH)
31
+ return ImageFont.truetype(FONT_PATH, size)
 
 
 
 
 
32
 
33
+ # ---------- Scrape ----------
34
  def scrape_url(url):
35
  try:
36
  import trafilatura
37
  text = trafilatura.extract(trafilatura.fetch_url(url))
38
  return text.strip() if text else ""
39
  except Exception as e:
40
+ print(f"Scrape error: {e}")
41
  return ""
42
 
43
+ # ---------- Script Generation (Groq) ----------
44
+ def generate_script(content, groq_key, num_points=8):
45
  from groq import Groq
46
  client = Groq(api_key=groq_key.strip())
47
+ prompt = f"""Create a FACTUAL reel script based STRICTLY on the provided news article.
48
+ Do NOT invent facts. Each line: 15-20 words.
49
+ The LAST line MUST be a call-to-action with the source domain (e.g., 'Visit ChainStreet.io for the full story').
50
+ Output exactly {num_points} lines, one per line."""
51
  resp = client.chat.completions.create(
52
  model="llama-3.3-70b-versatile",
53
+ messages=[
54
+ {"role": "system", "content": prompt},
55
+ {"role": "user", "content": f"Content:\n{content[:4000]}"}
56
+ ],
57
+ temperature=0.7,
58
  max_tokens=700,
59
  )
60
+ lines = [l.strip() for l in resp.choices[0].message.content.split("\n") if l.strip()]
61
  return lines[:num_points]
62
 
63
+ # ---------- Edge TTS ----------
64
+ async def _tts(text, voice, path):
65
  import edge_tts
66
  await edge_tts.Communicate(text, voice).save(path)
67
 
68
  def generate_audio(text, voice_key):
69
+ voice_id = EDGE_VOICES.get(voice_key, EDGE_VOICES[DEFAULT_VOICE])
 
 
 
70
  out = tempfile.mktemp(suffix=".mp3")
71
  loop = asyncio.new_event_loop()
72
  try:
73
+ loop.run_until_complete(_tts(text, voice_id, out))
74
  finally:
75
  loop.close()
76
  return out
77
 
78
+ # ---------- Pexels: fetch one portrait video ----------
79
  def fetch_pexels_video(query, api_key):
80
  if not api_key:
 
81
  return None
82
  try:
83
  resp = requests.get(
84
  "https://api.pexels.com/videos/search",
85
  headers={"Authorization": api_key},
86
  params={"query": query, "per_page": 5, "orientation": "portrait"},
87
+ timeout=10
88
  )
89
  data = resp.json()
90
  videos = data.get("videos", [])
 
 
 
 
91
  for vid in videos:
92
+ for file in vid.get("video_files", []):
93
+ if file.get("height") == VIDEO_H and file.get("width") == VIDEO_W:
94
+ url = file["link"]
 
95
  break
96
  else:
97
  # fallback to any portrait
98
+ for file in vid.get("video_files", []):
99
+ if file.get("width", 0) < file.get("height", 0):
100
+ url = file["link"]
101
  break
102
  else:
103
  continue
 
107
  with open(out, "wb") as f:
108
  for chunk in r.iter_content(32768):
109
  f.write(chunk)
 
110
  return out
111
  except Exception as e:
112
+ print(f"Pexels error: {e}")
113
  return None
114
 
115
+ # ---------- Create final video (background loop + outro) ----------
116
+ def create_reel(sentences, audio_path, bg_video_path, logo_path, outro_path):
117
  import moviepy.editor as mpe
118
+ from moviepy.video.fx import loop
119
 
120
  W, H = VIDEO_W, VIDEO_H
 
121
  audio = mpe.AudioFileClip(audio_path)
122
  total_dur = audio.duration
123
+ seg_dur = total_dur / len(sentences)
124
 
125
+ # Background
126
  if bg_video_path and os.path.exists(bg_video_path):
127
  bg = mpe.VideoFileClip(bg_video_path)
128
+ # Resize to cover 720x1280
129
  if bg.w / bg.h > W / H:
130
  bg = bg.resize(height=H)
131
  else:
 
134
  if bg.duration < total_dur:
135
  bg = mpe.concatenate_videoclips([bg] * int(np.ceil(total_dur / bg.duration)))
136
  bg = bg.subclip(0, total_dur)
137
+ # darken for text readability
138
  dark = mpe.ColorClip((W, H), color=(0,0,0)).set_opacity(0.3).set_duration(total_dur)
139
  bg_layer = mpe.CompositeVideoClip([bg, dark])
140
  else:
141
+ # gradient fallback
142
  frame = np.zeros((H, W, 3), dtype=np.uint8)
143
  for i in range(H):
144
  frame[i] = [int(30 + i/H*100), int(20 + i/H*50), int(40 + i/H*80)]
145
  bg_layer = mpe.ImageClip(frame).set_duration(total_dur)
146
 
147
+ # Text clips (no background)
148
+ def make_text_clip(text, start, duration):
149
  img = Image.new("RGBA", (W, H), (0,0,0,0))
150
  draw = ImageDraw.Draw(img)
151
  font = get_font(60)
152
+ # wrap
153
+ words = text.split()
154
  lines = []
155
  cur = []
156
  for w in words:
 
168
  for line in lines:
169
  bbox = draw.textbbox((0,0), line, font=font)
170
  x = (W - (bbox[2]-bbox[0]))//2
171
+ draw.text((x, y), line, font=font, fill=(255,255,255))
172
  y += line_h
173
+ # logo overlay if provided
174
  if logo_path and os.path.exists(logo_path):
175
  try:
176
  logo = Image.open(logo_path).convert("RGBA")
177
  logo = logo.resize((180, int(180*logo.height/logo.width)), Image.LANCZOS)
178
  img.paste(logo, (W-180-30, 30), logo)
179
  except Exception as e:
180
+ print(f"Logo error: {e}")
181
+ return mpe.ImageClip(np.array(img)).set_start(start).set_duration(duration)
 
182
 
183
+ text_clips = [make_text_clip(s, i*seg_dur, seg_dur) for i, s in enumerate(sentences)]
184
  final = mpe.CompositeVideoClip([bg_layer] + text_clips).set_audio(audio)
185
 
186
+ # Append outro
187
  if outro_path and os.path.exists(outro_path):
188
  outro = mpe.VideoFileClip(outro_path)
 
189
  if outro.w / outro.h > W / H:
190
  outro = outro.resize(height=H)
191
  else:
 
194
  final = mpe.concatenate_videoclips([final, outro])
195
 
196
  out = tempfile.mktemp(suffix=".mp4")
197
+ final.write_videofile(out, codec="libx264", audio_codec="aac", fps=FPS, preset="medium", logger=None)
198
  return out
199
 
200
+ # ---------- Gradio pipelines ----------
201
+ def generate_script_only(url_or_text, groq_key, num_points, progress=gr.Progress()):
202
  if not groq_key:
203
  groq_key = os.getenv("GROQ_API_KEY", "")
204
  if not groq_key:
205
+ return "", "❌ Groq API key required"
206
  raw = url_or_text.strip()
207
  if raw.startswith("http"):
208
  content = scrape_url(raw)
209
  if not content:
210
+ return "", "❌ Could not extract content from URL"
211
  else:
212
  content = raw
213
+ sentences = generate_script(content, groq_key, num_points)
 
 
214
  if not sentences:
215
  return "", "❌ Script generation failed"
216
  script_text = "\n".join(sentences)
217
  md = "\n\n".join(f"**{i+1}.** {s}" for i,s in enumerate(sentences))
218
  return script_text, f"## Script Ready\n\n{md}"
219
 
220
+ def create_video_from_script(script_text, groq_key, pexels_key, voice_key,
221
+ logo_file, outro_file, progress=gr.Progress()):
222
  if not groq_key:
223
  groq_key = os.getenv("GROQ_API_KEY", "")
224
  if not groq_key:
 
230
  if not sentences:
231
  return None, "❌ No script"
232
  full_script = " ".join(sentences)
233
+ # generate audio
234
+ audio_path = generate_audio(full_script, voice_key)
 
 
 
 
235
 
236
+ # fetch background video from Pexels
237
  bg_video = None
238
  if pexels_key:
239
+ # simple keyword from first sentence
240
  kw = re.sub(r'[^\w\s]', '', sentences[0]).split()[:3]
241
  kw = " ".join(kw) if kw else "news"
 
242
  bg_video = fetch_pexels_video(kw, pexels_key)
 
 
243
 
244
  logo_path = logo_file if isinstance(logo_file, str) else (logo_file.name if logo_file else None)
245
+ outro_path = outro_file if isinstance(outro_file, str) else (outro_file.name if outro_file else None)
246
 
247
+ video_path = create_reel(sentences, audio_path, bg_video, logo_path, outro_path)
248
  md = "\n\n".join(f"**{i+1}.** {s}" for i,s in enumerate(sentences))
249
  return video_path, f"## Reel Ready!\n\n{md}"
250
 
251
+ # ---------- UI ----------
252
  with gr.Blocks(title="AI Reels Maker") as demo:
253
+ gr.Markdown("# 🎬 AI Reels Maker\nFactual news reels with Pexels background + optional outro")
254
+ with gr.Row():
255
+ with gr.Column():
256
+ url_input = gr.Textbox(label="URL or Topic", lines=3)
257
+ groq_key = gr.Textbox(label="Groq API Key", type="password", placeholder="or set GROQ_API_KEY secret")
258
+ pexels_key = gr.Textbox(label="Pexels API Key", type="password", placeholder="or set PEXELS_API_KEY secret")
259
+ voice = gr.Dropdown(choices=list(EDGE_VOICES.keys()), value=DEFAULT_VOICE, label="Voice")
260
+ logo = gr.File(label="Logo (optional)", type="filepath")
261
+ outro = gr.File(label="Outro Video (optional)", type="filepath")
262
+ num_points = gr.Slider(6, 10, value=8, step=1, label="Number of sentences")
263
+ gen_btn = gr.Button("Generate Script")
264
+ create_btn = gr.Button("Create Video")
265
+ with gr.Column():
266
+ script_editor = gr.TextArea(label="Edit Script", lines=12, interactive=True)
267
+ video = gr.Video(label="Your Reel")
268
+ status = gr.Markdown()
 
 
 
 
 
 
 
 
269
 
270
+ gen_btn.click(generate_script_only, inputs=[url_input, groq_key, num_points], outputs=[script_editor, status])
271
+ create_btn.click(create_video_from_script, inputs=[script_editor, groq_key, pexels_key, voice, logo, outro], outputs=[video, status])
 
272
 
273
  demo.launch()