Update app.py
Browse files
app.py
CHANGED
@@ -9,55 +9,31 @@ import json
|
|
9 |
import random
|
10 |
import urllib.parse
|
11 |
import time
|
12 |
-
import gradio
|
13 |
|
14 |
# Check Gradio version
|
15 |
required_version = "4.44.0"
|
16 |
-
current_version =
|
17 |
if current_version < required_version:
|
18 |
raise ValueError(f"Gradio version {current_version} is outdated. Please upgrade to {required_version} or later using 'pip install gradio=={required_version}'.")
|
19 |
|
20 |
-
# Initialize the Google Generative AI client
|
21 |
try:
|
22 |
api_key = os.environ['GEMINI_API_KEY']
|
23 |
except KeyError:
|
24 |
raise ValueError("Please set the GEMINI_API_KEY environment variable.")
|
25 |
client = genai.Client(api_key=api_key)
|
26 |
|
27 |
-
#
|
28 |
SAFETY_SETTINGS = [
|
29 |
-
types.SafetySetting(
|
30 |
-
|
31 |
-
|
32 |
-
),
|
33 |
-
types.SafetySetting(
|
34 |
-
category=types.HarmCategory.HARM_CATEGORY_HATE_SPEECH,
|
35 |
-
threshold=types.HarmBlockThreshold.BLOCK_NONE,
|
36 |
-
),
|
37 |
-
types.SafetySetting(
|
38 |
-
category=types.HarmCategory.HARM_CATEGORY_SEXUALLY_EXPLICIT,
|
39 |
-
threshold=types.HarmBlockThreshold.BLOCK_NONE,
|
40 |
-
),
|
41 |
-
types.SafetySetting(
|
42 |
-
category=types.HarmCategory.HARM_CATEGORY_DANGEROUS_CONTENT,
|
43 |
-
threshold=types.HarmBlockThreshold.BLOCK_NONE,
|
44 |
-
),
|
45 |
-
types.SafetySetting(
|
46 |
-
category=types.HarmCategory.HARM_CATEGORY_CIVIC_INTEGRITY,
|
47 |
-
threshold=types.HarmBlockThreshold.BLOCK_NONE,
|
48 |
-
),
|
49 |
]
|
50 |
|
51 |
def clean_response_text(response_text):
|
52 |
-
"""
|
53 |
-
Clean the API response by removing Markdown code block markers.
|
54 |
-
|
55 |
-
Args:
|
56 |
-
response_text (str): The raw response text from the API.
|
57 |
-
|
58 |
-
Returns:
|
59 |
-
str: The cleaned response text.
|
60 |
-
"""
|
61 |
cleaned_text = response_text.strip()
|
62 |
if cleaned_text.startswith("```json"):
|
63 |
cleaned_text = cleaned_text[len("```json"):].strip()
|
@@ -66,25 +42,13 @@ def clean_response_text(response_text):
|
|
66 |
return cleaned_text
|
67 |
|
68 |
def generate_ideas(user_input):
|
69 |
-
"""
|
70 |
-
Generate a diverse set of ideas based on the user's input concept using the LLM.
|
71 |
-
Yields progress updates for the loading UI.
|
72 |
-
|
73 |
-
Args:
|
74 |
-
user_input (str): The user's input concept or idea.
|
75 |
-
|
76 |
-
Yields:
|
77 |
-
tuple: (progress_percentage, message) for progress_html updates.
|
78 |
-
list: Final list of ideas as strings.
|
79 |
-
"""
|
80 |
yield (10, f"Brainstorming epic ideas for {user_input}... 🌟")
|
81 |
|
82 |
prompt = f"""
|
83 |
-
The user has provided the concept: "{user_input}".
|
84 |
-
Each idea must clearly incorporate and focus on the core theme of "{user_input}" without deviating into unrelated topics.
|
85 |
Each idea should be a short sentence describing a specific scene or concept.
|
86 |
-
Return the response as a JSON object with a
|
87 |
-
Ensure the response is strictly in JSON format.
|
88 |
Example for "blindfolded Rubik's Cube challenge":
|
89 |
{{"ideas": [
|
90 |
"A blindfolded speedcubing competition with dramatic music",
|
@@ -98,23 +62,15 @@ def generate_ideas(user_input):
|
|
98 |
response = client.models.generate_content(
|
99 |
model='gemini-2.0-flash-lite',
|
100 |
contents=[prompt],
|
101 |
-
config=types.GenerateContentConfig(
|
102 |
-
temperature=1.2,
|
103 |
-
safety_settings=SAFETY_SETTINGS
|
104 |
-
)
|
105 |
)
|
106 |
-
print(f"Raw response for ideas: {response.text}") # Debugging
|
107 |
-
if not response.text or response.text.isspace():
|
108 |
-
raise ValueError("Empty response from API")
|
109 |
cleaned_text = clean_response_text(response.text)
|
110 |
response_json = json.loads(cleaned_text)
|
111 |
-
if 'ideas' not in response_json or
|
112 |
-
raise ValueError("Invalid JSON format
|
113 |
-
|
114 |
ideas = response_json['ideas']
|
115 |
yield (20, f"Ideas locked in for {user_input}! 🚀")
|
116 |
return ideas
|
117 |
-
|
118 |
except Exception as e:
|
119 |
print(f"Error generating ideas: {e}")
|
120 |
yield (20, f"Oops, tweaking the plan for {user_input}... 🔧")
|
@@ -126,999 +82,254 @@ def generate_ideas(user_input):
|
|
126 |
f"An action-packed {user_input} challenge with dynamic angles"
|
127 |
]
|
128 |
|
129 |
-
def generate_item(user_input, ideas, generate_video=False
|
130 |
-
"""
|
131 |
-
Generate a single feed item (image and optionally one video) using one of the ideas.
|
132 |
-
Yields progress updates for the loading UI.
|
133 |
-
|
134 |
-
Args:
|
135 |
-
user_input (str): The user's input concept or idea.
|
136 |
-
ideas (list): List of ideas to choose from.
|
137 |
-
generate_video (bool): Whether to generate a video from the image.
|
138 |
-
max_retries (int): Maximum number of retries for image generation per cycle.
|
139 |
-
|
140 |
-
Yields:
|
141 |
-
tuple: (progress_percentage, message) for progress_html updates.
|
142 |
-
dict: Final dictionary with 'text', 'image_base64', 'video_base64', and 'ideas'.
|
143 |
-
"""
|
144 |
video_base64 = None
|
145 |
-
|
146 |
|
147 |
total_attempts = 0
|
148 |
-
while total_attempts <
|
149 |
total_attempts += 1
|
150 |
yield (20 + total_attempts * 10, f"Attempt {total_attempts} to craft your {user_input} masterpiece... 🎨")
|
151 |
|
152 |
-
|
153 |
-
|
154 |
-
|
155 |
-
|
156 |
-
|
157 |
-
|
158 |
-
|
159 |
-
|
160 |
-
|
161 |
-
|
162 |
-
|
163 |
-
|
164 |
-
|
165 |
-
|
166 |
-
|
167 |
-
|
168 |
-
|
169 |
-
|
170 |
-
|
171 |
-
|
172 |
-
|
173 |
-
|
174 |
-
|
175 |
-
|
176 |
-
|
177 |
-
|
178 |
-
|
179 |
-
|
180 |
-
|
181 |
-
|
182 |
-
|
183 |
-
|
184 |
-
|
185 |
-
|
186 |
-
|
187 |
-
|
188 |
-
|
189 |
-
|
190 |
-
|
191 |
-
|
192 |
-
|
193 |
-
|
194 |
-
|
195 |
-
|
196 |
-
|
197 |
-
|
198 |
-
|
199 |
-
|
200 |
-
|
201 |
-
|
202 |
-
|
203 |
-
|
204 |
-
if imagen.generated_images and len(imagen.generated_images) > 0:
|
205 |
-
generated_image = imagen.generated_images[0]
|
206 |
-
image = Image.open(BytesIO(generated_image.image.image_bytes))
|
207 |
-
# Ensure the image matches the desired aspect ratio (9:16 = 0.5625)
|
208 |
-
target_width = 360
|
209 |
-
target_height = int(target_width / 9 * 16) # 9:16 aspect ratio
|
210 |
-
image = image.resize((target_width, target_height), Image.LANCZOS)
|
211 |
-
# Convert image to base64
|
212 |
-
buffered = BytesIO()
|
213 |
-
image.save(buffered, format="PNG")
|
214 |
-
img_str = base64.b64encode(buffered.getvalue()).decode()
|
215 |
-
yield (50, f"Image for {user_input} is ready! 🎉")
|
216 |
-
break # Successfully generated image, exit image retry loop
|
217 |
-
else:
|
218 |
-
print(f"Image generation failed (image attempt {image_attempt + 1}, total attempt {total_attempts}): No images returned")
|
219 |
-
if image_attempt == max_retries - 1:
|
220 |
-
yield (50, f"Tweaking the image for {user_input}... 🔄")
|
221 |
-
if total_attempts == max_total_attempts:
|
222 |
-
# Max total attempts reached, use a gray placeholder
|
223 |
-
image = Image.new('RGB', (360, 640), color='gray')
|
224 |
-
buffered = BytesIO()
|
225 |
-
image.save(buffered, format="PNG")
|
226 |
-
img_str = base64.b64encode(buffered.getvalue()).decode()
|
227 |
-
yield (60, f"Using a placeholder for {user_input}... 🖼️")
|
228 |
-
return {
|
229 |
-
'text': text,
|
230 |
-
'image_base64': img_str,
|
231 |
-
'video_base64': None,
|
232 |
-
'ideas': ideas
|
233 |
-
}
|
234 |
-
break # Exit inner loop to retry with new idea
|
235 |
-
except Exception as e:
|
236 |
-
print(f"Error generating image (image attempt {image_attempt + 1}, total attempt {total_attempts}): {e}")
|
237 |
-
if image_attempt == max_retries - 1:
|
238 |
-
yield (50, f"Retrying image for {user_input}... 🔄")
|
239 |
-
if total_attempts == max_total_attempts:
|
240 |
-
# Max total attempts reached, use a gray placeholder
|
241 |
-
image = Image.new('RGB', (360, 640), color='gray')
|
242 |
-
buffered = BytesIO()
|
243 |
-
image.save(buffered, format="PNG")
|
244 |
-
img_str = base64.b64encode(buffered.getvalue()).decode()
|
245 |
-
yield (60, f"Using a placeholder for {user_input}... 🖼️")
|
246 |
-
return {
|
247 |
-
'text': text,
|
248 |
-
'image_base64': img_str,
|
249 |
-
'video_base64': None,
|
250 |
-
'ideas': ideas
|
251 |
-
}
|
252 |
-
break # Exit inner loop to retry with new idea
|
253 |
-
|
254 |
-
# Step 2: Generate video if enabled (with fallback to text-to-video if image-to-video fails)
|
255 |
-
if generate_video and generated_image is not None:
|
256 |
-
max_video_retries_per_image = 2 # Try text-to-video generation twice if needed
|
257 |
-
video_generated = False
|
258 |
-
|
259 |
-
# First, try image-to-video generation (only once)
|
260 |
try:
|
261 |
yield (60, f"Filming a viral video for {user_input}... 🎥")
|
262 |
-
video_prompt = f"""
|
263 |
-
The user concept is "{user_input}". Based on this and the scene: {image_prompt}, create a video.
|
264 |
-
Use a close-up shot with a slow dolly shot circling around the subject,
|
265 |
-
using shallow focus on the main subject to emphasize details, in a realistic style with cinematic lighting.
|
266 |
-
"""
|
267 |
-
print(f"Attempting image-to-video generation (total attempt {total_attempts}): {video_prompt}")
|
268 |
operation = client.models.generate_videos(
|
269 |
model="veo-2.0-generate-001",
|
270 |
-
prompt=
|
271 |
-
image=
|
272 |
-
config=types.GenerateVideosConfig(
|
273 |
-
aspect_ratio="9:16",
|
274 |
-
number_of_videos=1,
|
275 |
-
duration_seconds=8,
|
276 |
-
negative_prompt="blurry, low quality, text, letters"
|
277 |
-
)
|
278 |
)
|
279 |
-
# Wait for video to generate
|
280 |
while not operation.done:
|
281 |
time.sleep(20)
|
282 |
operation = client.operations.get(operation)
|
283 |
-
|
284 |
-
|
285 |
-
|
286 |
-
print(f"Operation done: {operation.done}")
|
287 |
-
print(f"Operation error: {operation.error}")
|
288 |
-
if operation.error:
|
289 |
-
print(f"Operation error message: {operation.error.message}")
|
290 |
-
if hasattr(operation.error, 'code'):
|
291 |
-
print(f"Operation error code: {operation.error.code}")
|
292 |
-
if hasattr(operation.error, 'details'):
|
293 |
-
print(f"Operation error details: {operation.error.details}")
|
294 |
-
print(f"Operation response: {operation.response}")
|
295 |
-
if operation.response:
|
296 |
-
print(f"Operation response has generated_videos: {hasattr(operation.response, 'generated_videos')}")
|
297 |
-
if hasattr(operation.response, 'generated_videos'):
|
298 |
-
print(f"Generated videos: {operation.response.generated_videos}")
|
299 |
-
else:
|
300 |
-
print("No generated_videos attribute in response")
|
301 |
-
|
302 |
-
# Enhanced error handling for video generation response
|
303 |
-
if operation.error:
|
304 |
-
raise ValueError(f"Video generation operation failed with error: {operation.error.message}")
|
305 |
-
if operation.response is None:
|
306 |
-
raise ValueError("Video generation operation failed: No response")
|
307 |
-
if not hasattr(operation.response, 'generated_videos') or operation.response.generated_videos is None:
|
308 |
-
raise ValueError("Video generation operation failed: No generated_videos in response")
|
309 |
-
|
310 |
-
# Process the single generated video
|
311 |
-
if len(operation.response.generated_videos) > 0:
|
312 |
-
video = operation.response.generated_videos[0]
|
313 |
-
if video is None or not hasattr(video, 'video'):
|
314 |
-
raise ValueError("Video is invalid or missing video data")
|
315 |
-
fname = 'with_image_input.mp4'
|
316 |
-
print(f"Generated video: {fname}")
|
317 |
-
# Download the video and get the raw bytes
|
318 |
-
video_data = client.files.download(file=video.video)
|
319 |
-
# Ensure video_data is in bytes
|
320 |
-
if isinstance(video_data, bytes):
|
321 |
-
video_bytes = video_data
|
322 |
-
else:
|
323 |
-
# If video_data is a file-like object, read the bytes
|
324 |
-
video_buffer = BytesIO()
|
325 |
-
for chunk in video_data:
|
326 |
-
video_buffer.write(chunk)
|
327 |
-
video_bytes = video_buffer.getvalue()
|
328 |
-
# Encode the video bytes as base64
|
329 |
video_base64 = base64.b64encode(video_bytes).decode()
|
330 |
-
video_generated = True
|
331 |
yield (90, f"Video for {user_input} is a wrap! 🎬")
|
332 |
-
|
333 |
-
return {
|
334 |
-
'text': text,
|
335 |
-
'image_base64': img_str,
|
336 |
-
'video_base64': video_base64,
|
337 |
-
'ideas': ideas
|
338 |
-
}
|
339 |
-
else:
|
340 |
-
raise ValueError("No video was generated")
|
341 |
except Exception as e:
|
342 |
-
print(f"Error generating video
|
343 |
-
yield (70, f"
|
344 |
-
print("Image-to-video generation failed. Falling back to text-to-video generation.")
|
345 |
-
|
346 |
-
# If image-to-video generation failed, try text-to-video generation
|
347 |
-
if not video_generated:
|
348 |
-
for video_attempt in range(max_video_retries_per_image):
|
349 |
-
try:
|
350 |
-
yield (75 + video_attempt * 5, f"Trying a fresh video take for {user_input}... 📹")
|
351 |
-
# Use the same video prompt but without the image
|
352 |
-
video_prompt_base = f"""
|
353 |
-
The user concept is "{user_input}". Based on this and the scene: {image_prompt}, create a video.
|
354 |
-
Use a close-up shot with a slow dolly shot circling around the subject,
|
355 |
-
using shallow focus on the main subject to emphasize details, in a realistic style with cinematic lighting.
|
356 |
-
"""
|
357 |
-
if video_attempt == 0:
|
358 |
-
video_prompt = video_prompt_base
|
359 |
-
else:
|
360 |
-
video_prompt = f"""
|
361 |
-
The user concept is "{user_input}". Based on this and a simplified scene: {image_prompt}, create a video.
|
362 |
-
Use a static close-up shot of the subject in a realistic style.
|
363 |
-
"""
|
364 |
-
|
365 |
-
print(f"Attempting text-to-video generation (video attempt {video_attempt + 1}, total attempt {total_attempts}): {video_prompt}")
|
366 |
-
operation = client.models.generate_videos(
|
367 |
-
model="veo-2.0-generate-001",
|
368 |
-
prompt=video_prompt,
|
369 |
-
config=types.GenerateVideosConfig(
|
370 |
-
aspect_ratio="9:16",
|
371 |
-
number_of_videos=1,
|
372 |
-
duration_seconds=8,
|
373 |
-
negative_prompt="blurry, low quality, text, letters"
|
374 |
-
)
|
375 |
-
)
|
376 |
-
# Wait for video to generate
|
377 |
-
while not operation.done:
|
378 |
-
time.sleep(20)
|
379 |
-
operation = client.operations.get(operation)
|
380 |
-
|
381 |
-
# Log detailed information about the operation
|
382 |
-
print(f"Video generation operation completed: {operation}")
|
383 |
-
print(f"Operation done: {operation.done}")
|
384 |
-
print(f"Operation error: {operation.error}")
|
385 |
-
if operation.error:
|
386 |
-
print(f"Operation error message: {operation.error.message}")
|
387 |
-
if hasattr(operation.error, 'code'):
|
388 |
-
print(f"Operation error code: {operation.error.code}")
|
389 |
-
if hasattr(operation.error, 'details'):
|
390 |
-
print(f"Operation error details: {operation.error.details}")
|
391 |
-
print(f"Operation response: {operation.response}")
|
392 |
-
if operation.response:
|
393 |
-
print(f"Operation response has generated_videos: {hasattr(operation.response, 'generated_videos')}")
|
394 |
-
if hasattr(operation.response, 'generated_videos'):
|
395 |
-
print(f"Generated videos: {operation.response.generated_videos}")
|
396 |
-
else:
|
397 |
-
print("No generated_videos attribute in response")
|
398 |
-
|
399 |
-
# Enhanced error handling for video generation response
|
400 |
-
if operation.error:
|
401 |
-
raise ValueError(f"Video generation operation failed with error: {operation.error.message}")
|
402 |
-
if operation.response is None:
|
403 |
-
raise ValueError("Video generation operation failed: No response")
|
404 |
-
if not hasattr(operation.response, 'generated_videos') or operation.response.generated_videos is None:
|
405 |
-
raise ValueError("Video generation operation failed: No generated_videos in response")
|
406 |
-
|
407 |
-
# Process the single generated video
|
408 |
-
if len(operation.response.generated_videos) > 0:
|
409 |
-
video = operation.response.generated_videos[0]
|
410 |
-
if video is None or not hasattr(video, 'video'):
|
411 |
-
raise ValueError("Video is invalid or missing video data")
|
412 |
-
fname = 'text_to_video.mp4'
|
413 |
-
print(f"Generated video: {fname}")
|
414 |
-
# Download the video and get the raw bytes
|
415 |
-
video_data = client.files.download(file=video.video)
|
416 |
-
# Ensure video_data is in bytes
|
417 |
-
if isinstance(video_data, bytes):
|
418 |
-
video_bytes = video_data
|
419 |
-
else:
|
420 |
-
# If video_data is a file-like object, read the bytes
|
421 |
-
video_buffer = BytesIO()
|
422 |
-
for chunk in video_data:
|
423 |
-
video_buffer.write(chunk)
|
424 |
-
video_bytes = video_buffer.getvalue()
|
425 |
-
# Encode the video bytes as base64
|
426 |
-
video_base64 = base64.b64encode(video_bytes).decode()
|
427 |
-
video_generated = True
|
428 |
-
yield (90, f"Video for {user_input} is a wrap! 🎬")
|
429 |
-
# Successfully generated video, return the result
|
430 |
-
return {
|
431 |
-
'text': text,
|
432 |
-
'image_base64': img_str,
|
433 |
-
'video_base64': video_base64,
|
434 |
-
'ideas': ideas
|
435 |
-
}
|
436 |
-
else:
|
437 |
-
raise ValueError("No video was generated")
|
438 |
-
except Exception as e:
|
439 |
-
print(f"Error generating video (text-to-video attempt {video_attempt + 1}, total attempt {total_attempts}): {e}")
|
440 |
-
if video_attempt == max_video_retries_per_image - 1:
|
441 |
-
yield (85, f"Finalizing without video for {user_input}... 📌")
|
442 |
-
if total_attempts == max_total_attempts:
|
443 |
-
print("Max total attempts reached. Proceeding without video.")
|
444 |
-
video_base64 = None
|
445 |
-
yield (95, f"Polishing your {user_input} masterpiece... ✨")
|
446 |
-
return {
|
447 |
-
'text': text,
|
448 |
-
'image_base64': img_str,
|
449 |
-
'video_base64': video_base64,
|
450 |
-
'ideas': ideas
|
451 |
-
}
|
452 |
-
# Text-to-video failed, break to outer loop to try a new image
|
453 |
-
print(f"Text-to-video generation failed after {max_video_retries_per_image} attempts. Selecting a new idea and generating a new image.")
|
454 |
-
break
|
455 |
-
continue # Retry text-to-video generation with a modified prompt
|
456 |
|
457 |
-
|
458 |
-
|
459 |
-
yield (95, f"Polishing your {user_input} masterpiece... ✨")
|
460 |
-
return {
|
461 |
-
'text': text,
|
462 |
-
'image_base64': img_str,
|
463 |
-
'video_base64': video_base64,
|
464 |
-
'ideas': ideas
|
465 |
-
}
|
466 |
-
# If img_str is None, continue to next cycle or fall back if max attempts reached
|
467 |
-
|
468 |
-
# If max total attempts reached without success, use a gray placeholder image
|
469 |
-
print("Max total attempts reached without successful image generation. Using placeholder.")
|
470 |
-
yield (95, f"Falling back to a placeholder for {user_input}... 🖼️")
|
471 |
-
image = Image.new('RGB', (360, 640), color='gray')
|
472 |
-
buffered = BytesIO()
|
473 |
-
image.save(buffered, format="PNG")
|
474 |
-
img_str = base64.b64encode(buffered.getvalue()).decode()
|
475 |
-
yield (100, f"Ready to roll with {user_input}! 🚀")
|
476 |
-
return {
|
477 |
-
'text': f"Amazing {user_input}! 🔥 #{user_input.replace(' ', '')}",
|
478 |
-
'image_base64': img_str,
|
479 |
-
'video_base64': None,
|
480 |
-
'ideas': ideas
|
481 |
-
}
|
482 |
|
483 |
def generate_progress_html(progress, message, user_input):
|
484 |
-
"""
|
485 |
-
Generate HTML for the progress bar and witty text.
|
486 |
-
|
487 |
-
Args:
|
488 |
-
progress (float): Current progress percentage (0–100).
|
489 |
-
message (str): Current loading message to display.
|
490 |
-
user_input (str): The user's input concept or idea for context.
|
491 |
-
|
492 |
-
Returns:
|
493 |
-
str: HTML string for the progress bar.
|
494 |
-
"""
|
495 |
return f"""
|
496 |
<div id="progress-container" style="
|
497 |
-
display: flex;
|
498 |
-
|
499 |
-
|
500 |
-
|
501 |
-
max-width: 360px;
|
502 |
-
margin: 0 auto;
|
503 |
-
background-color: #000;
|
504 |
-
height: 200px;
|
505 |
-
border: 1px solid #333;
|
506 |
-
border-radius: 10px;
|
507 |
-
color: white;
|
508 |
-
font-family: Arial, sans-serif;
|
509 |
-
position: relative;
|
510 |
-
">
|
511 |
-
<div id="loading-message" style="
|
512 |
-
font-size: 18px;
|
513 |
-
font-weight: bold;
|
514 |
-
text-align: center;
|
515 |
-
margin-bottom: 20px;
|
516 |
-
text-shadow: 1px 1px 2px rgba(0,0,0,0.5);
|
517 |
-
">
|
518 |
{message}
|
519 |
</div>
|
520 |
-
<div style="
|
521 |
-
width:
|
522 |
-
height: 10px;
|
523 |
-
background-color: #333;
|
524 |
-
border-radius: 5px;
|
525 |
-
overflow: hidden;
|
526 |
-
">
|
527 |
-
<div id="progress-bar" style="
|
528 |
-
width: {progress}%;
|
529 |
-
height: 100%;
|
530 |
-
background: linear-gradient(to right, #ff2d55, #ff5e78);
|
531 |
-
transition: width 0.5s ease-in-out;
|
532 |
-
"></div>
|
533 |
-
</div>
|
534 |
-
<div style="
|
535 |
-
margin-top: 10px;
|
536 |
-
font-size: 14px;
|
537 |
-
color: #ccc;
|
538 |
-
">
|
539 |
-
{int(progress)}% Complete
|
540 |
</div>
|
|
|
541 |
<style>
|
542 |
-
@keyframes pulse {{
|
543 |
-
|
544 |
-
50% {{ opacity: 0.5; }}
|
545 |
-
100% {{ opacity: 1; }}
|
546 |
-
}}
|
547 |
-
#loading-message {{
|
548 |
-
animation: pulse 2s infinite;
|
549 |
-
}}
|
550 |
</style>
|
551 |
</div>
|
552 |
"""
|
553 |
|
554 |
-
def
|
555 |
-
"""
|
556 |
-
|
557 |
-
|
558 |
-
|
559 |
-
|
560 |
-
|
561 |
-
|
562 |
-
|
563 |
-
|
564 |
-
|
565 |
-
|
566 |
-
|
567 |
-
tuple: (current_user_input, current_index, feed_items, feed_html, share_html, is_loading) for final result.
|
568 |
"""
|
569 |
-
|
570 |
-
|
571 |
-
|
572 |
-
|
|
|
|
|
|
|
|
|
|
|
573 |
current_user_input = user_input
|
574 |
-
|
575 |
-
# Set initial loading state
|
576 |
is_loading = True
|
577 |
progress_html = generate_progress_html(0, f"Getting started with {user_input}... 🚀", user_input)
|
578 |
-
yield (
|
579 |
-
share_links = ""
|
580 |
|
581 |
try:
|
582 |
-
# Generate ideas with progress updates
|
583 |
ideas_gen = generate_ideas(user_input)
|
584 |
ideas = None
|
585 |
for update in ideas_gen:
|
586 |
if isinstance(update, tuple):
|
587 |
progress, message = update
|
588 |
progress_html = generate_progress_html(progress, message, user_input)
|
589 |
-
yield (
|
590 |
else:
|
591 |
ideas = update
|
592 |
|
593 |
-
|
594 |
-
item_gen = generate_item(user_input, ideas, generate_video=generate_video)
|
595 |
item = None
|
596 |
for update in item_gen:
|
597 |
if isinstance(update, tuple):
|
598 |
progress, message = update
|
599 |
progress_html = generate_progress_html(progress, message, user_input)
|
600 |
-
yield (
|
601 |
else:
|
602 |
item = update
|
603 |
|
604 |
feed_items = [item]
|
605 |
current_index = 0
|
606 |
feed_html = generate_html(feed_items, False, current_index, user_input, is_loading=False)
|
607 |
-
|
608 |
-
|
609 |
-
item['video_base64'],
|
610 |
-
item['text']
|
611 |
-
)
|
612 |
except Exception as e:
|
613 |
print(f"Error in start_feed: {e}")
|
614 |
-
|
615 |
-
current_index
|
616 |
-
feed_html = """
|
617 |
-
<div style="
|
618 |
-
display: flex;
|
619 |
-
flex-direction: column;
|
620 |
-
align-items: center;
|
621 |
-
justify-content: center;
|
622 |
-
max-width: 360px;
|
623 |
-
margin: 0 auto;
|
624 |
-
background-color: #000;
|
625 |
-
height: 640px;
|
626 |
-
border: 1px solid #333;
|
627 |
-
border-radius: 10px;
|
628 |
-
color: white;
|
629 |
-
font-family: Arial, sans-serif;
|
630 |
-
">
|
631 |
-
<p>Error generating content. Please try again!</p>
|
632 |
-
</div>
|
633 |
-
"""
|
634 |
-
progress_html = generate_progress_html(100, "Oops, something went wrong! 😅", user_input)
|
635 |
-
yield (progress_html,)
|
636 |
-
is_loading = False
|
637 |
-
return current_user_input, current_index, feed_items, feed_html, share_links, is_loading
|
638 |
-
|
639 |
-
# Set loading state to False and update UI
|
640 |
-
is_loading = False
|
641 |
-
progress_html = ""
|
642 |
-
return current_user_input, current_index, feed_items, feed_html, share_links, is_loading
|
643 |
|
644 |
def load_next(user_input, generate_video, current_index, feed_items):
|
645 |
-
"""
|
646 |
-
|
647 |
-
Yields progress updates and final feed content.
|
648 |
-
|
649 |
-
Args:
|
650 |
-
user_input (str): The user's input concept or idea (updated from the textbox).
|
651 |
-
generate_video (bool): Whether to generate a video.
|
652 |
-
current_index (int): The current item index.
|
653 |
-
feed_items (list): The current list of feed_items.
|
654 |
-
|
655 |
-
Yields:
|
656 |
-
tuple: (progress_html) for progress updates.
|
657 |
-
tuple: (current_user_input, current_index, feed_items, feed_html, share_html, is_loading) for final result.
|
658 |
-
"""
|
659 |
-
# Update current_user_input with the latest user_input from the textbox
|
660 |
-
current_user_input = user_input if user_input.strip() else "trending"
|
661 |
-
|
662 |
-
# Use current_user_input for generating the next item
|
663 |
-
user_input = current_user_input
|
664 |
-
|
665 |
is_loading = True
|
666 |
-
progress_html = generate_progress_html(0, f"Loading next {
|
667 |
-
yield (
|
668 |
-
share_links = ""
|
669 |
|
670 |
try:
|
671 |
if current_index + 1 < len(feed_items):
|
672 |
current_index += 1
|
673 |
-
progress_html = generate_progress_html(50, f"Switching to the next {
|
674 |
-
yield (
|
675 |
else:
|
676 |
ideas = feed_items[-1]['ideas'] if feed_items else None
|
677 |
if not ideas:
|
678 |
-
|
679 |
-
ideas_gen = generate_ideas(user_input)
|
680 |
for update in ideas_gen:
|
681 |
if isinstance(update, tuple):
|
682 |
progress, message = update
|
683 |
-
progress_html = generate_progress_html(progress, message,
|
684 |
-
yield (
|
685 |
else:
|
686 |
ideas = update
|
687 |
|
688 |
-
|
689 |
-
|
690 |
-
new_item = None
|
691 |
-
for update in new_item_gen:
|
692 |
if isinstance(update, tuple):
|
693 |
progress, message = update
|
694 |
-
progress_html = generate_progress_html(progress, message,
|
695 |
-
yield (
|
696 |
else:
|
697 |
-
|
698 |
-
|
699 |
-
feed_items.append(new_item)
|
700 |
-
current_index = len(feed_items) - 1
|
701 |
|
702 |
-
feed_html = generate_html(feed_items, False, current_index,
|
703 |
-
|
704 |
-
|
705 |
-
feed_items[current_index]['video_base64'],
|
706 |
-
feed_items[current_index]['text']
|
707 |
-
)
|
708 |
except Exception as e:
|
709 |
print(f"Error in load_next: {e}")
|
710 |
-
feed_html = ""
|
711 |
-
|
712 |
-
display: flex;
|
713 |
-
flex-direction: column;
|
714 |
-
align-items: center;
|
715 |
-
justify-content: center;
|
716 |
-
max-width: 360px;
|
717 |
-
margin: 0 auto;
|
718 |
-
background-color: #000;
|
719 |
-
height: 640px;
|
720 |
-
border: 1px solid #333;
|
721 |
-
border-radius: 10px;
|
722 |
-
color: white;
|
723 |
-
font-family: Arial, sans-serif;
|
724 |
-
">
|
725 |
-
<p>Error generating content. Please try again!</p>
|
726 |
-
</div>
|
727 |
-
"""
|
728 |
-
progress_html = generate_progress_html(100, "Oops, something went wrong! 😅", user_input)
|
729 |
-
yield (progress_html,)
|
730 |
-
is_loading = False
|
731 |
-
return current_user_input, current_index, feed_items, feed_html, share_links, is_loading
|
732 |
-
|
733 |
-
is_loading = False
|
734 |
-
progress_html = ""
|
735 |
-
return current_user_input, current_index, feed_items, feed_html, share_links, is_loading
|
736 |
|
737 |
def load_previous(user_input, generate_video, current_index, feed_items):
|
738 |
-
"""
|
739 |
-
|
740 |
-
|
741 |
-
Args:
|
742 |
-
user_input (str): The user's input concept or idea.
|
743 |
-
generate_video (bool): Whether to generate a video (not used here).
|
744 |
-
current_index (int): The current item index.
|
745 |
-
feed_items (list): The current list of feed items.
|
746 |
-
|
747 |
-
Returns:
|
748 |
-
tuple: (current_user_input, current_index, feed_items, feed_html, share_html, is_loading)
|
749 |
-
"""
|
750 |
-
# Update current_user_input with the latest user_input
|
751 |
-
current_user_input = user_input if user_input.strip() else "trending"
|
752 |
-
|
753 |
if current_index > 0:
|
754 |
current_index -= 1
|
755 |
-
feed_html = generate_html(feed_items, False, current_index,
|
756 |
-
|
757 |
-
|
758 |
-
|
759 |
-
|
760 |
-
|
761 |
-
|
762 |
-
|
763 |
-
|
764 |
-
|
765 |
-
|
766 |
-
|
767 |
-
|
768 |
-
|
769 |
-
image_base64 (str): The base64-encoded image data.
|
770 |
-
video_base64 (str or None): The base64-enoded video data (single video).
|
771 |
-
caption (str): The caption to share.
|
772 |
-
|
773 |
-
Returns:
|
774 |
-
str: HTML string with share links and download instructions.
|
775 |
-
"""
|
776 |
-
image_data_url = f"data:image/png;base64,{image_base64}"
|
777 |
-
encoded_caption = urllib.parse.quote(caption)
|
778 |
-
|
779 |
-
# Generate download links for image and video (if available)
|
780 |
-
download_links = f"""
|
781 |
-
<p style="text-align: center; margin-bottom: 10px;">Download the media to share:</p>
|
782 |
-
<div style="display: flex; flex-wrap: wrap; justify-content: center; gap: 8px; margin-bottom: 15px;">
|
783 |
-
<a href="{image_data_url}" download="feed_item.png" style="
|
784 |
-
background-color: #4CAF50;
|
785 |
-
color: white;
|
786 |
-
padding: 8px 16px;
|
787 |
-
border-radius: 5px;
|
788 |
-
text-decoration: none;
|
789 |
-
font-size: 14px;
|
790 |
-
font-weight: bold;
|
791 |
-
transition: background-color 0.3s;
|
792 |
-
" onmouseover="this.style.backgroundColor='#45a049'" onmouseout="this.style.backgroundColor='#4CAF50'">Download Image</a>
|
793 |
-
"""
|
794 |
-
if video_base64: # Only include video download link if a video exists
|
795 |
-
video_data_url = f"data:video/mp4;base64,{video_base64}"
|
796 |
-
download_links += f"""
|
797 |
-
<a href="{video_data_url}" download="feed_video.mp4" style="
|
798 |
-
background-color: #4CAF50;
|
799 |
-
color: white;
|
800 |
-
padding: 8px 16px;
|
801 |
-
border-radius: 5px;
|
802 |
-
text-decoration: none;
|
803 |
-
font-size: 14px;
|
804 |
-
font-weight: bold;
|
805 |
-
transition: background-color 0.3s;
|
806 |
-
" onmouseover="this.style.backgroundColor='#45a049'" onmouseout="this.style.backgroundColor='#4CAF50'">Download Video</a>
|
807 |
-
"""
|
808 |
-
download_links += "</div>"
|
809 |
-
|
810 |
-
# Instruction for users
|
811 |
-
instruction = """
|
812 |
-
<p style="text-align: center; margin-bottom: 10px;">
|
813 |
-
Click a share button below to start a post with the caption, then manually upload the downloaded image or video.
|
814 |
-
</p>
|
815 |
-
"""
|
816 |
-
|
817 |
-
# Generate share links for social media platforms (only passing the caption)
|
818 |
-
share_links = """
|
819 |
-
<div style="
|
820 |
-
display: flex;
|
821 |
-
flex-wrap: wrap;
|
822 |
-
justify-content: center;
|
823 |
-
gap: 8px;
|
824 |
-
margin-bottom: 15px;
|
825 |
-
">
|
826 |
-
<a href="https://www.tiktok.com/upload?caption={caption}" target="_blank" style="
|
827 |
-
background-color: #00f2ea;
|
828 |
-
color: #000;
|
829 |
-
padding: 8px 16px;
|
830 |
-
border-radius: 5px;
|
831 |
-
text-decoration: none;
|
832 |
-
font-size: 14px;
|
833 |
-
font-weight: bold;
|
834 |
-
transition: background-color 0.3s;
|
835 |
-
" onmouseover="this.style.backgroundColor='#00d9d1'" onmouseout="this.style.backgroundColor='#00f2ea'">Share on TikTok</a>
|
836 |
-
<a href="https://www.instagram.com/?caption={caption}" target="_blank" style="
|
837 |
-
background-color: #e1306c;
|
838 |
-
color: white;
|
839 |
-
padding: 8px 16px;
|
840 |
-
border-radius: 5px;
|
841 |
-
text-decoration: none;
|
842 |
-
font-size: 14px;
|
843 |
-
font-weight: bold;
|
844 |
-
transition: background-color 0.3s;
|
845 |
-
" onmouseover="this.style.backgroundColor='#c72b5e'" onmouseout="this.style.backgroundColor='#e1306c'">Share on Instagram</a>
|
846 |
-
<a href="https://www.facebook.com/sharer/sharer.php?quote={caption}" target="_blank" style="
|
847 |
-
background-color: #4267b2;
|
848 |
-
color: white;
|
849 |
-
padding: 8px 16px;
|
850 |
-
border-radius: 5px;
|
851 |
-
text-decoration: none;
|
852 |
-
font-size: 14px;
|
853 |
-
font-weight: bold;
|
854 |
-
transition: background-color 0.3s;
|
855 |
-
" onmouseover="this.style.backgroundColor='#395a9d'" onmouseout="this.style.backgroundColor='#4267b2'">Share on Facebook</a>
|
856 |
-
<a href="https://twitter.com/intent/tweet?text={caption}" target="_blank" style="
|
857 |
-
background-color: #1da1f2;
|
858 |
-
color: white;
|
859 |
-
padding: 8px 16px;
|
860 |
-
border-radius: 5px;
|
861 |
-
text-decoration: none;
|
862 |
-
font-size: 14px;
|
863 |
-
font-weight: bold;
|
864 |
-
transition: background-color 0.3s;
|
865 |
-
" onmouseover="this.style.backgroundColor='#1a91da'" onmouseout="this.style.backgroundColor='#1da1f2'">Share on X</a>
|
866 |
-
<a href="https://pinterest.com/pin/create/button/?description={caption}" target="_blank" style="
|
867 |
-
background-color: #bd081c;
|
868 |
-
color: white;
|
869 |
-
padding: 8px 16px;
|
870 |
-
border-radius: 5px;
|
871 |
-
text-decoration: none;
|
872 |
-
font-size: 14px;
|
873 |
-
font-weight: bold;
|
874 |
-
transition: background-color 0.3s;
|
875 |
-
" onmouseover="this.style.backgroundColor='#a30718'" onmouseout="this.style.backgroundColor='#bd081c'">Share on Pinterest</a>
|
876 |
-
</div>
|
877 |
-
"""
|
878 |
-
|
879 |
-
# Add YouTube Shorts share button if a video is available
|
880 |
-
youtube_share = ""
|
881 |
-
if video_base64: # Only show YouTube Shorts share button if a video is generated
|
882 |
-
youtube_share = f"""
|
883 |
-
<div style="
|
884 |
-
display: flex;
|
885 |
-
justify-content: center;
|
886 |
-
margin-top: 10px;
|
887 |
-
">
|
888 |
-
<a href="https://studio.youtube.com/channel/UC/videos/upload?description={caption}" target="_blank" style="
|
889 |
-
background-color: #ff0000;
|
890 |
-
color: white;
|
891 |
-
padding: 8px 16px;
|
892 |
-
border-radius: 5px;
|
893 |
-
text-decoration: none;
|
894 |
-
font-size: 14px;
|
895 |
-
font-weight: bold;
|
896 |
-
transition: background-color 0.3s;
|
897 |
-
" onmouseover="this.style.backgroundColor='#e60000'" onmouseout="this.style.backgroundColor='#ff0000'">Share to YouTube as a Short</a>
|
898 |
-
</div>
|
899 |
-
"""
|
900 |
-
|
901 |
-
return f"""
|
902 |
-
<div style="
|
903 |
-
display: flex;
|
904 |
-
flex-direction: column;
|
905 |
-
align-items: center;
|
906 |
-
gap: 10px;
|
907 |
-
margin-top: 10px;
|
908 |
-
color: white;
|
909 |
-
font-family: Arial, sans-serif;
|
910 |
-
">
|
911 |
-
{download_links}
|
912 |
-
{instruction}
|
913 |
-
{share_links}
|
914 |
-
{youtube_share}
|
915 |
-
</div>
|
916 |
-
""".format(caption=encoded_caption)
|
917 |
-
|
918 |
-
def generate_html(feed_items, scroll_to_latest=False, current_index=0, user_input="", is_loading=False, progress=0, message=""):
|
919 |
-
"""
|
920 |
-
Generate an HTML string to display the current feed item or loading state.
|
921 |
-
|
922 |
-
Args:
|
923 |
-
feed_items (list): List of dictionaries containing 'text', 'image_base64', and 'video_base64'.
|
924 |
-
scroll_to_latest (bool): Whether to auto-scroll to the latest item (not used here).
|
925 |
-
current_index (int): The index of the item to display.
|
926 |
-
user_input (str): The user's input concept or idea for context.
|
927 |
-
is_loading (bool): Whether the feed is currently loading.
|
928 |
-
progress (float): Current progress percentage (0–100).
|
929 |
-
message (str): Current loading message to display.
|
930 |
-
|
931 |
-
Returns:
|
932 |
-
str: HTML string representing the feed or loading state.
|
933 |
-
"""
|
934 |
-
if is_loading:
|
935 |
-
# When loading, return an empty feed container (progress is handled by progress_html)
|
936 |
-
return """
|
937 |
-
<div id="feed-container" style="
|
938 |
-
display: flex;
|
939 |
-
flex-direction: column;
|
940 |
-
align-items: center;
|
941 |
-
max-width: 360px;
|
942 |
-
margin: 0 auto;
|
943 |
-
background-color: #000;
|
944 |
-
height: 640px;
|
945 |
-
border: 1px solid #333;
|
946 |
-
border-radius: 10px;
|
947 |
-
color: white;
|
948 |
-
font-family: Arial, sans-serif;
|
949 |
-
">
|
950 |
-
</div>
|
951 |
-
"""
|
952 |
-
|
953 |
-
if not feed_items or current_index >= len(feed_items):
|
954 |
-
return """
|
955 |
-
<div id="feed-container" style="
|
956 |
-
display: flex;
|
957 |
-
flex-direction: column;
|
958 |
-
align-items: center;
|
959 |
-
justify-content: center;
|
960 |
-
max-width: 360px;
|
961 |
-
margin: 0 auto;
|
962 |
-
background-color: #000;
|
963 |
-
height: 640px;
|
964 |
-
border: 1px solid #333;
|
965 |
-
border-radius: 10px;
|
966 |
-
color: white;
|
967 |
-
font-family: Arial, sans-serif;
|
968 |
-
">
|
969 |
-
<p>Enter a concept or idea to start your feed!</p>
|
970 |
-
</div>
|
971 |
-
"""
|
972 |
-
|
973 |
-
item = feed_items[current_index]
|
974 |
-
# Check if there is a video to display
|
975 |
-
if item['video_base64']:
|
976 |
-
media_element = f"""
|
977 |
-
<video id="feed-video" controls style="
|
978 |
-
width: 100%;
|
979 |
-
height: 100%;
|
980 |
-
object-fit: cover;
|
981 |
-
position: absolute;
|
982 |
-
top: 0;
|
983 |
-
left: 0;
|
984 |
-
z-index: 1;
|
985 |
-
">
|
986 |
-
<source src="data:video/mp4;base64,{item['video_base64']}" type="video/mp4">
|
987 |
-
Your browser does not support the video tag.
|
988 |
-
</video>
|
989 |
-
"""
|
990 |
-
else:
|
991 |
-
# Fallback to image if no video is available
|
992 |
-
media_element = f"""
|
993 |
-
<img id="feed-image" src="data:image/png;base64,{item['image_base64']}" style="
|
994 |
-
width: 100%;
|
995 |
-
height: 100%;
|
996 |
-
object-fit: cover;
|
997 |
-
position: absolute;
|
998 |
-
top: 0;
|
999 |
-
left: 0;
|
1000 |
-
z-index: 1;
|
1001 |
-
">
|
1002 |
-
"""
|
1003 |
-
|
1004 |
-
html_str = f"""
|
1005 |
-
<div id="feed-container" style="
|
1006 |
-
display: flex;
|
1007 |
-
flex-direction: column;
|
1008 |
-
align-items: center;
|
1009 |
-
max-width: 360px;
|
1010 |
-
margin: 0 auto;
|
1011 |
-
background-color: #000;
|
1012 |
-
height: 640px;
|
1013 |
-
border: 1px solid #333;
|
1014 |
-
border-radius: 10px;
|
1015 |
-
position: relative;
|
1016 |
-
">
|
1017 |
-
<div class="feed-item" style="
|
1018 |
-
width: 100%;
|
1019 |
-
height: 100%;
|
1020 |
-
position: relative;
|
1021 |
-
display: flex;
|
1022 |
-
flex-direction: column;
|
1023 |
-
justify-content: flex-end;
|
1024 |
-
overflow: hidden;
|
1025 |
-
cursor: pointer;
|
1026 |
-
" onclick="handleClick(event)">
|
1027 |
-
{media_element}
|
1028 |
-
<div style="
|
1029 |
-
position: relative;
|
1030 |
-
z-index: 2;
|
1031 |
-
background: linear-gradient(to top, rgba(0,0,0,0.7), transparent);
|
1032 |
-
padding: 20px;
|
1033 |
-
color: white;
|
1034 |
-
font-family: Arial, sans-serif;
|
1035 |
-
font-size: 18px;
|
1036 |
-
font-weight: bold;
|
1037 |
-
text-shadow: 1px 1px 2px rgba(0,0,0,0.5);
|
1038 |
-
">
|
1039 |
-
{item['text']}
|
1040 |
-
</div>
|
1041 |
-
</div>
|
1042 |
-
</div>
|
1043 |
-
<script>
|
1044 |
-
function handleClick(event) {{
|
1045 |
-
const media = document.getElementById('feed-video') || document.getElementById('feed-image');
|
1046 |
-
const rect = media.getBoundingClientRect();
|
1047 |
-
const clickX = event.clientX - rect.left;
|
1048 |
-
const width = rect.width;
|
1049 |
-
if (clickX > width * 0.75) {{
|
1050 |
-
document.getElementById('previous-button').click();
|
1051 |
-
}}
|
1052 |
-
}}
|
1053 |
-
</script>
|
1054 |
-
<button id="previous-button" style="display: none;" onclick="document.getElementById('previous-button').click()"></button>
|
1055 |
-
"""
|
1056 |
-
return html_str
|
1057 |
-
|
1058 |
-
# Define the Gradio interface
|
1059 |
-
with gr.Blocks(
|
1060 |
-
css="""
|
1061 |
-
body { background-color: #000; color: #fff; font-family: Arial, sans-serif; }
|
1062 |
-
.gradio-container { max-width: 400px; margin: 0 auto; padding: 10px; }
|
1063 |
-
input, select, button, .gr-checkbox { border-radius: 5px; background-color: #222; color: #fff; border: 1px solid #444; }
|
1064 |
-
button { background-color: #ff2d55; border: none; }
|
1065 |
-
button:hover { background-color: #e0264b; }
|
1066 |
-
.gr-button { width: 100%; margin-top: 10px; }
|
1067 |
-
.gr-form { background-color: #111; padding: 15px; border-radius: 10px; }
|
1068 |
-
""",
|
1069 |
-
title="Create Your Feed"
|
1070 |
-
) as demo:
|
1071 |
-
# State variables
|
1072 |
current_user_input = gr.State(value="")
|
1073 |
current_index = gr.State(value=0)
|
1074 |
feed_items = gr.State(value=[])
|
1075 |
is_loading = gr.State(value=False)
|
1076 |
-
share_links = gr.State(value="")
|
1077 |
|
1078 |
-
# Input section
|
1079 |
with gr.Column(elem_classes="gr-form"):
|
1080 |
gr.Markdown("### Create Your Feed")
|
1081 |
-
user_input = gr.Textbox(
|
1082 |
-
|
1083 |
-
value="",
|
1084 |
-
placeholder="e.g., sushi adventure, neon tech",
|
1085 |
-
submit_btn=False
|
1086 |
-
)
|
1087 |
-
generate_video_checkbox = gr.Checkbox(
|
1088 |
-
label="Generate Video (may take longer)",
|
1089 |
-
value=False
|
1090 |
-
)
|
1091 |
magic_button = gr.Button("✨ Generate Next Item", elem_classes="gr-button")
|
1092 |
|
1093 |
-
|
1094 |
-
progress_html = gr.HTML(label="Progress", visible=True)
|
1095 |
feed_html = gr.HTML()
|
1096 |
share_html = gr.HTML(label="Share this item:")
|
1097 |
|
1098 |
-
|
1099 |
-
|
1100 |
-
|
1101 |
-
|
1102 |
-
|
1103 |
-
|
1104 |
-
|
1105 |
-
|
1106 |
-
# Handle magic button click to generate next item
|
1107 |
-
magic_button.click(
|
1108 |
-
fn=load_next,
|
1109 |
-
inputs=[user_input, generate_video_checkbox, current_index, feed_items],
|
1110 |
-
outputs=[current_user_input, current_index, feed_items, feed_html, share_html, is_loading, progress_html]
|
1111 |
-
)
|
1112 |
-
|
1113 |
-
# Hidden button for previous item navigation
|
1114 |
-
previous_button = gr.Button("Previous", elem_id="previous-button", visible=False)
|
1115 |
-
|
1116 |
-
# Handle click to go to previous item
|
1117 |
-
previous_button.click(
|
1118 |
-
fn=load_previous,
|
1119 |
-
inputs=[user_input, generate_video_checkbox, current_index, feed_items],
|
1120 |
-
outputs=[current_user_input, current_index, feed_items, feed_html, share_html, is_loading, progress_html]
|
1121 |
-
)
|
1122 |
|
1123 |
-
# Launch the app
|
1124 |
demo.launch()
|
|
|
9 |
import random
|
10 |
import urllib.parse
|
11 |
import time
|
|
|
12 |
|
13 |
# Check Gradio version
|
14 |
required_version = "4.44.0"
|
15 |
+
current_version = gr.__version__
|
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
|
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 |
+
# Safety settings to disable all filters
|
27 |
SAFETY_SETTINGS = [
|
28 |
+
types.SafetySetting(category=types.HarmCategory.HARM_CATEGORY_HARASSMENT, threshold=types.HarmBlockThreshold.BLOCK_NONE),
|
29 |
+
types.SafetySetting(category=types.HarmCategory.HARM_CATEGORY_HATE_SPEECH, threshold=types.HarmBlockThreshold.BLOCK_NONE),
|
30 |
+
types.SafetySetting(category=types.HarmCategory.HARM_CATEGORY_SEXUALLY_EXPLICIT, threshold=types.HarmBlockThreshold.BLOCK_NONE),
|
31 |
+
types.SafetySetting(category=types.HarmCategory.HARM_CATEGORY_DANGEROUS_CONTENT, threshold=types.HarmBlockThreshold.BLOCK_NONE),
|
32 |
+
types.SafetySetting(category=types.HarmCategory.HARM_CATEGORY_CIVIC_INTEGRITY, threshold=types.HarmBlockThreshold.BLOCK_NONE),
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
33 |
]
|
34 |
|
35 |
def clean_response_text(response_text):
|
36 |
+
"""Clean API response by removing Markdown code block markers."""
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
37 |
cleaned_text = response_text.strip()
|
38 |
if cleaned_text.startswith("```json"):
|
39 |
cleaned_text = cleaned_text[len("```json"):].strip()
|
|
|
42 |
return cleaned_text
|
43 |
|
44 |
def generate_ideas(user_input):
|
45 |
+
"""Generate 5 creative TikTok video ideas based on user input."""
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
46 |
yield (10, f"Brainstorming epic ideas for {user_input}... 🌟")
|
47 |
|
48 |
prompt = f"""
|
49 |
+
The user has provided the concept: "{user_input}". Generate 5 diverse and creative ideas for a TikTok video explicitly related to "{user_input}".
|
|
|
50 |
Each idea should be a short sentence describing a specific scene or concept.
|
51 |
+
Return the response as a JSON object with a key 'ideas' containing a list of 5 ideas.
|
|
|
52 |
Example for "blindfolded Rubik's Cube challenge":
|
53 |
{{"ideas": [
|
54 |
"A blindfolded speedcubing competition with dramatic music",
|
|
|
62 |
response = client.models.generate_content(
|
63 |
model='gemini-2.0-flash-lite',
|
64 |
contents=[prompt],
|
65 |
+
config=types.GenerateContentConfig(temperature=1.2, safety_settings=SAFETY_SETTINGS)
|
|
|
|
|
|
|
66 |
)
|
|
|
|
|
|
|
67 |
cleaned_text = clean_response_text(response.text)
|
68 |
response_json = json.loads(cleaned_text)
|
69 |
+
if 'ideas' not in response_json or len(response_json['ideas']) != 5:
|
70 |
+
raise ValueError("Invalid JSON format or incorrect number of ideas")
|
|
|
71 |
ideas = response_json['ideas']
|
72 |
yield (20, f"Ideas locked in for {user_input}! 🚀")
|
73 |
return ideas
|
|
|
74 |
except Exception as e:
|
75 |
print(f"Error generating ideas: {e}")
|
76 |
yield (20, f"Oops, tweaking the plan for {user_input}... 🔧")
|
|
|
82 |
f"An action-packed {user_input} challenge with dynamic angles"
|
83 |
]
|
84 |
|
85 |
+
def generate_item(user_input, ideas, generate_video=False):
|
86 |
+
"""Generate a feed item (image and optionally video) with progress updates."""
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
87 |
video_base64 = None
|
88 |
+
max_attempts = 3
|
89 |
|
90 |
total_attempts = 0
|
91 |
+
while total_attempts < max_attempts:
|
92 |
total_attempts += 1
|
93 |
yield (20 + total_attempts * 10, f"Attempt {total_attempts} to craft your {user_input} masterpiece... 🎨")
|
94 |
|
95 |
+
selected_idea = random.choice(ideas)
|
96 |
+
prompt = f"""
|
97 |
+
The user has provided the concept: "{user_input}". Based on this and the idea "{selected_idea}", create content for a TikTok video.
|
98 |
+
Return a JSON object with:
|
99 |
+
- 'caption': A short, viral TikTok-style caption with hashtags reflecting "{user_input}".
|
100 |
+
- 'image_prompt': A detailed image prompt for a high-quality visual scene, no text or letters.
|
101 |
+
Example: {{"caption": "Blindfolded Rubik's Cube MAGIC! 🤯 #rubiks", "image_prompt": "A close-up view of a person solving a Rubik's Cube blindfolded, dramatic style"}}
|
102 |
+
"""
|
103 |
+
try:
|
104 |
+
response = client.models.generate_content(
|
105 |
+
model='gemini-2.0-flash-lite',
|
106 |
+
contents=[prompt],
|
107 |
+
config=types.GenerateContentConfig(temperature=1.2, safety_settings=SAFETY_SETTINGS)
|
108 |
+
)
|
109 |
+
cleaned_text = clean_response_text(response.text)
|
110 |
+
response_json = json.loads(cleaned_text)
|
111 |
+
text = response_json['caption']
|
112 |
+
image_prompt = response_json['image_prompt']
|
113 |
+
except Exception as e:
|
114 |
+
print(f"Error generating item: {e}")
|
115 |
+
text = f"Amazing {user_input}! 🔥 #{user_input.replace(' ', '')}"
|
116 |
+
image_prompt = f"A vivid scene of {selected_idea} related to {user_input}, vibrant pop art style, no text"
|
117 |
+
|
118 |
+
# Generate image
|
119 |
+
try:
|
120 |
+
yield (40, f"Rendering your {user_input} vision... ✨")
|
121 |
+
imagen = client.models.generate_images(
|
122 |
+
model='imagen-3.0-generate-002',
|
123 |
+
prompt=image_prompt,
|
124 |
+
config=types.GenerateImagesConfig(aspect_ratio="9:16", number_of_images=1)
|
125 |
+
)
|
126 |
+
if imagen.generated_images:
|
127 |
+
image = Image.open(BytesIO(imagen.generated_images[0].image.image_bytes))
|
128 |
+
image = image.resize((360, 640), Image.LANCZOS)
|
129 |
+
buffered = BytesIO()
|
130 |
+
image.save(buffered, format="PNG")
|
131 |
+
img_str = base64.b64encode(buffered.getvalue()).decode()
|
132 |
+
yield (50, f"Image for {user_input} is ready! 🎉")
|
133 |
+
break
|
134 |
+
except Exception as e:
|
135 |
+
print(f"Error generating image: {e}")
|
136 |
+
if total_attempts == max_attempts:
|
137 |
+
image = Image.new('RGB', (360, 640), color='gray')
|
138 |
+
buffered = BytesIO()
|
139 |
+
image.save(buffered, format="PNG")
|
140 |
+
img_str = base64.b64encode(buffered.getvalue()).decode()
|
141 |
+
yield (60, f"Using a placeholder for {user_input}... 🖼️")
|
142 |
+
return {'text': text, 'image_base64': img_str, 'video_base64': None, 'ideas': ideas}
|
143 |
+
continue
|
144 |
+
|
145 |
+
# Generate video if requested
|
146 |
+
if generate_video:
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
147 |
try:
|
148 |
yield (60, f"Filming a viral video for {user_input}... 🎥")
|
|
|
|
|
|
|
|
|
|
|
|
|
149 |
operation = client.models.generate_videos(
|
150 |
model="veo-2.0-generate-001",
|
151 |
+
prompt=f"A close-up slow dolly shot of {image_prompt}, realistic style",
|
152 |
+
image=imagen.generated_images[0].image,
|
153 |
+
config=types.GenerateVideosConfig(aspect_ratio="9:16", duration_seconds=8)
|
|
|
|
|
|
|
|
|
|
|
154 |
)
|
|
|
155 |
while not operation.done:
|
156 |
time.sleep(20)
|
157 |
operation = client.operations.get(operation)
|
158 |
+
if operation.response and operation.response.generated_videos:
|
159 |
+
video_data = client.files.download(file=operation.response.generated_videos[0].video)
|
160 |
+
video_bytes = video_data if isinstance(video_data, bytes) else BytesIO(video_data).getvalue()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
161 |
video_base64 = base64.b64encode(video_bytes).decode()
|
|
|
162 |
yield (90, f"Video for {user_input} is a wrap! 🎬")
|
163 |
+
return {'text': text, 'image_base64': img_str, 'video_base64': video_base64, 'ideas': ideas}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
164 |
except Exception as e:
|
165 |
+
print(f"Error generating video: {e}")
|
166 |
+
yield (70, f"Skipping video for {user_input}... 📌")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
167 |
|
168 |
+
yield (95, f"Polishing your {user_input} masterpiece... ✨")
|
169 |
+
return {'text': text, 'image_base64': img_str, 'video_base64': video_base64, 'ideas': ideas}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
170 |
|
171 |
def generate_progress_html(progress, message, user_input):
|
172 |
+
"""Generate HTML for the progress bar."""
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
173 |
return f"""
|
174 |
<div id="progress-container" style="
|
175 |
+
display: flex; flex-direction: column; align-items: center; justify-content: center;
|
176 |
+
max-width: 360px; margin: 0 auto; background-color: #000; height: 200px;
|
177 |
+
border: 1px solid #333; border-radius: 10px; color: white; font-family: Arial, sans-serif;">
|
178 |
+
<div id="loading-message" style="font-size: 18px; font-weight: bold; text-align: center; margin-bottom: 20px;">
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
179 |
{message}
|
180 |
</div>
|
181 |
+
<div style="width: 80%; height: 10px; background-color: #333; border-radius: 5px; overflow: hidden;">
|
182 |
+
<div id="progress-bar" style="width: {progress}%; height: 100%; background: linear-gradient(to right, #ff2d55, #ff5e78);"></div>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
183 |
</div>
|
184 |
+
<div style="margin-top: 10px; font-size: 14px; color: #ccc;">{int(progress)}% Complete</div>
|
185 |
<style>
|
186 |
+
@keyframes pulse {{ 0% {{ opacity: 1; }} 50% {{ opacity: 0.5; }} 100% {{ opacity: 1; }} }}
|
187 |
+
#loading-message {{ animation: pulse 2s infinite; }}
|
|
|
|
|
|
|
|
|
|
|
|
|
188 |
</style>
|
189 |
</div>
|
190 |
"""
|
191 |
|
192 |
+
def generate_html(feed_items, manual_upload, current_index, user_input, is_loading):
|
193 |
+
"""Generate HTML for the feed display."""
|
194 |
+
if not feed_items or current_index >= len(feed_items):
|
195 |
+
return "<div style='color: white; text-align: center;'>No content yet!</div>"
|
196 |
+
item = feed_items[current_index]
|
197 |
+
media_html = f'<img src="data:image/png;base64,{item["image_base64"]}" style="width: 360px; height: 640px; object-fit: cover;" />'
|
198 |
+
if item['video_base64']:
|
199 |
+
media_html += f'<video controls src="data:video/mp4;base64,{item["video_base64"]}" style="width: 360px; height: 640px;"></video>'
|
200 |
+
return f"""
|
201 |
+
<div style="max-width: 360px; margin: 0 auto; background-color: #000; border: 1px solid #333; border-radius: 10px; color: white;">
|
202 |
+
{media_html}
|
203 |
+
<p style="padding: 10px;">{item['text']}</p>
|
204 |
+
</div>
|
|
|
205 |
"""
|
206 |
+
|
207 |
+
def generate_share_links(image_base64, video_base64, text):
|
208 |
+
"""Generate YouTube share links."""
|
209 |
+
share_text = urllib.parse.quote(f"{text} Check out this cool content!")
|
210 |
+
return f'<a href="https://www.youtube.com/upload?description={share_text}" target="_blank">Share on YouTube</a>'
|
211 |
+
|
212 |
+
def start_feed(user_input, generate_video, current_index, feed_items):
|
213 |
+
"""Start or reset the feed with a new item."""
|
214 |
+
user_input = user_input.strip() or "trending"
|
215 |
current_user_input = user_input
|
|
|
|
|
216 |
is_loading = True
|
217 |
progress_html = generate_progress_html(0, f"Getting started with {user_input}... 🚀", user_input)
|
218 |
+
yield (current_user_input, current_index, feed_items, gr.update(), gr.update(), is_loading, progress_html)
|
|
|
219 |
|
220 |
try:
|
|
|
221 |
ideas_gen = generate_ideas(user_input)
|
222 |
ideas = None
|
223 |
for update in ideas_gen:
|
224 |
if isinstance(update, tuple):
|
225 |
progress, message = update
|
226 |
progress_html = generate_progress_html(progress, message, user_input)
|
227 |
+
yield (gr.update(), gr.update(), gr.update(), gr.update(), gr.update(), is_loading, progress_html)
|
228 |
else:
|
229 |
ideas = update
|
230 |
|
231 |
+
item_gen = generate_item(user_input, ideas, generate_video)
|
|
|
232 |
item = None
|
233 |
for update in item_gen:
|
234 |
if isinstance(update, tuple):
|
235 |
progress, message = update
|
236 |
progress_html = generate_progress_html(progress, message, user_input)
|
237 |
+
yield (gr.update(), gr.update(), gr.update(), gr.update(), gr.update(), is_loading, progress_html)
|
238 |
else:
|
239 |
item = update
|
240 |
|
241 |
feed_items = [item]
|
242 |
current_index = 0
|
243 |
feed_html = generate_html(feed_items, False, current_index, user_input, is_loading=False)
|
244 |
+
share_html = generate_share_links(item['image_base64'], item['video_base64'], item['text'])
|
245 |
+
yield (current_user_input, current_index, feed_items, feed_html, share_html, False, "")
|
|
|
|
|
|
|
246 |
except Exception as e:
|
247 |
print(f"Error in start_feed: {e}")
|
248 |
+
feed_html = "<div style='color: white; text-align: center;'>Error generating content. Try again!</div>"
|
249 |
+
yield (current_user_input, current_index, feed_items, feed_html, "", False, generate_progress_html(100, "Oops, something went wrong! 😅", user_input))
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
250 |
|
251 |
def load_next(user_input, generate_video, current_index, feed_items):
|
252 |
+
"""Load the next item in the feed."""
|
253 |
+
current_user_input = user_input.strip() or "trending"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
254 |
is_loading = True
|
255 |
+
progress_html = generate_progress_html(0, f"Loading next {current_user_input} vibe... 🚀", current_user_input)
|
256 |
+
yield (current_user_input, current_index, feed_items, gr.update(), gr.update(), is_loading, progress_html)
|
|
|
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 {current_user_input} moment... 🔄", current_user_input)
|
262 |
+
yield (gr.update(), gr.update(), gr.update(), gr.update(), gr.update(), is_loading, progress_html)
|
263 |
else:
|
264 |
ideas = feed_items[-1]['ideas'] if feed_items else None
|
265 |
if not ideas:
|
266 |
+
ideas_gen = generate_ideas(current_user_input)
|
|
|
267 |
for update in ideas_gen:
|
268 |
if isinstance(update, tuple):
|
269 |
progress, message = update
|
270 |
+
progress_html = generate_progress_html(progress, message, current_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 |
+
item_gen = generate_item(current_user_input, ideas, generate_video)
|
276 |
+
for update in item_gen:
|
|
|
|
|
277 |
if isinstance(update, tuple):
|
278 |
progress, message = update
|
279 |
+
progress_html = generate_progress_html(progress, message, current_user_input)
|
280 |
+
yield (gr.update(), gr.update(), gr.update(), gr.update(), gr.update(), is_loading, progress_html)
|
281 |
else:
|
282 |
+
feed_items.append(update)
|
283 |
+
current_index = len(feed_items) - 1
|
|
|
|
|
284 |
|
285 |
+
feed_html = generate_html(feed_items, False, current_index, current_user_input, is_loading=False)
|
286 |
+
share_html = generate_share_links(feed_items[current_index]['image_base64'], feed_items[current_index]['video_base64'], feed_items[current_index]['text'])
|
287 |
+
yield (current_user_input, current_index, feed_items, feed_html, share_html, False, "")
|
|
|
|
|
|
|
288 |
except Exception as e:
|
289 |
print(f"Error in load_next: {e}")
|
290 |
+
feed_html = "<div style='color: white; text-align: center;'>Error generating content. Try again!</div>"
|
291 |
+
yield (current_user_input, current_index, feed_items, feed_html, "", False, generate_progress_html(100, "Oops, something went wrong! 😅", current_user_input))
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
292 |
|
293 |
def load_previous(user_input, generate_video, current_index, feed_items):
|
294 |
+
"""Load the previous item in the feed."""
|
295 |
+
current_user_input = user_input.strip() or "trending"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
296 |
if current_index > 0:
|
297 |
current_index -= 1
|
298 |
+
feed_html = generate_html(feed_items, False, current_index, current_user_input, is_loading=False)
|
299 |
+
share_html = generate_share_links(feed_items[current_index]['image_base64'], feed_items[current_index]['video_base64'], feed_items[current_index]['text'])
|
300 |
+
return current_user_input, current_index, feed_items, feed_html, share_html, False, ""
|
301 |
+
|
302 |
+
# Gradio Interface
|
303 |
+
with gr.Blocks(css="""
|
304 |
+
body { background-color: #000; color: #fff; font-family: Arial, sans-serif; }
|
305 |
+
.gradio-container { max-width: 400px; margin: 0 auto; padding: 10px; }
|
306 |
+
input, button { border-radius: 5px; background-color: #222; color: #fff; border: 1px solid #444; }
|
307 |
+
button { background-color: #ff2d55; }
|
308 |
+
button:hover { background-color: #e0264b; }
|
309 |
+
.gr-button { width: 100%; margin-top: 10px; }
|
310 |
+
.gr-form { background-color: #111; padding: 15px; border-radius: 10px; }
|
311 |
+
""", title="Create Your Feed") as demo:
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
312 |
current_user_input = gr.State(value="")
|
313 |
current_index = gr.State(value=0)
|
314 |
feed_items = gr.State(value=[])
|
315 |
is_loading = gr.State(value=False)
|
|
|
316 |
|
|
|
317 |
with gr.Column(elem_classes="gr-form"):
|
318 |
gr.Markdown("### Create Your Feed")
|
319 |
+
user_input = gr.Textbox(label="Enter Concept or Ideas", placeholder="e.g., jogging in gardens by the bay")
|
320 |
+
generate_video_checkbox = gr.Checkbox(label="Generate Video (may take longer)", value=False)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
321 |
magic_button = gr.Button("✨ Generate Next Item", elem_classes="gr-button")
|
322 |
|
323 |
+
progress_html = gr.HTML(label="Progress")
|
|
|
324 |
feed_html = gr.HTML()
|
325 |
share_html = gr.HTML(label="Share this item:")
|
326 |
|
327 |
+
user_input.submit(start_feed, [user_input, generate_video_checkbox, current_index, feed_items],
|
328 |
+
[current_user_input, current_index, feed_items, feed_html, share_html, is_loading, progress_html])
|
329 |
+
magic_button.click(load_next, [user_input, generate_video_checkbox, current_index, feed_items],
|
330 |
+
[current_user_input, current_index, feed_items, feed_html, share_html, is_loading, progress_html])
|
331 |
+
previous_button = gr.Button("Previous", visible=False).click(load_previous,
|
332 |
+
[user_input, generate_video_checkbox, current_index, feed_items],
|
333 |
+
[current_user_input, current_index, feed_items, feed_html, share_html, is_loading, progress_html])
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
334 |
|
|
|
335 |
demo.launch()
|