Spaces:
Running
Running
Commit
·
0f05fa7
1
Parent(s):
89ea8e4
refactor: integrate Modal API for video generation and update LoRA configurations
Browse files
app.py
CHANGED
@@ -10,7 +10,6 @@ from google.cloud import storage
|
|
10 |
import json
|
11 |
from pathlib import Path
|
12 |
import mimetypes
|
13 |
-
from workflow_handler import WanVideoWorkflow
|
14 |
from video_config import MODEL_FRAME_RATES, calculate_frames
|
15 |
import asyncio
|
16 |
from openai import OpenAI
|
@@ -21,71 +20,76 @@ from google.oauth2 import service_account
|
|
21 |
dotenv.load_dotenv()
|
22 |
|
23 |
SCRIPT_DIR = Path(__file__).parent
|
24 |
-
|
25 |
-
|
|
|
|
|
26 |
|
27 |
loras = [
|
28 |
-
|
29 |
-
"image": "https://
|
30 |
-
"id": "
|
31 |
-
"title": "
|
32 |
"example_prompt": "The video shows a man with a slight smile, then the j432mpscare jumpscare occurs, revealing a distorted and monstrous face with glowing red eyes, filling the frame and accompanied by a loud scream."
|
33 |
},
|
34 |
-
|
35 |
-
"image": "https://
|
36 |
-
"id": "
|
37 |
-
"title": "
|
38 |
-
"example_prompt": "The video
|
39 |
},
|
40 |
-
|
41 |
-
|
42 |
-
"
|
43 |
-
"
|
44 |
-
"
|
45 |
-
"example_prompt": "The video shows Pluto the dog, wearing a red collar, who is smiling wide, then his mouth transforms into a dr0p_j88 comical jaw drop, extending down in a long, rectangular shape, and revealing his tongue and teeth."
|
46 |
},
|
47 |
-
|
48 |
-
"image": "https://
|
49 |
-
"id": "
|
50 |
-
"title": "
|
51 |
-
"example_prompt": "
|
52 |
},
|
53 |
-
|
54 |
-
"image": "https://
|
55 |
-
"id": "
|
56 |
-
"title": "
|
57 |
-
"example_prompt": "
|
58 |
},
|
59 |
-
|
60 |
-
|
61 |
-
"
|
62 |
-
"
|
63 |
-
"
|
|
|
64 |
},
|
65 |
-
|
66 |
-
|
67 |
-
"
|
68 |
-
"
|
69 |
-
"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
70 |
},
|
71 |
-
|
72 |
-
"image": "https://huggingface.co/Remade-AI/
|
73 |
-
"id": "
|
74 |
-
"title": "
|
75 |
-
"example_prompt": "
|
76 |
-
},
|
77 |
-
|
78 |
-
"image": "https://huggingface.co/Remade-AI/
|
79 |
-
"id": "
|
80 |
-
"title": "
|
81 |
-
"example_prompt": "The video shows
|
82 |
},
|
83 |
-
|
84 |
-
|
85 |
-
"id": "b05c1dc7-a71c-4d24-b512-4877a12dea7e",
|
86 |
-
"title": "Cakeify",
|
87 |
-
"example_prompt": "The video opens on a woman. A knife, held by a hand, is coming into frame and hovering over the woman. The knife then begins cutting into the woman to c4k3 cakeify it. As the knife slices the woman open, the inside of the woman is revealed to be cake with chocolate layers. The knife cuts through and the contents of the woman are revealed."
|
88 |
-
},
|
89 |
]
|
90 |
|
91 |
# Initialize Supabase client with async support
|
@@ -241,9 +245,21 @@ def build_lora_prompt(subject, lora_id):
|
|
241 |
raise ValueError(f"Unknown LoRA ID: {lora_id} and no example prompt available")
|
242 |
|
243 |
def poll_generation_status(generation_id):
|
244 |
-
"""Poll generation status from database"""
|
245 |
try:
|
246 |
-
#
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
247 |
response = supabase.table('generations') \
|
248 |
.select('*') \
|
249 |
.eq('generation_id', generation_id) \
|
@@ -280,7 +296,7 @@ async def moderate_prompt(prompt: str) -> dict:
|
|
280 |
|
281 |
# Additional checks for keywords related to minors or inappropriate content
|
282 |
keywords = [
|
283 |
-
"child", "kid", "minor", "teen", "
|
284 |
"naked", "nude", "nsfw", "porn", "xxx", "sex", "explicit",
|
285 |
"inappropriate", "adult content"
|
286 |
]
|
@@ -345,27 +361,18 @@ async def moderate_image(image_path: str) -> dict:
|
|
345 |
|
346 |
# Get likelihood scores
|
347 |
adult_score = likelihood_values[safe_search.adult]
|
348 |
-
|
349 |
-
violence_score = likelihood_values[safe_search.violence]
|
350 |
-
medical_score = likelihood_values[safe_search.medical]
|
351 |
|
352 |
# Determine if content is NSFW according to Vision API
|
353 |
vision_reasons = []
|
354 |
if adult_score >= 3: # LIKELY or VERY_LIKELY
|
355 |
vision_reasons.append("adult content")
|
356 |
-
|
357 |
-
vision_reasons.append("suggestive content")
|
358 |
-
if violence_score >= 3: # LIKELY or VERY_LIKELY
|
359 |
-
vision_reasons.append("violent content")
|
360 |
-
|
361 |
|
362 |
# Print Vision API results
|
363 |
print("Google Cloud Vision API Results:")
|
364 |
print(f"Adult: {vision.Likelihood(safe_search.adult).name}")
|
365 |
-
|
366 |
-
print(f"Violence: {vision.Likelihood(safe_search.violence).name}")
|
367 |
-
print(f"Medical: {vision.Likelihood(safe_search.medical).name}")
|
368 |
-
|
369 |
except Exception as vision_error:
|
370 |
print(f"Error with Vision API: {vision_error}")
|
371 |
vision_reasons = [] # Continue with OpenAI check if Vision API fails
|
@@ -485,15 +492,8 @@ Be extremely cautious - if there's any doubt, mark it as NSFW."""
|
|
485 |
"reason": "Failed to verify content safety - please try again"
|
486 |
}
|
487 |
|
488 |
-
async def generate_video(input_image, subject,
|
489 |
try:
|
490 |
-
# Initialize workflow handler with explicit paths
|
491 |
-
workflow_handler = WanVideoWorkflow(
|
492 |
-
supabase,
|
493 |
-
config_path=str(CONFIG_PATH),
|
494 |
-
workflow_path=str(WORKFLOW_PATH)
|
495 |
-
)
|
496 |
-
|
497 |
# Check if the input is a URL (example image) or a file path (user upload)
|
498 |
if input_image.startswith('http'):
|
499 |
# It's already a URL, use it directly
|
@@ -502,12 +502,8 @@ async def generate_video(input_image, subject, duration, selected_index, progres
|
|
502 |
# It's a file path, upload to GCS
|
503 |
image_url = upload_to_gcs(input_image)
|
504 |
|
505 |
-
#
|
506 |
-
|
507 |
-
"Short (3s)": 3,
|
508 |
-
"Long (5s)": 5
|
509 |
-
}
|
510 |
-
video_duration = duration_mapping[duration]
|
511 |
|
512 |
# Get LoRA config
|
513 |
lora_config = next((lora for lora in loras if lora["id"] == selected_index), None)
|
@@ -517,58 +513,59 @@ async def generate_video(input_image, subject, duration, selected_index, progres
|
|
517 |
# Generate unique ID
|
518 |
generation_id = str(uuid.uuid4())
|
519 |
|
520 |
-
#
|
521 |
-
prompt =
|
522 |
-
|
523 |
-
|
524 |
-
|
525 |
-
|
526 |
-
|
527 |
-
|
528 |
-
#
|
529 |
-
|
530 |
-
|
531 |
-
|
532 |
-
|
533 |
-
|
534 |
-
|
535 |
-
|
536 |
-
|
537 |
-
|
538 |
-
|
539 |
-
|
540 |
-
"
|
541 |
-
"
|
542 |
-
|
543 |
-
|
544 |
-
|
545 |
-
|
546 |
-
"
|
547 |
-
|
548 |
-
|
549 |
-
|
550 |
-
|
551 |
-
|
552 |
-
|
553 |
-
|
554 |
-
|
555 |
-
|
556 |
-
|
557 |
-
|
558 |
-
|
559 |
-
|
560 |
-
|
561 |
-
|
562 |
-
|
563 |
-
|
564 |
-
|
565 |
-
|
566 |
-
|
567 |
-
|
568 |
-
|
569 |
-
|
570 |
-
|
571 |
-
|
|
|
572 |
|
573 |
except Exception as e:
|
574 |
print(f"Error in generate_video: {e}")
|
@@ -579,7 +576,7 @@ def update_selection(evt: gr.SelectData):
|
|
579 |
sentence = f"Selected LoRA: {selected_lora['title']}"
|
580 |
return selected_lora['id'], sentence
|
581 |
|
582 |
-
async def handle_generation(image_input, subject,
|
583 |
try:
|
584 |
if selected_index is None:
|
585 |
raise gr.Error("You must select a LoRA before proceeding.")
|
@@ -588,28 +585,36 @@ async def handle_generation(image_input, subject, duration, selected_index, prog
|
|
588 |
prompt_moderation = await moderate_prompt(subject)
|
589 |
print(f"Prompt moderation result: {prompt_moderation}")
|
590 |
if prompt_moderation["isNSFW"]:
|
|
|
|
|
591 |
raise gr.Error(f"Content moderation failed: {prompt_moderation['reason']}")
|
592 |
|
593 |
# Then, moderate the image
|
594 |
image_moderation = await moderate_image(image_input)
|
595 |
print(f"Image moderation result: {image_moderation}")
|
596 |
if image_moderation["isNSFW"]:
|
|
|
|
|
597 |
raise gr.Error(f"Content moderation failed: {image_moderation['reason']}")
|
598 |
|
599 |
# Finally, check the combination
|
600 |
combined_moderation = await moderate_combined(subject, image_input)
|
601 |
print(f"Combined moderation result: {combined_moderation}")
|
602 |
if combined_moderation["isNSFW"]:
|
|
|
|
|
603 |
raise gr.Error(f"Content moderation failed: {combined_moderation['reason']}")
|
604 |
|
605 |
# Generate the video and get generation ID
|
606 |
-
generation_id = await generate_video(image_input, subject,
|
607 |
|
608 |
# Poll for status updates
|
609 |
while True:
|
610 |
generation = poll_generation_status(generation_id)
|
611 |
|
612 |
if not generation:
|
|
|
|
|
613 |
raise ValueError(f"Generation {generation_id} not found")
|
614 |
|
615 |
# Update progress
|
@@ -619,20 +624,24 @@ async def handle_generation(image_input, subject, duration, selected_index, prog
|
|
619 |
|
620 |
# Check status
|
621 |
if generation['status'] == 'completed':
|
622 |
-
# Final yield with completed video
|
623 |
-
yield generation['output_url'], generation_id, gr.update(visible=False)
|
624 |
break # Exit the loop
|
625 |
elif generation['status'] == 'error':
|
|
|
|
|
626 |
raise ValueError(f"Generation failed: {generation.get('error')}")
|
627 |
else:
|
628 |
-
# Yield progress update
|
629 |
-
yield None, generation_id, gr.update(value=progress_bar, visible=True)
|
630 |
|
631 |
# Wait before next poll
|
632 |
await asyncio.sleep(2)
|
633 |
|
634 |
except Exception as e:
|
635 |
print(f"Error in handle_generation: {e}")
|
|
|
|
|
636 |
raise e
|
637 |
|
638 |
css = '''
|
@@ -711,7 +720,7 @@ css = '''
|
|
711 |
pointer-events: none;
|
712 |
}
|
713 |
.discord-locked::after {
|
714 |
-
content: "🔒
|
715 |
position: absolute;
|
716 |
top: 50%;
|
717 |
left: 50%;
|
@@ -818,25 +827,20 @@ with gr.Blocks(css=css, theme=gr.themes.Soft(primary_hue="indigo", neutral_hue="
|
|
818 |
selected_index = gr.State(None)
|
819 |
current_generation_id = gr.State(None)
|
820 |
|
821 |
-
# Updated title with
|
822 |
-
gr.Markdown("# Remade AI -
|
823 |
-
|
824 |
-
# Insert an April Fool's themed banner at the top
|
825 |
-
april_banner = gr.HTML(
|
826 |
-
"""
|
827 |
-
<div style="background-color: #8a0ee3; padding: 15px; border-radius: 8px; text-align: center; font-size: 1.5em; margin-bottom: 15px;">
|
828 |
-
🎉 Happy April Fool's Day! Enjoy some playful pranks and fun effects! 🎉
|
829 |
-
</div>
|
830 |
-
"""
|
831 |
-
)
|
832 |
|
833 |
-
#
|
834 |
-
|
835 |
"""
|
836 |
<div class="discord-banner">
|
837 |
-
<h3
|
838 |
-
<p>
|
839 |
-
<a href="https://remade.ai
|
|
|
|
|
|
|
|
|
840 |
</div>
|
841 |
"""
|
842 |
)
|
@@ -856,11 +860,11 @@ with gr.Blocks(css=css, theme=gr.themes.Soft(primary_hue="indigo", neutral_hue="
|
|
856 |
object_fit="contain"
|
857 |
)
|
858 |
|
859 |
-
# Updated Discord/
|
860 |
gr.HTML(
|
861 |
"""
|
862 |
<div class="discord-feature">
|
863 |
-
<span class="discord-feature-title"
|
864 |
</div>
|
865 |
"""
|
866 |
)
|
@@ -888,21 +892,6 @@ with gr.Blocks(css=css, theme=gr.themes.Soft(primary_hue="indigo", neutral_hue="
|
|
888 |
|
889 |
subject = gr.Textbox(label="Describe your subject", placeholder="Cat toy")
|
890 |
|
891 |
-
duration = gr.Radio(
|
892 |
-
["Short (3s)"],
|
893 |
-
label="Duration",
|
894 |
-
value="Short (3s)"
|
895 |
-
)
|
896 |
-
|
897 |
-
# Updated Discord feature callout for additional playful messaging
|
898 |
-
gr.HTML(
|
899 |
-
"""
|
900 |
-
<div class="discord-feature">
|
901 |
-
<span class="discord-feature-title">⏱️ Discord Members:</span> Enjoy extended pranks and video durations on our Discord!
|
902 |
-
</div>
|
903 |
-
"""
|
904 |
-
)
|
905 |
-
|
906 |
with gr.Row():
|
907 |
button = gr.Button("Generate", variant="primary", elem_id="gen_btn")
|
908 |
audio_button = gr.Button("Add Audio 🔒", interactive=False)
|
@@ -911,7 +900,7 @@ with gr.Blocks(css=css, theme=gr.themes.Soft(primary_hue="indigo", neutral_hue="
|
|
911 |
warning_message = gr.HTML(
|
912 |
"""
|
913 |
<div class="warning-message">
|
914 |
-
⚠️ Please DO NOT refresh the page during generation.
|
915 |
</div>
|
916 |
""",
|
917 |
visible=True
|
@@ -920,7 +909,7 @@ with gr.Blocks(css=css, theme=gr.themes.Soft(primary_hue="indigo", neutral_hue="
|
|
920 |
gr.HTML(
|
921 |
"""
|
922 |
<div class="discord-feature">
|
923 |
-
<span class="discord-feature-title">⚡
|
924 |
</div>
|
925 |
"""
|
926 |
)
|
@@ -983,24 +972,42 @@ with gr.Blocks(css=css, theme=gr.themes.Soft(primary_hue="indigo", neutral_hue="
|
|
983 |
if image_input is None:
|
984 |
raise gr.Error("Please upload an image or select an example image.")
|
985 |
|
|
|
|
|
|
|
|
|
986 |
# Use gr.on for the button click with validation
|
987 |
button.click(
|
988 |
fn=check_inputs,
|
989 |
inputs=[subject, image_input, selected_index],
|
990 |
outputs=None,
|
|
|
|
|
|
|
|
|
991 |
).success(
|
992 |
fn=handle_generation,
|
993 |
-
inputs=[image_input, subject,
|
994 |
-
outputs=[output, current_generation_id, progress_bar]
|
995 |
)
|
996 |
|
997 |
# Add a click handler for the disabled audio button
|
998 |
audio_button.click(
|
999 |
-
fn=lambda: gr.Info("
|
1000 |
inputs=None,
|
1001 |
outputs=None
|
1002 |
)
|
1003 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1004 |
if __name__ == "__main__":
|
1005 |
demo.queue(default_concurrency_limit=20)
|
1006 |
demo.launch(ssr_mode=False, share=True)
|
|
|
10 |
import json
|
11 |
from pathlib import Path
|
12 |
import mimetypes
|
|
|
13 |
from video_config import MODEL_FRAME_RATES, calculate_frames
|
14 |
import asyncio
|
15 |
from openai import OpenAI
|
|
|
20 |
dotenv.load_dotenv()
|
21 |
|
22 |
SCRIPT_DIR = Path(__file__).parent
|
23 |
+
|
24 |
+
# Modal configuration
|
25 |
+
MODAL_ENDPOINT = os.getenv('FAL_MODAL_ENDPOINT')
|
26 |
+
MODAL_AUTH_TOKEN = os.getenv('MODAL_AUTH_TOKEN')
|
27 |
|
28 |
loras = [
|
29 |
+
{
|
30 |
+
"image": "https://huggingface.co/Remade-AI/Crash-zoom-out/resolve/main/example_videos/1.gif",
|
31 |
+
"id": "44c05ca1-422d-4cd4-8508-acadb6d0248c",
|
32 |
+
"title": "Crash Zoom Out ",
|
33 |
"example_prompt": "The video shows a man with a slight smile, then the j432mpscare jumpscare occurs, revealing a distorted and monstrous face with glowing red eyes, filling the frame and accompanied by a loud scream."
|
34 |
},
|
35 |
+
{
|
36 |
+
"image": "https://huggingface.co/Remade-AI/Crash-zoom-in/resolve/main/example_videos/1.gif",
|
37 |
+
"id": "34a80641-4702-4c1c-91bf-c436a59c79cb",
|
38 |
+
"title": "Crash Zoom In ",
|
39 |
+
"example_prompt": "The video shows a man with a slight smile, then the j432mpscare jumpscare occurs, revealing a distorted and monstrous face with glowing red eyes, filling the frame and accompanied by a loud scream."
|
40 |
},
|
41 |
+
{
|
42 |
+
"image": "https://huggingface.co/Remade-AI/Car-chase/resolve/main/example_videos/2.gif",
|
43 |
+
"id": "8b36b7fe-0a0b-4849-b0ed-d9a51ff0cc85",
|
44 |
+
"title": "Car Chase",
|
45 |
+
"example_prompt": "The video shows a man with a slight smile, then the j432mpscare jumpscare occurs, revealing a distorted and monstrous face with glowing red eyes, filling the frame and accompanied by a loud scream."
|
|
|
46 |
},
|
47 |
+
{
|
48 |
+
"image": "https://huggingface.co/Remade-AI/Crane-down/resolve/main/example_videos/2.gif",
|
49 |
+
"id": "f26db0b7-1c26-4587-b2b5-1cfd0c51c5b3",
|
50 |
+
"title": "Crane Down ",
|
51 |
+
"example_prompt": "The video shows a man with a slight smile, then the j432mpscare jumpscare occurs, revealing a distorted and monstrous face with glowing red eyes, filling the frame and accompanied by a loud scream."
|
52 |
},
|
53 |
+
{
|
54 |
+
"image": "https://huggingface.co/Remade-AI/Crane_up/resolve/main/example_videos/1.gif",
|
55 |
+
"id": "07c5e22b-7028-437c-9479-6eb9a50cf993",
|
56 |
+
"title": "Crane Up ",
|
57 |
+
"example_prompt": "The video shows a man with a slight smile, then the j432mpscare jumpscare occurs, revealing a distorted and monstrous face with glowing red eyes, filling the frame and accompanied by a loud scream."
|
58 |
},
|
59 |
+
|
60 |
+
{
|
61 |
+
"image": "https://huggingface.co/Remade-AI/Crane_over_the_head/resolve/main/example_videos/1.gif",
|
62 |
+
"id": "9393f8f4-abe6-4aa7-ba01-0b62e1507feb",
|
63 |
+
"title": "Crane Overhead ",
|
64 |
+
"example_prompt": "The video shows a man with a slight smile, then the j432mpscare jumpscare occurs, revealing a distorted and monstrous face with glowing red eyes, filling the frame and accompanied by a loud scream."
|
65 |
},
|
66 |
+
|
67 |
+
{
|
68 |
+
"image": "https://huggingface.co/Remade-AI/matrix-shot/resolve/main/example_videos/1.gif",
|
69 |
+
"id": "219ad5ad-8f23-48dc-b098-b8e6d9fbe6c0",
|
70 |
+
"title": "Matrix Shot ",
|
71 |
+
"example_prompt": "The video shows a man with a slight smile, then the j432mpscare jumpscare occurs, revealing a distorted and monstrous face with glowing red eyes, filling the frame and accompanied by a loud scream."
|
72 |
+
},
|
73 |
+
{
|
74 |
+
"image": "https://huggingface.co/Remade-AI/360-Orbit/resolve/main/example_videos/1.gif",
|
75 |
+
"id": "aaa3e820-5d94-4612-9488-0c9a1b2f5843",
|
76 |
+
"title": "360 Orbit ",
|
77 |
+
"example_prompt": "The video shows a man with a slight smile, then the j432mpscare jumpscare occurs, revealing a distorted and monstrous face with glowing red eyes, filling the frame and accompanied by a loud scream."
|
78 |
},
|
79 |
+
{
|
80 |
+
"image": "https://huggingface.co/Remade-AI/Arc_shot/resolve/main/example_videos/1.gif",
|
81 |
+
"id": "a5949ee3-61ea-4a18-bd4d-54c855f5401c",
|
82 |
+
"title": "Arc Shot ",
|
83 |
+
"example_prompt": "The video shows a man with a slight smile, then the j432mpscare jumpscare occurs, revealing a distorted and monstrous face with glowing red eyes, filling the frame and accompanied by a loud scream."
|
84 |
+
},
|
85 |
+
{
|
86 |
+
"image": "https://huggingface.co/Remade-AI/Hero-run/resolve/main/example_videos/1.gif",
|
87 |
+
"id": "36b9edf7-31d7-47d3-ad3b-e166fb3a9842",
|
88 |
+
"title": "Hero Run ",
|
89 |
+
"example_prompt": "The video shows a man with a slight smile, then the j432mpscare jumpscare occurs, revealing a distorted and monstrous face with glowing red eyes, filling the frame and accompanied by a loud scream."
|
90 |
},
|
91 |
+
|
92 |
+
|
|
|
|
|
|
|
|
|
93 |
]
|
94 |
|
95 |
# Initialize Supabase client with async support
|
|
|
245 |
raise ValueError(f"Unknown LoRA ID: {lora_id} and no example prompt available")
|
246 |
|
247 |
def poll_generation_status(generation_id):
|
248 |
+
"""Poll generation status from Modal backend or database"""
|
249 |
try:
|
250 |
+
# First try to get status from Modal backend if available
|
251 |
+
if MODAL_ENDPOINT:
|
252 |
+
try:
|
253 |
+
response = requests.get(
|
254 |
+
f"{MODAL_ENDPOINT}/fal-effects/status?generation_id={generation_id}",
|
255 |
+
headers=get_modal_auth_headers()
|
256 |
+
)
|
257 |
+
|
258 |
+
except Exception as e:
|
259 |
+
print(f"Error polling Modal backend: {e}")
|
260 |
+
|
261 |
+
|
262 |
+
|
263 |
response = supabase.table('generations') \
|
264 |
.select('*') \
|
265 |
.eq('generation_id', generation_id) \
|
|
|
296 |
|
297 |
# Additional checks for keywords related to minors or inappropriate content
|
298 |
keywords = [
|
299 |
+
"child", "kid", "minor", "teen", "baby", "infant", "underage",
|
300 |
"naked", "nude", "nsfw", "porn", "xxx", "sex", "explicit",
|
301 |
"inappropriate", "adult content"
|
302 |
]
|
|
|
361 |
|
362 |
# Get likelihood scores
|
363 |
adult_score = likelihood_values[safe_search.adult]
|
364 |
+
|
|
|
|
|
365 |
|
366 |
# Determine if content is NSFW according to Vision API
|
367 |
vision_reasons = []
|
368 |
if adult_score >= 3: # LIKELY or VERY_LIKELY
|
369 |
vision_reasons.append("adult content")
|
370 |
+
|
|
|
|
|
|
|
|
|
371 |
|
372 |
# Print Vision API results
|
373 |
print("Google Cloud Vision API Results:")
|
374 |
print(f"Adult: {vision.Likelihood(safe_search.adult).name}")
|
375 |
+
|
|
|
|
|
|
|
376 |
except Exception as vision_error:
|
377 |
print(f"Error with Vision API: {vision_error}")
|
378 |
vision_reasons = [] # Continue with OpenAI check if Vision API fails
|
|
|
492 |
"reason": "Failed to verify content safety - please try again"
|
493 |
}
|
494 |
|
495 |
+
async def generate_video(input_image, subject, selected_index, progress=gr.Progress()):
|
496 |
try:
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
497 |
# Check if the input is a URL (example image) or a file path (user upload)
|
498 |
if input_image.startswith('http'):
|
499 |
# It's already a URL, use it directly
|
|
|
502 |
# It's a file path, upload to GCS
|
503 |
image_url = upload_to_gcs(input_image)
|
504 |
|
505 |
+
# Hardcode duration to 3 seconds
|
506 |
+
video_duration = 5
|
|
|
|
|
|
|
|
|
507 |
|
508 |
# Get LoRA config
|
509 |
lora_config = next((lora for lora in loras if lora["id"] == selected_index), None)
|
|
|
513 |
# Generate unique ID
|
514 |
generation_id = str(uuid.uuid4())
|
515 |
|
516 |
+
# Build prompt for the LoRA
|
517 |
+
prompt = subject
|
518 |
+
|
519 |
+
# Check if Modal endpoint is configured
|
520 |
+
if not MODAL_ENDPOINT:
|
521 |
+
raise ValueError("Modal endpoint not configured - FAL_MODAL_ENDPOINT environment variable not found")
|
522 |
+
|
523 |
+
# Calculate frames based on duration and frame rate
|
524 |
+
frame_rate = 16 # WanVideo frame rate
|
525 |
+
num_frames = calculate_frames(video_duration, frame_rate)
|
526 |
+
|
527 |
+
print(f"Sending request to Modal backend: {MODAL_ENDPOINT}/fal-effects")
|
528 |
+
|
529 |
+
# Make POST request to the modal backend
|
530 |
+
response = requests.post(f"{MODAL_ENDPOINT}/fal-effects",
|
531 |
+
headers=get_modal_auth_headers(),
|
532 |
+
json={
|
533 |
+
"user_id": "anonymous", # Since we don't have user auth in this app
|
534 |
+
"image_url": image_url,
|
535 |
+
"subject": prompt, # Use the built prompt as subject
|
536 |
+
"aspect_ratio": "16:9", # Default aspect ratio for effects
|
537 |
+
"num_frames": 81,
|
538 |
+
"frames_per_second": frame_rate,
|
539 |
+
"length": str(5),
|
540 |
+
"enhance_prompt": False,
|
541 |
+
"lora_scale": 1.0,
|
542 |
+
"turbo_mode": False,
|
543 |
+
"lora_id": selected_index,
|
544 |
+
"lora_strength": 1.0,
|
545 |
+
"generation_ids": [generation_id]
|
546 |
+
}
|
547 |
+
)
|
548 |
+
|
549 |
+
if not response.ok:
|
550 |
+
error_text = response.text
|
551 |
+
try:
|
552 |
+
error_json = response.json()
|
553 |
+
error_message = error_json.get('detail') or error_json.get('error') or 'Failed to create generation'
|
554 |
+
except:
|
555 |
+
error_message = f'Failed to create generation: {error_text}'
|
556 |
+
raise ValueError(error_message)
|
557 |
+
|
558 |
+
result = response.json()
|
559 |
+
print(f"Modal backend response: {result}")
|
560 |
+
|
561 |
+
# Extract generation ID from response
|
562 |
+
if 'generation_id' in result:
|
563 |
+
return result['generation_id']
|
564 |
+
elif 'id' in result:
|
565 |
+
return result['id']
|
566 |
+
else:
|
567 |
+
# Fallback to our generated ID if the response doesn't contain one
|
568 |
+
return generation_id
|
569 |
|
570 |
except Exception as e:
|
571 |
print(f"Error in generate_video: {e}")
|
|
|
576 |
sentence = f"Selected LoRA: {selected_lora['title']}"
|
577 |
return selected_lora['id'], sentence
|
578 |
|
579 |
+
async def handle_generation(image_input, subject, selected_index, progress=gr.Progress(track_tqdm=True)):
|
580 |
try:
|
581 |
if selected_index is None:
|
582 |
raise gr.Error("You must select a LoRA before proceeding.")
|
|
|
585 |
prompt_moderation = await moderate_prompt(subject)
|
586 |
print(f"Prompt moderation result: {prompt_moderation}")
|
587 |
if prompt_moderation["isNSFW"]:
|
588 |
+
# Re-enable button on error
|
589 |
+
yield None, None, gr.update(visible=False), gr.update(value="Generate", interactive=True)
|
590 |
raise gr.Error(f"Content moderation failed: {prompt_moderation['reason']}")
|
591 |
|
592 |
# Then, moderate the image
|
593 |
image_moderation = await moderate_image(image_input)
|
594 |
print(f"Image moderation result: {image_moderation}")
|
595 |
if image_moderation["isNSFW"]:
|
596 |
+
# Re-enable button on error
|
597 |
+
yield None, None, gr.update(visible=False), gr.update(value="Generate", interactive=True)
|
598 |
raise gr.Error(f"Content moderation failed: {image_moderation['reason']}")
|
599 |
|
600 |
# Finally, check the combination
|
601 |
combined_moderation = await moderate_combined(subject, image_input)
|
602 |
print(f"Combined moderation result: {combined_moderation}")
|
603 |
if combined_moderation["isNSFW"]:
|
604 |
+
# Re-enable button on error
|
605 |
+
yield None, None, gr.update(visible=False), gr.update(value="Generate", interactive=True)
|
606 |
raise gr.Error(f"Content moderation failed: {combined_moderation['reason']}")
|
607 |
|
608 |
# Generate the video and get generation ID
|
609 |
+
generation_id = await generate_video(image_input, subject, selected_index)
|
610 |
|
611 |
# Poll for status updates
|
612 |
while True:
|
613 |
generation = poll_generation_status(generation_id)
|
614 |
|
615 |
if not generation:
|
616 |
+
# Re-enable button on error
|
617 |
+
yield None, None, gr.update(visible=False), gr.update(value="Generate", interactive=True)
|
618 |
raise ValueError(f"Generation {generation_id} not found")
|
619 |
|
620 |
# Update progress
|
|
|
624 |
|
625 |
# Check status
|
626 |
if generation['status'] == 'completed':
|
627 |
+
# Final yield with completed video and re-enabled button
|
628 |
+
yield generation['output_url'], generation_id, gr.update(visible=False), gr.update(value="Generate", interactive=True)
|
629 |
break # Exit the loop
|
630 |
elif generation['status'] == 'error':
|
631 |
+
# Re-enable button on error
|
632 |
+
yield None, None, gr.update(visible=False), gr.update(value="Generate", interactive=True)
|
633 |
raise ValueError(f"Generation failed: {generation.get('error')}")
|
634 |
else:
|
635 |
+
# Yield progress update with button still disabled
|
636 |
+
yield None, generation_id, gr.update(value=progress_bar, visible=True), gr.update(value="Generating...", interactive=False)
|
637 |
|
638 |
# Wait before next poll
|
639 |
await asyncio.sleep(2)
|
640 |
|
641 |
except Exception as e:
|
642 |
print(f"Error in handle_generation: {e}")
|
643 |
+
# Re-enable button on any error
|
644 |
+
yield None, None, gr.update(visible=False), gr.update(value="Generate", interactive=True)
|
645 |
raise e
|
646 |
|
647 |
css = '''
|
|
|
720 |
pointer-events: none;
|
721 |
}
|
722 |
.discord-locked::after {
|
723 |
+
content: "🔒 Remade Canvas exclusive";
|
724 |
position: absolute;
|
725 |
top: 50%;
|
726 |
left: 50%;
|
|
|
827 |
selected_index = gr.State(None)
|
828 |
current_generation_id = gr.State(None)
|
829 |
|
830 |
+
# Updated title with Remade Canvas theme
|
831 |
+
gr.Markdown("# Remade AI - Open Source Camera Controls")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
832 |
|
833 |
+
# Updated Remade Canvas callout
|
834 |
+
gr.HTML(
|
835 |
"""
|
836 |
<div class="discord-banner">
|
837 |
+
<h3>🚀 Unlock 100s of AI Video Effects! 🎬</h3>
|
838 |
+
<p>Access Remade Canvas with Veo, Kling, and hundreds of professional video effects. Create cinematic content with the most advanced AI video models!</p>
|
839 |
+
<a href="https://app.remade.ai?utm_source=Huggingface&utm_medium=Social&utm_campaign=hugginface_space&utm_content=canvas_effects" target="_blank">Try Remade Canvas</a>
|
840 |
+
<div style="margin-top: 15px; padding-top: 15px; border-top: 1px solid rgba(255,255,255,0.7);">
|
841 |
+
<p style="font-size: 0.9em; margin-bottom: 10px;">Join our community for updates and tips:</p>
|
842 |
+
<a href="https://remade.ai/join-discord?utm_source=Huggingface&utm_medium=Social&utm_campaign=hugginface_space&utm_content=canvas_effects" target="_blank" style="background-color: rgba(255,255,255,0.2); font-size: 0.9em; padding: 8px 16px;">Discord Community</a>
|
843 |
+
</div>
|
844 |
</div>
|
845 |
"""
|
846 |
)
|
|
|
860 |
object_fit="contain"
|
861 |
)
|
862 |
|
863 |
+
# Updated Discord/camera controls callout
|
864 |
gr.HTML(
|
865 |
"""
|
866 |
<div class="discord-feature">
|
867 |
+
<span class="discord-feature-title">🎬 Remade Canvas:</span> Access 100s of effects including Veo, Kling, and advanced camera controls beyond these samples!
|
868 |
</div>
|
869 |
"""
|
870 |
)
|
|
|
892 |
|
893 |
subject = gr.Textbox(label="Describe your subject", placeholder="Cat toy")
|
894 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
895 |
with gr.Row():
|
896 |
button = gr.Button("Generate", variant="primary", elem_id="gen_btn")
|
897 |
audio_button = gr.Button("Add Audio 🔒", interactive=False)
|
|
|
900 |
warning_message = gr.HTML(
|
901 |
"""
|
902 |
<div class="warning-message">
|
903 |
+
⚠️ Please DO NOT refresh the page during generation. Processing camera controls takes time for best quality!
|
904 |
</div>
|
905 |
""",
|
906 |
visible=True
|
|
|
909 |
gr.HTML(
|
910 |
"""
|
911 |
<div class="discord-feature">
|
912 |
+
<span class="discord-feature-title">⚡ Remade Canvas:</span> Get faster generation speeds and access to Veo, Kling, and 100s of premium effects!
|
913 |
</div>
|
914 |
"""
|
915 |
)
|
|
|
972 |
if image_input is None:
|
973 |
raise gr.Error("Please upload an image or select an example image.")
|
974 |
|
975 |
+
# Function to immediately disable button
|
976 |
+
def start_generation():
|
977 |
+
return gr.update(value="Generating...", interactive=False)
|
978 |
+
|
979 |
# Use gr.on for the button click with validation
|
980 |
button.click(
|
981 |
fn=check_inputs,
|
982 |
inputs=[subject, image_input, selected_index],
|
983 |
outputs=None,
|
984 |
+
).success(
|
985 |
+
fn=start_generation,
|
986 |
+
inputs=None,
|
987 |
+
outputs=[button]
|
988 |
).success(
|
989 |
fn=handle_generation,
|
990 |
+
inputs=[image_input, subject, selected_index],
|
991 |
+
outputs=[output, current_generation_id, progress_bar, button]
|
992 |
)
|
993 |
|
994 |
# Add a click handler for the disabled audio button
|
995 |
audio_button.click(
|
996 |
+
fn=lambda: gr.Info("Try Remade Canvas to unlock audio generation and 100s of other effects!"),
|
997 |
inputs=None,
|
998 |
outputs=None
|
999 |
)
|
1000 |
|
1001 |
+
def get_modal_auth_headers():
|
1002 |
+
"""Get authentication headers for Modal API requests"""
|
1003 |
+
if not MODAL_AUTH_TOKEN:
|
1004 |
+
raise ValueError("MODAL_AUTH_TOKEN environment variable not found")
|
1005 |
+
|
1006 |
+
return {
|
1007 |
+
'Authorization': f'Bearer {MODAL_AUTH_TOKEN}',
|
1008 |
+
'Content-Type': 'application/json'
|
1009 |
+
}
|
1010 |
+
|
1011 |
if __name__ == "__main__":
|
1012 |
demo.queue(default_concurrency_limit=20)
|
1013 |
demo.launch(ssr_mode=False, share=True)
|