Update app.py
Browse files
app.py
CHANGED
|
@@ -8,6 +8,12 @@ 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 ----------
|
|
@@ -66,16 +72,15 @@ async def _tts(text, voice, path):
|
|
| 66 |
await edge_tts.Communicate(text, voice).save(path)
|
| 67 |
|
| 68 |
def generate_audio(text, voice_key):
|
| 69 |
-
# Sanitize text
|
| 70 |
-
text = re.sub(r'[ββββ]', '"', text)
|
| 71 |
-
text = re.sub(r'[^\x00-\x7F]+', ' ', text)
|
| 72 |
text = re.sub(r'\s+', ' ', text).strip()
|
| 73 |
if len(text) < 10:
|
| 74 |
raise ValueError(f"Text too short ({len(text)} chars): '{text}'")
|
| 75 |
print(f"[TTS] Original text length: {len(text)} chars")
|
| 76 |
print(f"[TTS] Text snippet: {text[:200]}...")
|
| 77 |
|
| 78 |
-
# Get voice ID
|
| 79 |
voice_id = EDGE_VOICES.get(voice_key)
|
| 80 |
if not voice_id:
|
| 81 |
print(f"[TTS] Unknown voice key '{voice_key}', using default '{DEFAULT_VOICE}'")
|
|
@@ -83,7 +88,6 @@ def generate_audio(text, voice_key):
|
|
| 83 |
print(f"[TTS] Using voice ID: {voice_id} (key: {voice_key})")
|
| 84 |
|
| 85 |
out = tempfile.mktemp(suffix=".mp3")
|
| 86 |
-
# Try selected voice, then fallback to default voice if it fails
|
| 87 |
for attempt, try_voice in enumerate([voice_id, EDGE_VOICES[DEFAULT_VOICE]]):
|
| 88 |
if attempt > 0:
|
| 89 |
print(f"[TTS] Retry with fallback voice: {try_voice}")
|
|
@@ -122,14 +126,12 @@ def fetch_pexels_video(query, api_key):
|
|
| 122 |
url = file["link"]
|
| 123 |
break
|
| 124 |
else:
|
| 125 |
-
# fallback to any portrait
|
| 126 |
for file in vid.get("video_files", []):
|
| 127 |
if file.get("width", 0) < file.get("height", 0):
|
| 128 |
url = file["link"]
|
| 129 |
break
|
| 130 |
else:
|
| 131 |
continue
|
| 132 |
-
# download
|
| 133 |
r = requests.get(url, stream=True)
|
| 134 |
out = tempfile.mktemp(suffix=".mp4")
|
| 135 |
with open(out, "wb") as f:
|
|
@@ -140,10 +142,9 @@ def fetch_pexels_video(query, api_key):
|
|
| 140 |
print(f"Pexels error: {e}")
|
| 141 |
return None
|
| 142 |
|
| 143 |
-
# ---------- Create final video
|
| 144 |
def create_reel(sentences, audio_path, bg_video_path, logo_path, outro_path):
|
| 145 |
import moviepy.editor as mpe
|
| 146 |
-
from moviepy.video.fx import loop
|
| 147 |
|
| 148 |
W, H = VIDEO_W, VIDEO_H
|
| 149 |
audio = mpe.AudioFileClip(audio_path)
|
|
@@ -153,7 +154,6 @@ def create_reel(sentences, audio_path, bg_video_path, logo_path, outro_path):
|
|
| 153 |
# Background
|
| 154 |
if bg_video_path and os.path.exists(bg_video_path):
|
| 155 |
bg = mpe.VideoFileClip(bg_video_path)
|
| 156 |
-
# Resize to cover 720x1280
|
| 157 |
if bg.w / bg.h > W / H:
|
| 158 |
bg = bg.resize(height=H)
|
| 159 |
else:
|
|
@@ -162,11 +162,9 @@ def create_reel(sentences, audio_path, bg_video_path, logo_path, outro_path):
|
|
| 162 |
if bg.duration < total_dur:
|
| 163 |
bg = mpe.concatenate_videoclips([bg] * int(np.ceil(total_dur / bg.duration)))
|
| 164 |
bg = bg.subclip(0, total_dur)
|
| 165 |
-
# darken for text readability
|
| 166 |
dark = mpe.ColorClip((W, H), color=(0,0,0)).set_opacity(0.3).set_duration(total_dur)
|
| 167 |
bg_layer = mpe.CompositeVideoClip([bg, dark])
|
| 168 |
else:
|
| 169 |
-
# gradient fallback
|
| 170 |
frame = np.zeros((H, W, 3), dtype=np.uint8)
|
| 171 |
for i in range(H):
|
| 172 |
frame[i] = [int(30 + i/H*100), int(20 + i/H*50), int(40 + i/H*80)]
|
|
@@ -177,7 +175,6 @@ def create_reel(sentences, audio_path, bg_video_path, logo_path, outro_path):
|
|
| 177 |
img = Image.new("RGBA", (W, H), (0,0,0,0))
|
| 178 |
draw = ImageDraw.Draw(img)
|
| 179 |
font = get_font(60)
|
| 180 |
-
# wrap
|
| 181 |
words = text.split()
|
| 182 |
lines = []
|
| 183 |
cur = []
|
|
@@ -198,7 +195,6 @@ def create_reel(sentences, audio_path, bg_video_path, logo_path, outro_path):
|
|
| 198 |
x = (W - (bbox[2]-bbox[0]))//2
|
| 199 |
draw.text((x, y), line, font=font, fill=(255,255,255))
|
| 200 |
y += line_h
|
| 201 |
-
# logo overlay if provided
|
| 202 |
if logo_path and os.path.exists(logo_path):
|
| 203 |
try:
|
| 204 |
logo = Image.open(logo_path).convert("RGBA")
|
|
@@ -259,16 +255,13 @@ def create_video_from_script(script_text, groq_key, pexels_key, voice_key,
|
|
| 259 |
return None, "β No script"
|
| 260 |
full_script = " ".join(sentences)
|
| 261 |
|
| 262 |
-
# Generate audio (with retry and fallback)
|
| 263 |
try:
|
| 264 |
audio_path = generate_audio(full_script, voice_key)
|
| 265 |
except Exception as e:
|
| 266 |
return None, f"β Audio generation failed: {str(e)}"
|
| 267 |
|
| 268 |
-
# fetch background video from Pexels
|
| 269 |
bg_video = None
|
| 270 |
if pexels_key:
|
| 271 |
-
# simple keyword from first sentence
|
| 272 |
kw = re.sub(r'[^\w\s]', '', sentences[0]).split()[:3]
|
| 273 |
kw = " ".join(kw) if kw else "news"
|
| 274 |
print(f"[Pexels] Searching for: {kw}")
|
|
|
|
| 8 |
import requests
|
| 9 |
import numpy as np
|
| 10 |
from PIL import Image, ImageDraw, ImageFont
|
| 11 |
+
|
| 12 |
+
# ---------- PILLOW COMPATIBILITY FIX ----------
|
| 13 |
+
# MoviePy uses Image.ANTIALIAS which was removed in Pillow 10+
|
| 14 |
+
if not hasattr(Image, 'ANTIALIAS'):
|
| 15 |
+
Image.ANTIALIAS = Image.LANCZOS
|
| 16 |
+
|
| 17 |
import gradio as gr
|
| 18 |
|
| 19 |
# ---------- Constants ----------
|
|
|
|
| 72 |
await edge_tts.Communicate(text, voice).save(path)
|
| 73 |
|
| 74 |
def generate_audio(text, voice_key):
|
| 75 |
+
# Sanitize text
|
| 76 |
+
text = re.sub(r'[ββββ]', '"', text)
|
| 77 |
+
text = re.sub(r'[^\x00-\x7F]+', ' ', text)
|
| 78 |
text = re.sub(r'\s+', ' ', text).strip()
|
| 79 |
if len(text) < 10:
|
| 80 |
raise ValueError(f"Text too short ({len(text)} chars): '{text}'")
|
| 81 |
print(f"[TTS] Original text length: {len(text)} chars")
|
| 82 |
print(f"[TTS] Text snippet: {text[:200]}...")
|
| 83 |
|
|
|
|
| 84 |
voice_id = EDGE_VOICES.get(voice_key)
|
| 85 |
if not voice_id:
|
| 86 |
print(f"[TTS] Unknown voice key '{voice_key}', using default '{DEFAULT_VOICE}'")
|
|
|
|
| 88 |
print(f"[TTS] Using voice ID: {voice_id} (key: {voice_key})")
|
| 89 |
|
| 90 |
out = tempfile.mktemp(suffix=".mp3")
|
|
|
|
| 91 |
for attempt, try_voice in enumerate([voice_id, EDGE_VOICES[DEFAULT_VOICE]]):
|
| 92 |
if attempt > 0:
|
| 93 |
print(f"[TTS] Retry with fallback voice: {try_voice}")
|
|
|
|
| 126 |
url = file["link"]
|
| 127 |
break
|
| 128 |
else:
|
|
|
|
| 129 |
for file in vid.get("video_files", []):
|
| 130 |
if file.get("width", 0) < file.get("height", 0):
|
| 131 |
url = file["link"]
|
| 132 |
break
|
| 133 |
else:
|
| 134 |
continue
|
|
|
|
| 135 |
r = requests.get(url, stream=True)
|
| 136 |
out = tempfile.mktemp(suffix=".mp4")
|
| 137 |
with open(out, "wb") as f:
|
|
|
|
| 142 |
print(f"Pexels error: {e}")
|
| 143 |
return None
|
| 144 |
|
| 145 |
+
# ---------- Create final video ----------
|
| 146 |
def create_reel(sentences, audio_path, bg_video_path, logo_path, outro_path):
|
| 147 |
import moviepy.editor as mpe
|
|
|
|
| 148 |
|
| 149 |
W, H = VIDEO_W, VIDEO_H
|
| 150 |
audio = mpe.AudioFileClip(audio_path)
|
|
|
|
| 154 |
# Background
|
| 155 |
if bg_video_path and os.path.exists(bg_video_path):
|
| 156 |
bg = mpe.VideoFileClip(bg_video_path)
|
|
|
|
| 157 |
if bg.w / bg.h > W / H:
|
| 158 |
bg = bg.resize(height=H)
|
| 159 |
else:
|
|
|
|
| 162 |
if bg.duration < total_dur:
|
| 163 |
bg = mpe.concatenate_videoclips([bg] * int(np.ceil(total_dur / bg.duration)))
|
| 164 |
bg = bg.subclip(0, total_dur)
|
|
|
|
| 165 |
dark = mpe.ColorClip((W, H), color=(0,0,0)).set_opacity(0.3).set_duration(total_dur)
|
| 166 |
bg_layer = mpe.CompositeVideoClip([bg, dark])
|
| 167 |
else:
|
|
|
|
| 168 |
frame = np.zeros((H, W, 3), dtype=np.uint8)
|
| 169 |
for i in range(H):
|
| 170 |
frame[i] = [int(30 + i/H*100), int(20 + i/H*50), int(40 + i/H*80)]
|
|
|
|
| 175 |
img = Image.new("RGBA", (W, H), (0,0,0,0))
|
| 176 |
draw = ImageDraw.Draw(img)
|
| 177 |
font = get_font(60)
|
|
|
|
| 178 |
words = text.split()
|
| 179 |
lines = []
|
| 180 |
cur = []
|
|
|
|
| 195 |
x = (W - (bbox[2]-bbox[0]))//2
|
| 196 |
draw.text((x, y), line, font=font, fill=(255,255,255))
|
| 197 |
y += line_h
|
|
|
|
| 198 |
if logo_path and os.path.exists(logo_path):
|
| 199 |
try:
|
| 200 |
logo = Image.open(logo_path).convert("RGBA")
|
|
|
|
| 255 |
return None, "β No script"
|
| 256 |
full_script = " ".join(sentences)
|
| 257 |
|
|
|
|
| 258 |
try:
|
| 259 |
audio_path = generate_audio(full_script, voice_key)
|
| 260 |
except Exception as e:
|
| 261 |
return None, f"β Audio generation failed: {str(e)}"
|
| 262 |
|
|
|
|
| 263 |
bg_video = None
|
| 264 |
if pexels_key:
|
|
|
|
| 265 |
kw = re.sub(r'[^\w\s]', '', sentences[0]).split()[:3]
|
| 266 |
kw = " ".join(kw) if kw else "news"
|
| 267 |
print(f"[Pexels] Searching for: {kw}")
|