yonagush commited on
Commit
0118580
Β·
verified Β·
1 Parent(s): 0217ca1

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +46 -43
app.py CHANGED
@@ -1,13 +1,12 @@
1
  """
2
- AI Reels Maker β€” HuggingFace Space v3.1
3
  =========================================
4
- Professional viral reel generator with:
5
- β€’ 45-60 second reels (8+ sentences, 15-20 words each)
6
- β€’ 2-3 stitched background videos from Pexels
7
- β€’ Optional outro video (plays after main reel)
8
- β€’ Editable script: generate, edit, then render
9
- β€’ Factual, CTA‑enforced prompts (no invented data)
10
- β€’ Robust Edge TTS with retry & fallback
11
  """
12
 
13
  import sys
@@ -40,7 +39,7 @@ FONT_BOLD_PATH = "/tmp/Montserrat-Bold.ttf"
40
  FONT_LIGHT_PATH = "/tmp/Montserrat-Regular.ttf"
41
 
42
  # ─────────────────────────────────────────────────────────────────────────────
43
- # VOICE CONFIGS
44
  # ─────────────────────────────────────────────────────────────────────────────
45
  EDGE_VOICES = {
46
  "Aria (US Female)" : "en-US-AriaNeural",
@@ -234,7 +233,7 @@ def generate_script(
234
  return lines[:num_points]
235
 
236
  # ─────────────────────────────────────────────────────────────────────────────
237
- # AUDIO β€” Edge TTS (improved with retry and fallback)
238
  # ─────────────────────────────────────────────────────────────────────────────
239
  async def _edge_tts_save(text: str, voice: str, path: str) -> None:
240
  import edge_tts
@@ -246,16 +245,18 @@ def generate_audio_edge(text: str, voice_key: str) -> str:
246
  if len(text.strip()) < 5:
247
  raise ValueError(f"Text too short: '{text}'")
248
 
 
249
  voice = EDGE_VOICES.get(voice_key)
250
  if voice is None:
251
- print(f"[TTS] Unknown voice key '{voice_key}', using default '{DEFAULT_VOICE_KEY}'")
252
  voice = EDGE_VOICES[DEFAULT_VOICE_KEY]
 
 
253
 
254
- print(f"[TTS] Using voice ID: {voice} (key: {voice_key})")
255
  print(f"[TTS] Text snippet: {text[:200]}... (total chars: {len(text)})")
256
 
257
  out = tempfile.mktemp(suffix=".mp3")
258
- # Try selected voice first, then fallback to default voice
259
  for attempt, try_voice in enumerate([voice, EDGE_VOICES[DEFAULT_VOICE_KEY]]):
260
  if attempt > 0:
261
  print(f"[TTS] Retry with fallback voice: {try_voice}")
@@ -299,7 +300,8 @@ def generate_audio_kokoro(text: str, voice_key: str) -> str:
299
  if _kokoro_pipeline is None:
300
  from kokoro import KPipeline
301
  _kokoro_pipeline = KPipeline(lang_code="a")
302
- voice = KOKORO_VOICES.get(voice_key, "af_heart")
 
303
  chunks = [a for _, _, a in _kokoro_pipeline(text, voice=voice, speed=1.05)]
304
  out = tempfile.mktemp(suffix=".wav")
305
  sf.write(out, np.concatenate(chunks), 24000)
@@ -347,7 +349,8 @@ def _fetch_one_video(query: str, api_key: str, used_ids: set) -> str | None:
347
  timeout=20,
348
  )
349
  resp.raise_for_status()
350
- for vid in resp.json().get("videos", []):
 
351
  vid_id = vid.get("id")
352
  if vid_id in used_ids:
353
  continue
@@ -371,14 +374,23 @@ def _fetch_one_video(query: str, api_key: str, used_ids: set) -> str | None:
371
  return None
372
 
373
  def fetch_bg_videos(sentences: list[str], api_key: str, count: int = 3) -> list[str]:
374
- queries = _extract_queries(sentences, count=count)
375
- used_ids = set()
376
- paths = []
 
 
 
 
377
  for q in queries:
378
  p = _fetch_one_video(q, api_key, used_ids)
379
  if p:
380
  paths.append(p)
381
- print(f"[pexels] Fetched {len(paths)}/{count} videos", flush=True)
 
 
 
 
 
382
  return paths
383
 
384
  # ─────────────────────────────────────────────────────────────────────────────
@@ -413,7 +425,7 @@ def paste_logo(base: Image.Image, logo: Image.Image | None, position: str = "top
413
  return out
414
 
415
  # ─────────────────────────────────────────────────────────────────────────────
416
- # TEXT FRAME RENDERER
417
  # ─────────────────────────────────────────────────────────────────────────────
418
  def render_text_frame(
419
  text: str, width: int, height: int,
@@ -421,15 +433,17 @@ def render_text_frame(
421
  logo_pos: str = "top-right",
422
  accent_color: tuple = (124, 58, 237),
423
  ) -> np.ndarray:
424
- img = Image.new("RGBA", (width, height), (0, 0, 0, 0))
 
425
  draw = ImageDraw.Draw(img)
426
  font = get_font(60, bold=True)
427
 
 
428
  words, lines, cur = text.split(), [], []
429
  for word in words:
430
  test = " ".join(cur + [word])
431
  bbox = draw.textbbox((0, 0), test, font=font)
432
- if bbox[2] - bbox[0] > width * 0.84 and cur:
433
  lines.append(" ".join(cur))
434
  cur = [word]
435
  else:
@@ -437,26 +451,14 @@ def render_text_frame(
437
  if cur:
438
  lines.append(" ".join(cur))
439
 
440
- line_h = 76
441
- total_text_h = len(lines) * line_h
442
- pad_v, pad_h = 44, 24
443
- box_y1 = max(pad_h, (height - total_text_h) // 2 - pad_v)
444
- box_y2 = min(height - pad_h, box_y1 + total_text_h + pad_v * 2)
445
-
446
- overlay = Image.new("RGBA", (width, height), (0, 0, 0, 0))
447
- od = ImageDraw.Draw(overlay)
448
- od.rounded_rectangle([20, box_y1, width - 20, box_y2], radius=22, fill=(6, 6, 20, 190))
449
- od.rectangle([20, box_y1, 29, box_y2], fill=(*accent_color, 245))
450
- img = Image.alpha_composite(img, overlay)
451
- draw = ImageDraw.Draw(img)
452
 
453
- y = box_y1 + pad_v
454
  for line in lines:
455
- bbox = draw.textbbox((0, 0), line, font=font)
456
  text_w = bbox[2] - bbox[0]
457
- x = (width - text_w) // 2
458
- for dx, dy in [(-2, -2), (2, -2), (-2, 2), (2, 2), (0, 3), (3, 0), (-3, 0), (0, -3)]:
459
- draw.text((x + dx, y + dy), line, font=font, fill=(0, 0, 0, 170))
460
  draw.text((x, y), line, font=font, fill=(255, 255, 255, 255))
461
  y += line_h
462
 
@@ -529,7 +531,8 @@ def create_reel(
529
  loops = int(np.ceil(seg_dur / bg.duration)) + 1
530
  bg = mpe.concatenate_videoclips([bg] * loops)
531
  bg = bg.subclip(0, seg_dur)
532
- dark = mpe.ColorClip((W, H), color=[0, 0, 0]).set_opacity(0.42).set_duration(seg_dur)
 
533
  seg = mpe.CompositeVideoClip([bg, dark]).set_start(start)
534
  bg_segs.append(seg)
535
  continue
@@ -540,7 +543,7 @@ def create_reel(
540
 
541
  bg_layer = mpe.CompositeVideoClip(bg_segs, size=(W, H)).set_duration(total_dur)
542
 
543
- # Text overlays
544
  text_clips = []
545
  for i, sentence in enumerate(sentences):
546
  arr = render_text_frame(sentence, W, H, logo, logo_pos, accent_color)
@@ -559,7 +562,7 @@ def create_reel(
559
  return out
560
 
561
  # ─────────────────────────────────────────────────────────────────────────────
562
- # NEWS ANCHOR MODE (simplified, kept from previous working version)
563
  # ─────────────────────────────────────────────────────────────────────────────
564
  def _make_studio_bg(W: int, H: int, dark: bool, accent_color: tuple) -> np.ndarray:
565
  frame = np.zeros((H, W, 3), dtype=np.uint8)
@@ -876,7 +879,7 @@ with gr.Blocks(title="🎬 AI Reels Maker", css=CSS, theme=gr.themes.Soft()) as
876
  logo_file = gr.File(label="πŸ“€ Upload Logo (PNG/JPG)", file_types=["image"], type="filepath")
877
  logo_pos = gr.Dropdown(choices=["top-right","top-left","bottom-right","bottom-left"], value="top-right", label="Logo Position")
878
  accent_hex = gr.ColorPicker(value="#d4af37", label="Accent Color")
879
- show_caption_bg = gr.Checkbox(label="Show caption background", value=True, info="Toggle semi-transparent card behind text")
880
  outro_video = gr.File(label="🎬 Optional Outro Video (plays after the reel)", file_types=["video"], type="filepath")
881
 
882
  num_points = gr.Slider(6, 10, value=8, step=1, label="πŸ“Š Number of points (8 = ~55s reel)")
 
1
  """
2
+ AI Reels Maker β€” HuggingFace Space v3.3
3
  =========================================
4
+ - Clean captions (no background, no shadow)
5
+ - Reliable Pexels background videos
6
+ - Voice selection works (Edge TTS & Kokoro)
7
+ - Optional outro video
8
+ - Factual, CTA‑enforced prompts
9
+ - Groq & Pexels API secrets support
 
10
  """
11
 
12
  import sys
 
39
  FONT_LIGHT_PATH = "/tmp/Montserrat-Regular.ttf"
40
 
41
  # ─────────────────────────────────────────────────────────────────────────────
42
+ # VOICE CONFIGS (plain ASCII keys)
43
  # ─────────────────────────────────────────────────────────────────────────────
44
  EDGE_VOICES = {
45
  "Aria (US Female)" : "en-US-AriaNeural",
 
233
  return lines[:num_points]
234
 
235
  # ─────────────────────────────────────────────────────────────────────────────
236
+ # AUDIO β€” Edge TTS (with voice selection debug)
237
  # ─────────────────────────────────────────────────────────────────────────────
238
  async def _edge_tts_save(text: str, voice: str, path: str) -> None:
239
  import edge_tts
 
245
  if len(text.strip()) < 5:
246
  raise ValueError(f"Text too short: '{text}'")
247
 
248
+ # Get voice ID from key; if not found, use default
249
  voice = EDGE_VOICES.get(voice_key)
250
  if voice is None:
251
+ print(f"[TTS] WARNING: Voice key '{voice_key}' not found. Using default '{DEFAULT_VOICE_KEY}'")
252
  voice = EDGE_VOICES[DEFAULT_VOICE_KEY]
253
+ else:
254
+ print(f"[TTS] Using voice: {voice} (key: {voice_key})")
255
 
 
256
  print(f"[TTS] Text snippet: {text[:200]}... (total chars: {len(text)})")
257
 
258
  out = tempfile.mktemp(suffix=".mp3")
259
+ # Try selected voice first, then fallback to default voice if it fails
260
  for attempt, try_voice in enumerate([voice, EDGE_VOICES[DEFAULT_VOICE_KEY]]):
261
  if attempt > 0:
262
  print(f"[TTS] Retry with fallback voice: {try_voice}")
 
300
  if _kokoro_pipeline is None:
301
  from kokoro import KPipeline
302
  _kokoro_pipeline = KPipeline(lang_code="a")
303
+ voice = KOKORO_VOICES.get(voice_key, "af_heart")
304
+ print(f"[Kokoro] Using voice: {voice}")
305
  chunks = [a for _, _, a in _kokoro_pipeline(text, voice=voice, speed=1.05)]
306
  out = tempfile.mktemp(suffix=".wav")
307
  sf.write(out, np.concatenate(chunks), 24000)
 
349
  timeout=20,
350
  )
351
  resp.raise_for_status()
352
+ data = resp.json()
353
+ for vid in data.get("videos", []):
354
  vid_id = vid.get("id")
355
  if vid_id in used_ids:
356
  continue
 
374
  return None
375
 
376
  def fetch_bg_videos(sentences: list[str], api_key: str, count: int = 3) -> list[str]:
377
+ if not api_key:
378
+ print("[pexels] No API key provided – skipping background videos.")
379
+ return []
380
+ queries = _extract_queries(sentences, count=count)
381
+ print(f"[pexels] Searching for videos with queries: {queries}")
382
+ used_ids = set()
383
+ paths = []
384
  for q in queries:
385
  p = _fetch_one_video(q, api_key, used_ids)
386
  if p:
387
  paths.append(p)
388
+ if len(paths) >= count:
389
+ break
390
+ if not paths:
391
+ print("[pexels] WARNING: No videos found. Using gradient fallback.")
392
+ else:
393
+ print(f"[pexels] Successfully fetched {len(paths)} videos: {paths}")
394
  return paths
395
 
396
  # ─────────────────────────────────────────────────────────────────────────────
 
425
  return out
426
 
427
  # ─────────────────────────────────────────────────────────────────────────────
428
+ # TEXT FRAME RENDERER (NO background, NO shadow)
429
  # ─────────────────────────────────────────────────────────────────────────────
430
  def render_text_frame(
431
  text: str, width: int, height: int,
 
433
  logo_pos: str = "top-right",
434
  accent_color: tuple = (124, 58, 237),
435
  ) -> np.ndarray:
436
+ """Render text with NO background and NO shadow – just clean text on video."""
437
+ img = Image.new("RGBA", (width, height), (0, 0, 0, 0)) # fully transparent
438
  draw = ImageDraw.Draw(img)
439
  font = get_font(60, bold=True)
440
 
441
+ # Word wrap to fit 90% of width (no background, so more space)
442
  words, lines, cur = text.split(), [], []
443
  for word in words:
444
  test = " ".join(cur + [word])
445
  bbox = draw.textbbox((0, 0), test, font=font)
446
+ if bbox[2] - bbox[0] > width * 0.90 and cur:
447
  lines.append(" ".join(cur))
448
  cur = [word]
449
  else:
 
451
  if cur:
452
  lines.append(" ".join(cur))
453
 
454
+ line_h = 72
455
+ y = (height - (len(lines) * line_h)) // 2
 
 
 
 
 
 
 
 
 
 
456
 
 
457
  for line in lines:
458
+ bbox = draw.textbbox((0, 0), line, font=font)
459
  text_w = bbox[2] - bbox[0]
460
+ x = (width - text_w) // 2
461
+ # Draw text directly, no shadow, no background
 
462
  draw.text((x, y), line, font=font, fill=(255, 255, 255, 255))
463
  y += line_h
464
 
 
531
  loops = int(np.ceil(seg_dur / bg.duration)) + 1
532
  bg = mpe.concatenate_videoclips([bg] * loops)
533
  bg = bg.subclip(0, seg_dur)
534
+ # Darken only if you want, but now text has no background so we keep it lighter
535
+ dark = mpe.ColorClip((W, H), color=[0, 0, 0]).set_opacity(0.35).set_duration(seg_dur)
536
  seg = mpe.CompositeVideoClip([bg, dark]).set_start(start)
537
  bg_segs.append(seg)
538
  continue
 
543
 
544
  bg_layer = mpe.CompositeVideoClip(bg_segs, size=(W, H)).set_duration(total_dur)
545
 
546
+ # Text overlays (clean, no background)
547
  text_clips = []
548
  for i, sentence in enumerate(sentences):
549
  arr = render_text_frame(sentence, W, H, logo, logo_pos, accent_color)
 
562
  return out
563
 
564
  # ─────────────────────────────────────────────────────────────────────────────
565
+ # NEWS ANCHOR MODE (simplified)
566
  # ─────────────────────────────────────────────────────────────────────────────
567
  def _make_studio_bg(W: int, H: int, dark: bool, accent_color: tuple) -> np.ndarray:
568
  frame = np.zeros((H, W, 3), dtype=np.uint8)
 
879
  logo_file = gr.File(label="πŸ“€ Upload Logo (PNG/JPG)", file_types=["image"], type="filepath")
880
  logo_pos = gr.Dropdown(choices=["top-right","top-left","bottom-right","bottom-left"], value="top-right", label="Logo Position")
881
  accent_hex = gr.ColorPicker(value="#d4af37", label="Accent Color")
882
+ show_caption_bg = gr.Checkbox(label="Show caption background", value=False, info="Unchecked = no background/shadow (clean text)", visible=False) # hidden because we removed background
883
  outro_video = gr.File(label="🎬 Optional Outro Video (plays after the reel)", file_types=["video"], type="filepath")
884
 
885
  num_points = gr.Slider(6, 10, value=8, step=1, label="πŸ“Š Number of points (8 = ~55s reel)")