Update app.py
Browse files
app.py
CHANGED
@@ -1,7 +1,11 @@
|
|
1 |
import gradio as gr
|
|
|
|
|
2 |
from PIL import Image
|
3 |
from io import BytesIO
|
4 |
import base64
|
|
|
|
|
5 |
import random
|
6 |
import urllib.parse
|
7 |
import time
|
@@ -12,8 +16,41 @@ current_version = gr.__version__
|
|
12 |
if current_version < required_version:
|
13 |
raise ValueError(f"Gradio version {current_version} is outdated. Please upgrade to {required_version} or later using 'pip install gradio=={required_version}'.")
|
14 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
15 |
def clean_response_text(response_text):
|
16 |
-
"""
|
|
|
|
|
17 |
cleaned_text = response_text.strip()
|
18 |
if cleaned_text.startswith("```json"):
|
19 |
cleaned_text = cleaned_text[len("```json"):].strip()
|
@@ -22,290 +59,795 @@ def clean_response_text(response_text):
|
|
22 |
return cleaned_text
|
23 |
|
24 |
def generate_ideas(user_input):
|
25 |
-
"""
|
|
|
|
|
|
|
26 |
yield (10, f"Brainstorming epic ideas for {user_input}... 🌟")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
27 |
try:
|
28 |
-
|
29 |
-
|
30 |
-
|
31 |
-
|
32 |
-
|
33 |
-
|
34 |
-
|
35 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
36 |
yield (20, f"Ideas locked in for {user_input}! 🚀")
|
37 |
-
|
|
|
38 |
except Exception as e:
|
39 |
print(f"Error generating ideas: {e}")
|
40 |
yield (20, f"Oops, tweaking the plan for {user_input}... 🔧")
|
41 |
-
|
42 |
f"A dramatic {user_input} scene with cinematic lighting",
|
43 |
f"A close-up of {user_input} in a futuristic setting",
|
44 |
-
f"A high-energy {user_input} moment with vibrant colors"
|
|
|
|
|
45 |
]
|
46 |
-
yield default_ideas # Yield default ideas on failure
|
47 |
|
48 |
-
def generate_item(user_input, ideas, generate_video=False):
|
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 |
def generate_progress_html(progress, message, user_input):
|
86 |
-
"""
|
|
|
|
|
87 |
return f"""
|
88 |
<div id="progress-container" style="
|
89 |
-
display: flex;
|
90 |
-
|
91 |
-
|
92 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
93 |
{message}
|
94 |
</div>
|
95 |
-
<div style="
|
96 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
97 |
</div>
|
98 |
-
<div style="margin-top: 10px; font-size: 14px; color: #ccc;">{int(progress)}% Complete</div>
|
99 |
<style>
|
100 |
-
@keyframes pulse {{
|
101 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
102 |
</style>
|
103 |
</div>
|
104 |
"""
|
105 |
|
106 |
-
def
|
107 |
-
"""Generate HTML for the feed display."""
|
108 |
-
if is_loading:
|
109 |
-
return """
|
110 |
-
<div id="feed-container" style="
|
111 |
-
display: flex; flex-direction: column; align-items: center;
|
112 |
-
max-width: 360px; margin: 0 auto; background-color: #000; height: 640px;
|
113 |
-
border: 1px solid #333; border-radius: 10px; color: white;">
|
114 |
-
</div>
|
115 |
-
"""
|
116 |
-
if not feed_items or current_index >= len(feed_items):
|
117 |
-
return """
|
118 |
-
<div style="color: white; text-align: center; max-width: 360px; margin: 0 auto;">
|
119 |
-
Enter a concept or idea to start your feed!
|
120 |
-
</div>
|
121 |
-
"""
|
122 |
-
item = feed_items[current_index]
|
123 |
-
media_element = f"""
|
124 |
-
<img id="feed-image" src="data:image/png;base64,{item['image_base64']}" style="
|
125 |
-
width: 100%; height: 100%; object-fit: cover; position: absolute; top: 0; left: 0; z-index: 1;">
|
126 |
"""
|
127 |
-
|
128 |
-
|
129 |
-
<video id="feed-video" controls style="
|
130 |
-
width: 100%; height: 100%; object-fit: cover; position: absolute; top: 0; left: 0; z-index: 1;">
|
131 |
-
<source src="data:video/mp4;base64,{item['video_base64']}" type="video/mp4">
|
132 |
-
Your browser does not support the video tag.
|
133 |
-
</video>
|
134 |
-
"""
|
135 |
-
return f"""
|
136 |
-
<div id="feed-container" style="
|
137 |
-
display: flex; flex-direction: column; align-items: center;
|
138 |
-
max-width: 360px; margin: 0 auto; background-color: #000; height: 640px;
|
139 |
-
border: 1px solid #333; border-radius: 10px; position: relative;">
|
140 |
-
<div class="feed-item" style="
|
141 |
-
width: 100%; height: 100%; position: relative; display: flex;
|
142 |
-
flex-direction: column; justify-content: flex-end; overflow: hidden; cursor: pointer;"
|
143 |
-
onclick="handleClick(event)">
|
144 |
-
{media_element}
|
145 |
-
<div style="
|
146 |
-
position: relative; z-index: 2; background: linear-gradient(to top, rgba(0,0,0,0.7), transparent);
|
147 |
-
padding: 20px; color: white; font-family: Arial, sans-serif; font-size: 18px; font-weight: bold;">
|
148 |
-
{item['text']}
|
149 |
-
</div>
|
150 |
-
</div>
|
151 |
-
</div>
|
152 |
-
<script>
|
153 |
-
function handleClick(event) {{
|
154 |
-
const media = document.getElementById('feed-video') || document.getElementById('feed-image');
|
155 |
-
const rect = media.getBoundingClientRect();
|
156 |
-
const clickX = event.clientX - rect.left;
|
157 |
-
const width = rect.width;
|
158 |
-
if (clickX > width * 0.75) {{
|
159 |
-
document.getElementById('previous-button').click();
|
160 |
-
}}
|
161 |
-
}}
|
162 |
-
</script>
|
163 |
"""
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
164 |
|
165 |
-
def
|
166 |
-
"""Generate share links for social media platforms."""
|
167 |
-
image_data_url = f"data:image/png;base64,{image_base64}"
|
168 |
-
encoded_caption = urllib.parse.quote(caption)
|
169 |
-
download_links = f"""
|
170 |
-
<p style="text-align: center; margin-bottom: 10px;">Download the media to share:</p>
|
171 |
-
<div style="display: flex; flex-wrap: wrap; justify-content: center; gap: 8px; margin-bottom: 15px;">
|
172 |
-
<a href="{image_data_url}" download="feed_item.png" style="
|
173 |
-
background-color: #4CAF50; color: white; padding: 8px 16px; border-radius: 5px;
|
174 |
-
text-decoration: none; font-size: 14px; font-weight: bold;">Download Image</a>
|
175 |
-
"""
|
176 |
-
if video_base64:
|
177 |
-
video_data_url = f"data:video/mp4;base64,{video_base64}"
|
178 |
-
download_links += f"""
|
179 |
-
<a href="{video_data_url}" download="feed_video.mp4" style="
|
180 |
-
background-color: #4CAF50; color: white; padding: 8px 16px; border-radius: 5px;
|
181 |
-
text-decoration: none; font-size: 14px; font-weight: bold;">Download Video</a>
|
182 |
-
"""
|
183 |
-
download_links += "</div>"
|
184 |
-
instruction = """
|
185 |
-
<p style="text-align: center; margin-bottom: 10px;">
|
186 |
-
Click a share button below to start a post with the caption, then manually upload the downloaded media.
|
187 |
-
</p>
|
188 |
"""
|
189 |
-
|
190 |
-
|
191 |
-
<a href="https://studio.youtube.com/channel/UC/videos/upload?description={encoded_caption}" target="_blank" style="
|
192 |
-
background-color: #ff0000; color: white; padding: 8px 16px; border-radius: 5px;
|
193 |
-
text-decoration: none; font-size: 14px; font-weight: bold;">Share to YouTube as a Short</a>
|
194 |
-
</div>
|
195 |
"""
|
196 |
-
|
197 |
-
|
198 |
-
|
199 |
-
{instruction}
|
200 |
-
{share_links}
|
201 |
-
</div>
|
202 |
-
"""
|
203 |
-
|
204 |
-
def start_feed(user_input, generate_video, current_index, feed_items):
|
205 |
-
"""Start or update the feed based on user input."""
|
206 |
-
user_input = user_input.strip() or "trending"
|
207 |
current_user_input = user_input
|
208 |
is_loading = True
|
209 |
progress_html = generate_progress_html(0, f"Getting started with {user_input}... 🚀", user_input)
|
210 |
-
|
211 |
|
212 |
try:
|
|
|
213 |
ideas_gen = generate_ideas(user_input)
|
214 |
-
ideas =
|
215 |
-
for
|
216 |
-
|
217 |
-
|
218 |
-
|
219 |
-
|
220 |
-
|
221 |
-
|
222 |
-
|
223 |
-
|
224 |
-
|
225 |
-
|
226 |
-
item_gen = generate_item(user_input, ideas, generate_video)
|
227 |
-
item = None
|
228 |
-
for update in item_gen:
|
229 |
-
if isinstance(update, tuple):
|
230 |
-
progress, message = update
|
231 |
-
progress_html = generate_progress_html(progress, message, user_input)
|
232 |
-
yield (gr.update(), gr.update(), gr.update(), gr.update(), gr.update(), is_loading, progress_html)
|
233 |
-
else:
|
234 |
-
item = update # Final item dictionary
|
235 |
-
|
236 |
-
if item is None or not isinstance(item, dict):
|
237 |
-
raise ValueError("Failed to generate item")
|
238 |
-
|
239 |
feed_items = [item]
|
240 |
current_index = 0
|
241 |
feed_html = generate_html(feed_items, False, current_index, user_input, is_loading=False)
|
242 |
-
|
243 |
-
|
|
|
|
|
|
|
244 |
except Exception as e:
|
245 |
print(f"Error in start_feed: {e}")
|
246 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
247 |
progress_html = generate_progress_html(100, "Oops, something went wrong! 😅", user_input)
|
248 |
-
|
|
|
|
|
|
|
|
|
|
|
249 |
|
250 |
def load_next(user_input, generate_video, current_index, feed_items):
|
251 |
-
"""
|
252 |
-
|
|
|
|
|
|
|
253 |
user_input = current_user_input
|
254 |
is_loading = True
|
255 |
progress_html = generate_progress_html(0, f"Loading next {user_input} vibe... 🚀", user_input)
|
256 |
-
|
257 |
|
258 |
try:
|
259 |
if current_index + 1 < len(feed_items):
|
260 |
current_index += 1
|
261 |
progress_html = generate_progress_html(50, f"Switching to the next {user_input} moment... 🔄", user_input)
|
262 |
-
yield (
|
263 |
else:
|
264 |
ideas = feed_items[-1]['ideas'] if feed_items else None
|
265 |
if not ideas:
|
266 |
ideas_gen = generate_ideas(user_input)
|
267 |
-
|
268 |
-
|
269 |
-
progress, message = update
|
270 |
-
progress_html = generate_progress_html(progress, message, user_input)
|
271 |
-
yield (gr.update(), gr.update(), gr.update(), gr.update(), gr.update(), is_loading, progress_html)
|
272 |
-
else:
|
273 |
-
ideas = update
|
274 |
-
|
275 |
-
if ideas is None or not isinstance(ideas, list):
|
276 |
-
raise ValueError("Failed to generate ideas")
|
277 |
-
|
278 |
-
item_gen = generate_item(user_input, ideas, generate_video)
|
279 |
-
for update in item_gen:
|
280 |
-
if isinstance(update, tuple):
|
281 |
-
progress, message = update
|
282 |
progress_html = generate_progress_html(progress, message, user_input)
|
283 |
-
yield (
|
284 |
-
|
285 |
-
|
286 |
-
|
287 |
-
|
288 |
-
|
289 |
-
|
290 |
-
|
291 |
-
|
|
|
|
|
292 |
feed_html = generate_html(feed_items, False, current_index, user_input, is_loading=False)
|
293 |
-
|
294 |
-
|
|
|
|
|
|
|
295 |
except Exception as e:
|
296 |
print(f"Error in load_next: {e}")
|
297 |
-
feed_html = "
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
298 |
progress_html = generate_progress_html(100, "Oops, something went wrong! 😅", user_input)
|
299 |
-
|
|
|
|
|
|
|
|
|
|
|
300 |
|
301 |
def load_previous(user_input, generate_video, current_index, feed_items):
|
302 |
-
"""
|
303 |
-
|
|
|
|
|
|
|
304 |
if current_index > 0:
|
305 |
current_index -= 1
|
306 |
-
feed_html = generate_html(feed_items, False, current_index,
|
307 |
-
|
308 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
309 |
|
310 |
# Define the Gradio interface
|
311 |
with gr.Blocks(
|
@@ -320,14 +862,12 @@ with gr.Blocks(
|
|
320 |
""",
|
321 |
title="Create Your Feed"
|
322 |
) as demo:
|
323 |
-
# State variables
|
324 |
current_user_input = gr.State(value="")
|
325 |
current_index = gr.State(value=0)
|
326 |
feed_items = gr.State(value=[])
|
327 |
is_loading = gr.State(value=False)
|
328 |
share_links = gr.State(value="")
|
329 |
|
330 |
-
# Input section
|
331 |
with gr.Column(elem_classes="gr-form"):
|
332 |
gr.Markdown("### Create Your Feed")
|
333 |
user_input = gr.Textbox(
|
@@ -342,12 +882,10 @@ with gr.Blocks(
|
|
342 |
)
|
343 |
magic_button = gr.Button("✨ Generate Next Item", elem_classes="gr-button")
|
344 |
|
345 |
-
# Output display
|
346 |
progress_html = gr.HTML(label="Progress", visible=True)
|
347 |
feed_html = gr.HTML()
|
348 |
share_html = gr.HTML(label="Share this item:")
|
349 |
|
350 |
-
# Event handlers
|
351 |
user_input.submit(
|
352 |
fn=start_feed,
|
353 |
inputs=[user_input, generate_video_checkbox, current_index, feed_items],
|
@@ -361,11 +899,12 @@ with gr.Blocks(
|
|
361 |
)
|
362 |
|
363 |
previous_button = gr.Button("Previous", elem_id="previous-button", visible=False)
|
|
|
364 |
previous_button.click(
|
365 |
fn=load_previous,
|
366 |
inputs=[user_input, generate_video_checkbox, current_index, feed_items],
|
367 |
outputs=[current_user_input, current_index, feed_items, feed_html, share_html, is_loading, progress_html]
|
368 |
)
|
369 |
|
370 |
-
# Launch the app
|
371 |
-
demo.launch(
|
|
|
1 |
import gradio as gr
|
2 |
+
from google import genai
|
3 |
+
from google.genai import types
|
4 |
from PIL import Image
|
5 |
from io import BytesIO
|
6 |
import base64
|
7 |
+
import os
|
8 |
+
import json
|
9 |
import random
|
10 |
import urllib.parse
|
11 |
import time
|
|
|
16 |
if current_version < required_version:
|
17 |
raise ValueError(f"Gradio version {current_version} is outdated. Please upgrade to {required_version} or later using 'pip install gradio=={required_version}'.")
|
18 |
|
19 |
+
# Initialize the Google Generative AI client with the API key from environment variables
|
20 |
+
try:
|
21 |
+
api_key = os.environ['GEMINI_API_KEY']
|
22 |
+
except KeyError:
|
23 |
+
raise ValueError("Please set the GEMINI_API_KEY environment variable.")
|
24 |
+
client = genai.Client(api_key=api_key)
|
25 |
+
|
26 |
+
# Define safety settings to disable all filters for content generation
|
27 |
+
SAFETY_SETTINGS = [
|
28 |
+
types.SafetySetting(
|
29 |
+
category=types.HarmCategory.HARM_CATEGORY_HARASSMENT,
|
30 |
+
threshold=types.HarmBlockThreshold.BLOCK_NONE,
|
31 |
+
),
|
32 |
+
types.SafetySetting(
|
33 |
+
category=types.HarmCategory.HARM_CATEGORY_HATE_SPEECH,
|
34 |
+
threshold=types.HarmBlockThreshold.BLOCK_NONE,
|
35 |
+
),
|
36 |
+
types.SafetySetting(
|
37 |
+
category=types.HarmCategory.HARM_CATEGORY_SEXUALLY_EXPLICIT,
|
38 |
+
threshold=types.HarmBlockThreshold.BLOCK_NONE,
|
39 |
+
),
|
40 |
+
types.SafetySetting(
|
41 |
+
category=types.HarmCategory.HARM_CATEGORY_DANGEROUS_CONTENT,
|
42 |
+
threshold=types.HarmBlockThreshold.BLOCK_NONE,
|
43 |
+
),
|
44 |
+
types.SafetySetting(
|
45 |
+
category=types.HarmCategory.HARM_CATEGORY_CIVIC_INTEGRITY,
|
46 |
+
threshold=types.HarmBlockThreshold.BLOCK_NONE,
|
47 |
+
),
|
48 |
+
]
|
49 |
+
|
50 |
def clean_response_text(response_text):
|
51 |
+
"""
|
52 |
+
Clean the API response by removing Markdown code block markers.
|
53 |
+
"""
|
54 |
cleaned_text = response_text.strip()
|
55 |
if cleaned_text.startswith("```json"):
|
56 |
cleaned_text = cleaned_text[len("```json"):].strip()
|
|
|
59 |
return cleaned_text
|
60 |
|
61 |
def generate_ideas(user_input):
|
62 |
+
"""
|
63 |
+
Generate a diverse set of ideas based on the user's input concept using the LLM.
|
64 |
+
Yields progress updates for the loading UI.
|
65 |
+
"""
|
66 |
yield (10, f"Brainstorming epic ideas for {user_input}... 🌟")
|
67 |
+
|
68 |
+
prompt = f"""
|
69 |
+
The user has provided the concept: "{user_input}". You must generate 5 diverse and creative ideas for a TikTok video that are directly and explicitly related to "{user_input}".
|
70 |
+
Each idea must clearly incorporate and focus on the core theme of "{user_input}" without deviating into unrelated topics.
|
71 |
+
Each idea should be a short sentence describing a specific scene or concept.
|
72 |
+
Return the response as a JSON object with a single key 'ideas' containing a list of 5 ideas.
|
73 |
+
Ensure the response is strictly in JSON format.
|
74 |
+
"""
|
75 |
try:
|
76 |
+
response = client.models.generate_content(
|
77 |
+
model='gemini-2.0-flash-lite',
|
78 |
+
contents=[prompt],
|
79 |
+
config=types.GenerateContentConfig(
|
80 |
+
temperature=1.2,
|
81 |
+
safety_settings=SAFETY_SETTINGS
|
82 |
+
)
|
83 |
+
)
|
84 |
+
if not response.text or response.text.isspace():
|
85 |
+
raise ValueError("Empty response from API")
|
86 |
+
cleaned_text = clean_response_text(response.text)
|
87 |
+
response_json = json.loads(cleaned_text)
|
88 |
+
if 'ideas' not in response_json or not isinstance(response_json['ideas'], list) or len(response_json['ideas']) != 5:
|
89 |
+
raise ValueError("Invalid JSON format: 'ideas' key missing, not a list, or incorrect length")
|
90 |
+
|
91 |
+
ideas = response_json['ideas']
|
92 |
yield (20, f"Ideas locked in for {user_input}! 🚀")
|
93 |
+
return ideas
|
94 |
+
|
95 |
except Exception as e:
|
96 |
print(f"Error generating ideas: {e}")
|
97 |
yield (20, f"Oops, tweaking the plan for {user_input}... 🔧")
|
98 |
+
return [
|
99 |
f"A dramatic {user_input} scene with cinematic lighting",
|
100 |
f"A close-up of {user_input} in a futuristic setting",
|
101 |
+
f"A high-energy {user_input} moment with vibrant colors",
|
102 |
+
f"A serene {user_input} scene with soft focus",
|
103 |
+
f"An action-packed {user_input} challenge with dynamic angles"
|
104 |
]
|
|
|
105 |
|
106 |
+
def generate_item(user_input, ideas, generate_video=False, max_retries=3):
|
107 |
+
"""
|
108 |
+
Generate a single feed item (image and optionally one video) using one of the ideas.
|
109 |
+
Yields progress updates for the loading UI.
|
110 |
+
"""
|
111 |
+
video_base64 = None
|
112 |
+
max_total_attempts = 3
|
113 |
+
|
114 |
+
total_attempts = 0
|
115 |
+
while total_attempts < max_total_attempts:
|
116 |
+
total_attempts += 1
|
117 |
+
yield (20 + total_attempts * 10, f"Attempt {total_attempts} to craft your {user_input} masterpiece... 🎨")
|
118 |
+
|
119 |
+
generated_image = None
|
120 |
+
text = None
|
121 |
+
img_str = None
|
122 |
+
image_prompt = None
|
123 |
+
|
124 |
+
for image_attempt in range(max_retries):
|
125 |
+
yield (20 + total_attempts * 10 + image_attempt * 5, f"Crafting a stunning image for {user_input}... 📸")
|
126 |
+
selected_idea = random.choice(ideas)
|
127 |
+
prompt = f"""
|
128 |
+
The user has provided the concept: "{user_input}". Based on this concept and the specific idea "{selected_idea}", create content for a TikTok video.
|
129 |
+
Return a JSON object with two keys:
|
130 |
+
- 'caption': A short, viral TikTok-style caption with hashtags that reflects "{user_input}".
|
131 |
+
- 'image_prompt': A detailed image prompt for generating a high-quality visual scene, ensuring the theme of "{user_input}" is central.
|
132 |
+
The image prompt should describe the scene vividly, specify a perspective and style, and ensure no text or letters are included.
|
133 |
+
Ensure the response is strictly in JSON format.
|
134 |
+
"""
|
135 |
+
try:
|
136 |
+
response = client.models.generate_content(
|
137 |
+
model='gemini-2.0-flash-lite',
|
138 |
+
contents=[prompt],
|
139 |
+
config=types.GenerateContentConfig(
|
140 |
+
temperature=1.2,
|
141 |
+
safety_settings=SAFETY_SETTINGS
|
142 |
+
)
|
143 |
+
)
|
144 |
+
if not response.text or response.text.isspace():
|
145 |
+
raise ValueError("Empty response from API")
|
146 |
+
cleaned_text = clean_response_text(response.text)
|
147 |
+
response_json = json.loads(cleaned_text)
|
148 |
+
if 'caption' not in response_json or 'image_prompt' not in response_json:
|
149 |
+
raise ValueError("Invalid JSON format: 'caption' or 'image_prompt' key missing")
|
150 |
+
text = response_json['caption']
|
151 |
+
image_prompt = response_json['image_prompt']
|
152 |
+
except Exception as e:
|
153 |
+
print(f"Error generating item (image attempt {image_attempt + 1}): {e}")
|
154 |
+
text = f"Amazing {user_input}! 🔥 #{user_input.replace(' ', '')}"
|
155 |
+
image_prompt = f"A vivid scene of {selected_idea} related to {user_input}, in a vibrant pop art style, no text or letters"
|
156 |
+
|
157 |
+
try:
|
158 |
+
yield (40 + image_attempt * 5, f"Rendering your {user_input} vision... ✨")
|
159 |
+
imagen = client.models.generate_images(
|
160 |
+
model='imagen-3.0-generate-002',
|
161 |
+
prompt=image_prompt,
|
162 |
+
config=types.GenerateImagesConfig(
|
163 |
+
aspect_ratio="9:16",
|
164 |
+
number_of_images=1
|
165 |
+
)
|
166 |
+
)
|
167 |
+
if imagen.generated_images and len(imagen.generated_images) > 0:
|
168 |
+
generated_image = imagen.generated_images[0]
|
169 |
+
image = Image.open(BytesIO(generated_image.image.image_bytes))
|
170 |
+
target_width = 360
|
171 |
+
target_height = int(target_width / 9 * 16)
|
172 |
+
image = image.resize((target_width, target_height), Image.LANCZOS)
|
173 |
+
buffered = BytesIO()
|
174 |
+
image.save(buffered, format="PNG")
|
175 |
+
img_str = base64.b64encode(buffered.getvalue()).decode()
|
176 |
+
yield (50, f"Image for {user_input} is ready! 🎉")
|
177 |
+
break
|
178 |
+
else:
|
179 |
+
if image_attempt == max_retries - 1:
|
180 |
+
yield (50, f"Tweaking the image for {user_input}... 🔄")
|
181 |
+
if total_attempts == max_total_attempts:
|
182 |
+
image = Image.new('RGB', (360, 640), color='gray')
|
183 |
+
buffered = BytesIO()
|
184 |
+
image.save(buffered, format="PNG")
|
185 |
+
img_str = base64.b64encode(buffered.getvalue()).decode()
|
186 |
+
yield (60, f"Using a placeholder for {user_input}... 🖼️")
|
187 |
+
return {
|
188 |
+
'text': text,
|
189 |
+
'image_base64': img_str,
|
190 |
+
'video_base64': None,
|
191 |
+
'ideas': ideas
|
192 |
+
}
|
193 |
+
break
|
194 |
+
except Exception as e:
|
195 |
+
print(f"Error generating image (image attempt {image_attempt + 1}): {e}")
|
196 |
+
if image_attempt == max_retries - 1:
|
197 |
+
yield (50, f"Retrying image for {user_input}... 🔄")
|
198 |
+
if total_attempts == max_total_attempts:
|
199 |
+
image = Image.new('RGB', (360, 640), color='gray')
|
200 |
+
buffered = BytesIO()
|
201 |
+
image.save(buffered, format="PNG")
|
202 |
+
img_str = base64.b64encode(buffered.getvalue()).decode()
|
203 |
+
yield (60, f"Using a placeholder for {user_input}... 🖼️")
|
204 |
+
return {
|
205 |
+
'text': text,
|
206 |
+
'image_base64': img_str,
|
207 |
+
'video_base64': None,
|
208 |
+
'ideas': ideas
|
209 |
+
}
|
210 |
+
break
|
211 |
+
|
212 |
+
if generate_video and generated_image is not None:
|
213 |
+
max_video_retries_per_image = 2
|
214 |
+
video_generated = False
|
215 |
+
|
216 |
+
try:
|
217 |
+
yield (60, f"Filming a viral video for {user_input}... 🎥")
|
218 |
+
video_prompt = f"""
|
219 |
+
The user concept is "{user_input}". Based on this and the scene: {image_prompt}, create a video.
|
220 |
+
Use a close-up shot with a slow dolly shot circling around the subject,
|
221 |
+
using shallow focus on the main subject to emphasize details, in a realistic style with cinematic lighting.
|
222 |
+
"""
|
223 |
+
operation = client.models.generate_videos(
|
224 |
+
model="veo-2.0-generate-001",
|
225 |
+
prompt=video_prompt,
|
226 |
+
image=generated_image.image,
|
227 |
+
config=types.GenerateVideosConfig(
|
228 |
+
aspect_ratio="9:16",
|
229 |
+
number_of_videos=1,
|
230 |
+
duration_seconds=8,
|
231 |
+
negative_prompt="blurry, low quality, text, letters"
|
232 |
+
)
|
233 |
+
)
|
234 |
+
while not operation.done:
|
235 |
+
time.sleep(20)
|
236 |
+
operation = client.operations.get(operation)
|
237 |
+
|
238 |
+
if operation.error:
|
239 |
+
raise ValueError(f"Video generation failed: {operation.error.message}")
|
240 |
+
if operation.response is None or not hasattr(operation.response, 'generated_videos'):
|
241 |
+
raise ValueError("Video generation failed: No generated_videos in response")
|
242 |
+
|
243 |
+
if len(operation.response.generated_videos) > 0:
|
244 |
+
video = operation.response.generated_videos[0]
|
245 |
+
if video is None or not hasattr(video, 'video'):
|
246 |
+
raise ValueError("Video is invalid or missing video data")
|
247 |
+
video_data = client.files.download(file=video.video)
|
248 |
+
if isinstance(video_data, bytes):
|
249 |
+
video_bytes = video_data
|
250 |
+
else:
|
251 |
+
video_buffer = BytesIO()
|
252 |
+
for chunk in video_data:
|
253 |
+
video_buffer.write(chunk)
|
254 |
+
video_bytes = video_buffer.getvalue()
|
255 |
+
video_base64 = base64.b64encode(video_bytes).decode()
|
256 |
+
video_generated = True
|
257 |
+
yield (90, f"Video for {user_input} is a wrap! 🎬")
|
258 |
+
return {
|
259 |
+
'text': text,
|
260 |
+
'image_base64': img_str,
|
261 |
+
'video_base64': video_base64,
|
262 |
+
'ideas': ideas
|
263 |
+
}
|
264 |
+
except Exception as e:
|
265 |
+
print(f"Error generating video (image-to-video): {e}")
|
266 |
+
yield (70, f"Switching to a new video approach for {user_input}... 🎞️")
|
267 |
+
|
268 |
+
if not video_generated:
|
269 |
+
for video_attempt in range(max_video_retries_per_image):
|
270 |
+
try:
|
271 |
+
yield (75 + video_attempt * 5, f"Trying a fresh video take for {user_input}... 📹")
|
272 |
+
video_prompt_base = f"""
|
273 |
+
The user concept is "{user_input}". Based on this and the scene: {image_prompt}, create a video.
|
274 |
+
Use a close-up shot with a slow dolly shot circling around the subject,
|
275 |
+
using shallow focus on the main subject to emphasize details, in a realistic style with cinematic lighting.
|
276 |
+
"""
|
277 |
+
video_prompt = video_prompt_base if video_attempt == 0 else f"""
|
278 |
+
The user concept is "{user_input}". Based on this and a simplified scene: {image_prompt}, create a video.
|
279 |
+
Use a static close-up shot of the subject in a realistic style.
|
280 |
+
"""
|
281 |
+
operation = client.models.generate_videos(
|
282 |
+
model="veo-2.0-generate-001",
|
283 |
+
prompt=video_prompt,
|
284 |
+
config=types.GenerateVideosConfig(
|
285 |
+
aspect_ratio="9:16",
|
286 |
+
number_of_videos=1,
|
287 |
+
duration_seconds=8,
|
288 |
+
negative_prompt="blurry, low quality, text, letters"
|
289 |
+
)
|
290 |
+
)
|
291 |
+
while not operation.done:
|
292 |
+
time.sleep(20)
|
293 |
+
operation = client.operations.get(operation)
|
294 |
+
|
295 |
+
if operation.error:
|
296 |
+
raise ValueError(f"Video generation failed: {operation.error.message}")
|
297 |
+
if operation.response is None or not hasattr(operation.response, 'generated_videos'):
|
298 |
+
raise ValueError("Video generation failed: No generated_videos in response")
|
299 |
+
|
300 |
+
if len(operation.response.generated_videos) > 0:
|
301 |
+
video = operation.response.generated_videos[0]
|
302 |
+
if video is None or not hasattr(video, 'video'):
|
303 |
+
raise ValueError("Video is invalid or missing video data")
|
304 |
+
video_data = client.files.download(file=video.video)
|
305 |
+
if isinstance(video_data, bytes):
|
306 |
+
video_bytes = video_data
|
307 |
+
else:
|
308 |
+
video_buffer = BytesIO()
|
309 |
+
for chunk in video_data:
|
310 |
+
video_buffer.write(chunk)
|
311 |
+
video_bytes = video_buffer.getvalue()
|
312 |
+
video_base64 = base64.b64encode(video_bytes).decode()
|
313 |
+
video_generated = True
|
314 |
+
yield (90, f"Video for {user_input} is a wrap! 🎬")
|
315 |
+
return {
|
316 |
+
'text': text,
|
317 |
+
'image_base64': img_str,
|
318 |
+
'video_base64': video_base64,
|
319 |
+
'ideas': ideas
|
320 |
+
}
|
321 |
+
except Exception as e:
|
322 |
+
print(f"Error generating video (text-to-video attempt {video_attempt + 1}): {e}")
|
323 |
+
if video_attempt == max_video_retries_per_image - 1:
|
324 |
+
yield (85, f"Finalizing without video for {user_input}... 📌")
|
325 |
+
if total_attempts == max_total_attempts:
|
326 |
+
yield (95, f"Polishing your {user_input} masterpiece... ✨")
|
327 |
+
return {
|
328 |
+
'text': text,
|
329 |
+
'image_base64': img_str,
|
330 |
+
'video_base64': video_base64,
|
331 |
+
'ideas': ideas
|
332 |
+
}
|
333 |
+
break
|
334 |
+
|
335 |
+
if img_str is not None:
|
336 |
+
yield (95, f"Polishing your {user_input} masterpiece... ✨")
|
337 |
+
return {
|
338 |
+
'text': text,
|
339 |
+
'image_base64': img_str,
|
340 |
+
'video_base64': video_base64,
|
341 |
+
'ideas': ideas
|
342 |
+
}
|
343 |
+
|
344 |
+
print("Max total attempts reached without successful image generation. Using placeholder.")
|
345 |
+
yield (95, f"Falling back to a placeholder for {user_input}... 🖼️")
|
346 |
+
image = Image.new('RGB', (360, 640), color='gray')
|
347 |
+
buffered = BytesIO()
|
348 |
+
image.save(buffered, format="PNG")
|
349 |
+
img_str = base64.b64encode(buffered.getvalue()).decode()
|
350 |
+
yield (100, f"Ready to roll with {user_input}! 🚀")
|
351 |
+
return {
|
352 |
+
'text': f"Amazing {user_input}! 🔥 #{user_input.replace(' ', '')}",
|
353 |
+
'image_base64': img_str,
|
354 |
+
'video_base64': None,
|
355 |
+
'ideas': ideas
|
356 |
+
}
|
357 |
|
358 |
def generate_progress_html(progress, message, user_input):
|
359 |
+
"""
|
360 |
+
Generate HTML for the progress bar and witty text.
|
361 |
+
"""
|
362 |
return f"""
|
363 |
<div id="progress-container" style="
|
364 |
+
display: flex;
|
365 |
+
flex-direction: column;
|
366 |
+
align-items: center;
|
367 |
+
justify-content: center;
|
368 |
+
max-width: 360px;
|
369 |
+
margin: 0 auto;
|
370 |
+
background-color: #000;
|
371 |
+
height: 200px;
|
372 |
+
border: 1px solid #333;
|
373 |
+
border-radius: 10px;
|
374 |
+
color: white;
|
375 |
+
font-family: Arial, sans-serif;
|
376 |
+
position: relative;
|
377 |
+
">
|
378 |
+
<div id="loading-message" style="
|
379 |
+
font-size: 18px;
|
380 |
+
font-weight: bold;
|
381 |
+
text-align: center;
|
382 |
+
margin-bottom: 20px;
|
383 |
+
text-shadow: 1px 1px 2px rgba(0,0,0,0.5);
|
384 |
+
">
|
385 |
{message}
|
386 |
</div>
|
387 |
+
<div style="
|
388 |
+
width: 80%;
|
389 |
+
height: 10px;
|
390 |
+
background-color: #333;
|
391 |
+
border-radius: 5px;
|
392 |
+
overflow: hidden;
|
393 |
+
">
|
394 |
+
<div id="progress-bar" style="
|
395 |
+
width: {progress}%;
|
396 |
+
height: 100%;
|
397 |
+
background: linear-gradient(to right, #ff2d55, #ff5e78);
|
398 |
+
transition: width 0.5s ease-in-out;
|
399 |
+
"></div>
|
400 |
+
</div>
|
401 |
+
<div style="
|
402 |
+
margin-top: 10px;
|
403 |
+
font-size: 14px;
|
404 |
+
color: #ccc;
|
405 |
+
">
|
406 |
+
{int(progress)}% Complete
|
407 |
</div>
|
|
|
408 |
<style>
|
409 |
+
@keyframes pulse {{
|
410 |
+
0% {{ opacity: 1; }}
|
411 |
+
50% {{ opacity: 0.5; }}
|
412 |
+
100% {{ opacity: 1; }}
|
413 |
+
}}
|
414 |
+
#loading-message {{
|
415 |
+
animation: pulse 2s infinite;
|
416 |
+
}}
|
417 |
</style>
|
418 |
</div>
|
419 |
"""
|
420 |
|
421 |
+
def process_generator(generator):
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
422 |
"""
|
423 |
+
Process a generator to collect progress updates and the final result.
|
424 |
+
Returns a tuple of (progress_updates, final_result).
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
425 |
"""
|
426 |
+
progress_updates = []
|
427 |
+
final_result = None
|
428 |
+
for item in generator:
|
429 |
+
if isinstance(item, tuple):
|
430 |
+
progress_updates.append(item)
|
431 |
+
else:
|
432 |
+
final_result = item
|
433 |
+
return progress_updates, final_result
|
434 |
|
435 |
+
def start_feed(user_input, generate_video, current_index, feed_items):
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
436 |
"""
|
437 |
+
Start or update the feed based on the user input.
|
438 |
+
Returns all outputs expected by Gradio.
|
|
|
|
|
|
|
|
|
439 |
"""
|
440 |
+
if not user_input.strip():
|
441 |
+
user_input = "trending"
|
442 |
+
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
443 |
current_user_input = user_input
|
444 |
is_loading = True
|
445 |
progress_html = generate_progress_html(0, f"Getting started with {user_input}... 🚀", user_input)
|
446 |
+
share_links = ""
|
447 |
|
448 |
try:
|
449 |
+
# Generate ideas
|
450 |
ideas_gen = generate_ideas(user_input)
|
451 |
+
progress_updates, ideas = process_generator(ideas_gen)
|
452 |
+
for progress, message in progress_updates:
|
453 |
+
progress_html = generate_progress_html(progress, message, user_input)
|
454 |
+
yield (progress_html, None, None, None, None, is_loading, progress_html)
|
455 |
+
|
456 |
+
# Generate item
|
457 |
+
item_gen = generate_item(user_input, ideas, generate_video=generate_video)
|
458 |
+
progress_updates, item = process_generator(item_gen)
|
459 |
+
for progress, message in progress_updates:
|
460 |
+
progress_html = generate_progress_html(progress, message, user_input)
|
461 |
+
yield (progress_html, None, None, None, None, is_loading, progress_html)
|
462 |
+
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
463 |
feed_items = [item]
|
464 |
current_index = 0
|
465 |
feed_html = generate_html(feed_items, False, current_index, user_input, is_loading=False)
|
466 |
+
share_links = generate_share_links(
|
467 |
+
item['image_base64'],
|
468 |
+
item['video_base64'],
|
469 |
+
item['text']
|
470 |
+
)
|
471 |
except Exception as e:
|
472 |
print(f"Error in start_feed: {e}")
|
473 |
+
feed_items = []
|
474 |
+
current_index = 0
|
475 |
+
feed_html = """
|
476 |
+
<div style="
|
477 |
+
display: flex;
|
478 |
+
flex-direction: column;
|
479 |
+
align-items: center;
|
480 |
+
justify-content: center;
|
481 |
+
max-width: 360px;
|
482 |
+
margin: 0 auto;
|
483 |
+
background-color: #000;
|
484 |
+
height: 640px;
|
485 |
+
border: 1px solid #333;
|
486 |
+
border-radius: 10px;
|
487 |
+
color: white;
|
488 |
+
font-family: Arial, sans-serif;
|
489 |
+
">
|
490 |
+
<p>Error generating content. Please try again!</p>
|
491 |
+
</div>
|
492 |
+
"""
|
493 |
progress_html = generate_progress_html(100, "Oops, something went wrong! 😅", user_input)
|
494 |
+
is_loading = False
|
495 |
+
return current_user_input, current_index, feed_items, feed_html, share_links, is_loading, progress_html
|
496 |
+
|
497 |
+
is_loading = False
|
498 |
+
progress_html = ""
|
499 |
+
return current_user_input, current_index, feed_items, feed_html, share_links, is_loading, progress_html
|
500 |
|
501 |
def load_next(user_input, generate_video, current_index, feed_items):
|
502 |
+
"""
|
503 |
+
Load the next item in the feed.
|
504 |
+
Returns all outputs expected by Gradio.
|
505 |
+
"""
|
506 |
+
current_user_input = user_input if user_input.strip() else "trending"
|
507 |
user_input = current_user_input
|
508 |
is_loading = True
|
509 |
progress_html = generate_progress_html(0, f"Loading next {user_input} vibe... 🚀", user_input)
|
510 |
+
share_links = ""
|
511 |
|
512 |
try:
|
513 |
if current_index + 1 < len(feed_items):
|
514 |
current_index += 1
|
515 |
progress_html = generate_progress_html(50, f"Switching to the next {user_input} moment... 🔄", user_input)
|
516 |
+
yield (progress_html, None, None, None, None, is_loading, progress_html)
|
517 |
else:
|
518 |
ideas = feed_items[-1]['ideas'] if feed_items else None
|
519 |
if not ideas:
|
520 |
ideas_gen = generate_ideas(user_input)
|
521 |
+
progress_updates, ideas = process_generator(ideas_gen)
|
522 |
+
for progress, message in progress_updates:
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
523 |
progress_html = generate_progress_html(progress, message, user_input)
|
524 |
+
yield (progress_html, None, None, None, None, is_loading, progress_html)
|
525 |
+
|
526 |
+
new_item_gen = generate_item(user_input, ideas, generate_video=generate_video)
|
527 |
+
progress_updates, new_item = process_generator(new_item_gen)
|
528 |
+
for progress, message in progress_updates:
|
529 |
+
progress_html = generate_progress_html(progress, message, user_input)
|
530 |
+
yield (progress_html, None, None, None, None, is_loading, progress_html)
|
531 |
+
|
532 |
+
feed_items.append(new_item)
|
533 |
+
current_index = len(feed_items) - 1
|
534 |
+
|
535 |
feed_html = generate_html(feed_items, False, current_index, user_input, is_loading=False)
|
536 |
+
share_links = generate_share_links(
|
537 |
+
feed_items[current_index]['image_base64'],
|
538 |
+
feed_items[current_index]['video_base64'],
|
539 |
+
feed_items[current_index]['text']
|
540 |
+
)
|
541 |
except Exception as e:
|
542 |
print(f"Error in load_next: {e}")
|
543 |
+
feed_html = """
|
544 |
+
<div style="
|
545 |
+
display: flex;
|
546 |
+
flex-direction: column;
|
547 |
+
align-items: center;
|
548 |
+
justify-content: center;
|
549 |
+
max-width: 360px;
|
550 |
+
margin: 0 auto;
|
551 |
+
background-color: #000;
|
552 |
+
height: 640px;
|
553 |
+
border: 1px solid #333;
|
554 |
+
border-radius: 10px;
|
555 |
+
color: white;
|
556 |
+
font-family: Arial, sans-serif;
|
557 |
+
">
|
558 |
+
<p>Error generating content. Please try again!</p>
|
559 |
+
</div>
|
560 |
+
"""
|
561 |
progress_html = generate_progress_html(100, "Oops, something went wrong! 😅", user_input)
|
562 |
+
is_loading = False
|
563 |
+
return current_user_input, current_index, feed_items, feed_html, share_links, is_loading, progress_html
|
564 |
+
|
565 |
+
is_loading = False
|
566 |
+
progress_html = ""
|
567 |
+
return current_user_input, current_index, feed_items, feed_html, share_links, is_loading, progress_html
|
568 |
|
569 |
def load_previous(user_input, generate_video, current_index, feed_items):
|
570 |
+
"""
|
571 |
+
Load the previous item in the feed.
|
572 |
+
"""
|
573 |
+
current_user_input = user_input if user_input.strip() else "trending"
|
574 |
+
|
575 |
if current_index > 0:
|
576 |
current_index -= 1
|
577 |
+
feed_html = generate_html(feed_items, False, current_index, user_input, is_loading=False)
|
578 |
+
share_links = generate_share_links(
|
579 |
+
feed_items[current_index]['image_base64'],
|
580 |
+
feed_items[current_index]['video_base64'],
|
581 |
+
feed_items[current_index]['text']
|
582 |
+
)
|
583 |
+
return current_user_input, current_index, feed_items, feed_html, share_links, False, ""
|
584 |
+
|
585 |
+
def generate_share_links(image_base64, video_base64, caption):
|
586 |
+
"""
|
587 |
+
Generate share links for social media platforms with download links for image and video.
|
588 |
+
"""
|
589 |
+
image_data_url = f"data:image/png;base64,{image_base64}"
|
590 |
+
encoded_caption = urllib.parse.quote(caption)
|
591 |
+
|
592 |
+
download_links = f"""
|
593 |
+
<p style="text-align: center; margin-bottom: 10px;">Download the media to share:</p>
|
594 |
+
<div style="display: flex; flex-wrap: wrap; justify-content: center; gap: 8px; margin-bottom: 15px;">
|
595 |
+
<a href="{image_data_url}" download="feed_item.png" style="
|
596 |
+
background-color: #4CAF50;
|
597 |
+
color: white;
|
598 |
+
padding: 8px 16px;
|
599 |
+
border-radius: 5px;
|
600 |
+
text-decoration: none;
|
601 |
+
font-size: 14px;
|
602 |
+
font-weight: bold;
|
603 |
+
transition: background-color 0.3s;
|
604 |
+
" onmouseover="this.style.backgroundColor='#45a049'" onmouseout="this.style.backgroundColor='#4CAF50'">Download Image</a>
|
605 |
+
"""
|
606 |
+
if video_base64:
|
607 |
+
video_data_url = f"data:video/mp4;base64,{video_base64}"
|
608 |
+
download_links += f"""
|
609 |
+
<a href="{video_data_url}" download="feed_video.mp4" style="
|
610 |
+
background-color: #4CAF50;
|
611 |
+
color: white;
|
612 |
+
padding: 8px 16px;
|
613 |
+
border-radius: 5px;
|
614 |
+
text-decoration: none;
|
615 |
+
font-size: 14px;
|
616 |
+
font-weight: bold;
|
617 |
+
transition: background-color 0.3s;
|
618 |
+
" onmouseover="this.style.backgroundColor='#45a049'" onmouseout="this.style.backgroundColor='#4CAF50'">Download Video</a>
|
619 |
+
"""
|
620 |
+
download_links += "</div>"
|
621 |
+
|
622 |
+
instruction = """
|
623 |
+
<p style="text-align: center; margin-bottom: 10px;">
|
624 |
+
Click a share button below to start a post with the caption, then manually upload the downloaded image or video.
|
625 |
+
</p>
|
626 |
+
"""
|
627 |
+
|
628 |
+
share_links = """
|
629 |
+
<div style="
|
630 |
+
display: flex;
|
631 |
+
flex-wrap: wrap;
|
632 |
+
justify-content: center;
|
633 |
+
gap: 8px;
|
634 |
+
margin-bottom: 15px;
|
635 |
+
">
|
636 |
+
<a href="https://www.tiktok.com/upload?caption={caption}" target="_blank" style="
|
637 |
+
background-color: #00f2ea;
|
638 |
+
color: #000;
|
639 |
+
padding: 8px 16px;
|
640 |
+
border-radius: 5px;
|
641 |
+
text-decoration: none;
|
642 |
+
font-size: 14px;
|
643 |
+
font-weight: bold;
|
644 |
+
transition: background-color 0.3s;
|
645 |
+
" onmouseover="this.style.backgroundColor='#00d9d1'" onmouseout="this.style.backgroundColor='#00f2ea'">Share on TikTok</a>
|
646 |
+
<a href="https://www.instagram.com/?caption={caption}" target="_blank" style="
|
647 |
+
background-color: #e1306c;
|
648 |
+
color: white;
|
649 |
+
padding: 8px 16px;
|
650 |
+
border-radius: 5px;
|
651 |
+
text-decoration: none;
|
652 |
+
font-size: 14px;
|
653 |
+
font-weight: bold;
|
654 |
+
transition: background-color 0.3s;
|
655 |
+
" onmouseover="this.style.backgroundColor='#c72b5e'" onmouseout="this.style.backgroundColor='#e1306c'">Share on Instagram</a>
|
656 |
+
<a href="https://www.facebook.com/sharer/sharer.php?quote={caption}" target="_blank" style="
|
657 |
+
background-color: #4267b2;
|
658 |
+
color: white;
|
659 |
+
padding: 8px 16px;
|
660 |
+
border-radius: 5px;
|
661 |
+
text-decoration: none;
|
662 |
+
font-size: 14px;
|
663 |
+
font-weight: bold;
|
664 |
+
transition: background-color 0.3s;
|
665 |
+
" onmouseover="this.style.backgroundColor='#395a9d'" onmouseout="this.style.backgroundColor='#4267b2'">Share on Facebook</a>
|
666 |
+
<a href="https://twitter.com/intent/tweet?text={caption}" target="_blank" style="
|
667 |
+
background-color: #1da1f2;
|
668 |
+
color: white;
|
669 |
+
padding: 8px 16px;
|
670 |
+
border-radius: 5px;
|
671 |
+
text-decoration: none;
|
672 |
+
font-size: 14px;
|
673 |
+
font-weight: bold;
|
674 |
+
transition: background-color 0.3s;
|
675 |
+
" onmouseover="this.style.backgroundColor='#1a91da'" onmouseout="this.style.backgroundColor='#1da1f2'">Share on X</a>
|
676 |
+
<a href="https://pinterest.com/pin/create/button/?description={caption}" target="_blank" style="
|
677 |
+
background-color: #bd081c;
|
678 |
+
color: white;
|
679 |
+
padding: 8px 16px;
|
680 |
+
border-radius: 5px;
|
681 |
+
text-decoration: none;
|
682 |
+
font-size: 14px;
|
683 |
+
font-weight: bold;
|
684 |
+
transition: background-color 0.3s;
|
685 |
+
" onmouseover="this.style.backgroundColor='#a30718'" onmouseout="this.style.backgroundColor='#bd081c'">Share on Pinterest</a>
|
686 |
+
</div>
|
687 |
+
"""
|
688 |
+
|
689 |
+
youtube_share = ""
|
690 |
+
if video_base64:
|
691 |
+
youtube_share = f"""
|
692 |
+
<div style="
|
693 |
+
display: flex;
|
694 |
+
justify-content: center;
|
695 |
+
margin-top: 10px;
|
696 |
+
">
|
697 |
+
<a href="https://studio.youtube.com/channel/UC/videos/upload?description={caption}" target="_blank" style="
|
698 |
+
background-color: #ff0000;
|
699 |
+
color: white;
|
700 |
+
padding: 8px 16px;
|
701 |
+
border-radius: 5px;
|
702 |
+
text-decoration: none;
|
703 |
+
font-size: 14px;
|
704 |
+
font-weight: bold;
|
705 |
+
transition: background-color 0.3s;
|
706 |
+
" onmouseover="this.style.backgroundColor='#e60000'" onmouseout="this.style.backgroundColor='#ff0000'">Share to YouTube as a Short</a>
|
707 |
+
</div>
|
708 |
+
"""
|
709 |
+
|
710 |
+
return f"""
|
711 |
+
<div style="
|
712 |
+
display: flex;
|
713 |
+
flex-direction: column;
|
714 |
+
align-items: center;
|
715 |
+
gap: 10px;
|
716 |
+
margin-top: 10px;
|
717 |
+
color: white;
|
718 |
+
font-family: Arial, sans-serif;
|
719 |
+
">
|
720 |
+
{download_links}
|
721 |
+
{instruction}
|
722 |
+
{share_links}
|
723 |
+
{youtube_share}
|
724 |
+
</div>
|
725 |
+
""".format(caption=encoded_caption)
|
726 |
+
|
727 |
+
def generate_html(feed_items, scroll_to_latest=False, current_index=0, user_input="", is_loading=False, progress=0, message=""):
|
728 |
+
"""
|
729 |
+
Generate an HTML string to display the current feed item or loading state.
|
730 |
+
"""
|
731 |
+
if is_loading:
|
732 |
+
return """
|
733 |
+
<div id="feed-container" style="
|
734 |
+
display: flex;
|
735 |
+
flex-direction: column;
|
736 |
+
align-items: center;
|
737 |
+
max-width: 360px;
|
738 |
+
margin: 0 auto;
|
739 |
+
background-color: #000;
|
740 |
+
height: 640px;
|
741 |
+
border: 1px solid #333;
|
742 |
+
border-radius: 10px;
|
743 |
+
color: white;
|
744 |
+
font-family: Arial, sans-serif;
|
745 |
+
">
|
746 |
+
</div>
|
747 |
+
"""
|
748 |
+
|
749 |
+
if not feed_items or current_index >= len(feed_items):
|
750 |
+
return """
|
751 |
+
<div id="feed-container" style="
|
752 |
+
display: flex;
|
753 |
+
flex-direction: column;
|
754 |
+
align-items: center;
|
755 |
+
justify-content: center;
|
756 |
+
max-width: 360px;
|
757 |
+
margin: 0 auto;
|
758 |
+
background-color: #000;
|
759 |
+
height: 640px;
|
760 |
+
border: 1px solid #333;
|
761 |
+
border-radius: 10px;
|
762 |
+
color: white;
|
763 |
+
font-family: Arial, sans-serif;
|
764 |
+
">
|
765 |
+
<p>Enter a concept or idea to start your feed!</p>
|
766 |
+
</div>
|
767 |
+
"""
|
768 |
+
|
769 |
+
item = feed_items[current_index]
|
770 |
+
if item['video_base64']:
|
771 |
+
media_element = f"""
|
772 |
+
<video id="feed-video" controls style="
|
773 |
+
width: 100%;
|
774 |
+
height: 100%;
|
775 |
+
object-fit: cover;
|
776 |
+
position: absolute;
|
777 |
+
top: 0;
|
778 |
+
left: 0;
|
779 |
+
z-index: 1;
|
780 |
+
">
|
781 |
+
<source src="data:video/mp4;base64,{item['video_base64']}" type="video/mp4">
|
782 |
+
Your browser does not support the video tag.
|
783 |
+
</video>
|
784 |
+
"""
|
785 |
+
else:
|
786 |
+
media_element = f"""
|
787 |
+
<img id="feed-image" src="data:image/png;base64,{item['image_base64']}" style="
|
788 |
+
width: 100%;
|
789 |
+
height: 100%;
|
790 |
+
object-fit: cover;
|
791 |
+
position: absolute;
|
792 |
+
top: 0;
|
793 |
+
left: 0;
|
794 |
+
z-index: 1;
|
795 |
+
">
|
796 |
+
"""
|
797 |
+
|
798 |
+
html_str = f"""
|
799 |
+
<div id="feed-container" style="
|
800 |
+
display: flex;
|
801 |
+
flex-direction: column;
|
802 |
+
align-items: center;
|
803 |
+
max-width: 360px;
|
804 |
+
margin: 0 auto;
|
805 |
+
background-color: #000;
|
806 |
+
height: 640px;
|
807 |
+
border: 1px solid #333;
|
808 |
+
border-radius: 10px;
|
809 |
+
position: relative;
|
810 |
+
">
|
811 |
+
<div class="feed-item" style="
|
812 |
+
width: 100%;
|
813 |
+
height: 100%;
|
814 |
+
position: relative;
|
815 |
+
display: flex;
|
816 |
+
flex-direction: column;
|
817 |
+
justify-content: flex-end;
|
818 |
+
overflow: hidden;
|
819 |
+
cursor: pointer;
|
820 |
+
" onclick="handleClick(event)">
|
821 |
+
{media_element}
|
822 |
+
<div style="
|
823 |
+
position: relative;
|
824 |
+
z-index: 2;
|
825 |
+
background: linear-gradient(to top, rgba(0,0,0,0.7), transparent);
|
826 |
+
padding: 20px;
|
827 |
+
color: white;
|
828 |
+
font-family: Arial, sans-serif;
|
829 |
+
font-size: 18px;
|
830 |
+
font-weight: bold;
|
831 |
+
text-shadow: 1px 1px 2px rgba(0,0,0,0.5);
|
832 |
+
">
|
833 |
+
{item['text']}
|
834 |
+
</div>
|
835 |
+
</div>
|
836 |
+
</div>
|
837 |
+
<script>
|
838 |
+
function handleClick(event) {{
|
839 |
+
const media = document.getElementById('feed-video') || document.getElementById('feed-image');
|
840 |
+
const rect = media.getBoundingClientRect();
|
841 |
+
const clickX = event.clientX - rect.left;
|
842 |
+
const width = rect.width;
|
843 |
+
if (clickX > width * 0.75) {{
|
844 |
+
document.getElementById('previous-button').click();
|
845 |
+
}}
|
846 |
+
}}
|
847 |
+
</script>
|
848 |
+
<button id="previous-button" style="display: none;" onclick="document.getElementById('previous-button').click()"></button>
|
849 |
+
"""
|
850 |
+
return html_str
|
851 |
|
852 |
# Define the Gradio interface
|
853 |
with gr.Blocks(
|
|
|
862 |
""",
|
863 |
title="Create Your Feed"
|
864 |
) as demo:
|
|
|
865 |
current_user_input = gr.State(value="")
|
866 |
current_index = gr.State(value=0)
|
867 |
feed_items = gr.State(value=[])
|
868 |
is_loading = gr.State(value=False)
|
869 |
share_links = gr.State(value="")
|
870 |
|
|
|
871 |
with gr.Column(elem_classes="gr-form"):
|
872 |
gr.Markdown("### Create Your Feed")
|
873 |
user_input = gr.Textbox(
|
|
|
882 |
)
|
883 |
magic_button = gr.Button("✨ Generate Next Item", elem_classes="gr-button")
|
884 |
|
|
|
885 |
progress_html = gr.HTML(label="Progress", visible=True)
|
886 |
feed_html = gr.HTML()
|
887 |
share_html = gr.HTML(label="Share this item:")
|
888 |
|
|
|
889 |
user_input.submit(
|
890 |
fn=start_feed,
|
891 |
inputs=[user_input, generate_video_checkbox, current_index, feed_items],
|
|
|
899 |
)
|
900 |
|
901 |
previous_button = gr.Button("Previous", elem_id="previous-button", visible=False)
|
902 |
+
|
903 |
previous_button.click(
|
904 |
fn=load_previous,
|
905 |
inputs=[user_input, generate_video_checkbox, current_index, feed_items],
|
906 |
outputs=[current_user_input, current_index, feed_items, feed_html, share_html, is_loading, progress_html]
|
907 |
)
|
908 |
|
909 |
+
# Launch the app
|
910 |
+
demo.launch()
|