alex-remade commited on
Commit
0f05fa7
·
1 Parent(s): 89ea8e4

refactor: integrate Modal API for video generation and update LoRA configurations

Browse files
Files changed (1) hide show
  1. app.py +188 -181
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
- CONFIG_PATH = SCRIPT_DIR / "config.json"
25
- WORKFLOW_PATH = SCRIPT_DIR / "wani2v.json"
 
 
26
 
27
  loras = [
28
- {
29
- "image": "https://storage.googleapis.com/remade-v2/huggingface_assets/69e1adfc-b99a-4559-b745-f193a1bca0e2.gif",
30
- "id": "c8972c6d-ab8a-4988-9a9d-38082264ef22",
31
- "title": "Jumpscare",
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://storage.googleapis.com/remade-v2/huggingface_assets/2dede2c5-38e0-4acb-9e99-027e804a0455.gif",
36
- "id": "d7cbf9b4-82cd-4a94-ba2f-040e809635fa",
37
- "title": "Angry",
38
- "example_prompt": "The video starts with a man looking at the camera with a neutral face. Then his facial expression changes to 4ngr23 angry face, and he begins to yell with clenched fists. "
39
  },
40
-
41
- {
42
- "image": "https://storage.googleapis.com/remade-v2/huggingface_assets/7ba11998-2421-4ae1-8cf8-47b076387c2e.gif",
43
- "id": "e17959c4-9fa5-4e5b-8f69-d1fb01bbe4fa",
44
- "title": "Cartoon Jaw Drop",
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://storage.googleapis.com/remade-v2/huggingface_assets/46b5c7a3-60d5-469e-9454-9d16bf20afe4.gif",
49
- "id": "687255bb-959e-4422-bdbb-5aba93c7c180",
50
- "title": "Kissing",
51
- "example_prompt": "A man with a beard is shown smiling. A woman comes into the scene and starts passionately k144ing kissing the man."
52
  },
53
- {
54
- "image": "https://storage.googleapis.com/remade-v2/huggingface_assets/9f11b800-25c1-42f9-a687-97d706fef06d.gif",
55
- "id": "4ac2fb4e-5ca2-4338-a59c-549167f5b6d0",
56
- "title": "Laughing",
57
- "example_prompt": "A [Subject] is smiling at the camera. He/she then begins l4a6ing laughing."
58
  },
59
- {
60
- "image": "https://storage.googleapis.com/remade-v2/huggingface_assets/7cc38d79-f09e-4a5d-9e17-65867f8fd3a7.gif",
61
- "id": "bcc4163d-ebda-4cdc-b153-7136cdbf563a",
62
- "title": "Crying",
63
- "example_prompt": "The video starts with a [Subject] with a solemn expression. Then a tear rolls down his/her cheek, as he/she is cr471ng crying."
 
64
  },
65
- {
66
- "image": "https://storage.googleapis.com/remade-v2/huggingface_assets/62256895-10b2-4bd1-8ffe-4ecec530c4ec.gif",
67
- "id": "13093298-652c-4df8-ba28-62d9d5924754",
68
- "title": "Take a selfie with your younger self",
69
- "example_prompt": "The video starts with the a man with a beard smiling at the camera, then s31lf13 taking a selfie with their younger self, and the younger self appears next to him with similar facial features and eye color. The younger self wears a white t-shirt and has a cream white jacket. The younger self is smiling slightly."
 
 
 
 
 
 
 
70
  },
71
- {
72
- "image": "https://huggingface.co/Remade-AI/Squish/resolve/main/example_gifs/person_squish.gif",
73
- "id": "06ce6840-f976-4963-9644-b6cf7f323f90",
74
- "title": "Squish",
75
- "example_prompt": "In the video, a miniature rodent is presented. The rodent is held in a person's hands. The person then presses on the rodent, causing a sq41sh squish effect. The person keeps pressing down on the rodent, further showing the sq41sh squish effect.",
76
- },
77
- {
78
- "image": "https://huggingface.co/Remade-AI/Rotate/resolve/main/example_videos/chair-rotate.gif",
79
- "id": "4ac08cfa-841e-4aa9-9022-c3fc80fb6ef4",
80
- "title": "Rotate",
81
- "example_prompt": "The video shows an elderly Asian man's head and shoulders with blurred background, performing a r0t4tion 360 degrees rotation.",
82
  },
83
- {
84
- "image": "https://huggingface.co/Remade-AI/Cakeify/resolve/main/example_gifs/timberland_cakeify.gif",
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
- # Query the database for the current status
 
 
 
 
 
 
 
 
 
 
 
 
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", "young", "baby", "infant", "underage",
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
- racy_score = likelihood_values[safe_search.racy]
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
- if racy_score >= 3: # LIKELY or VERY_LIKELY
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
- print(f"Racy: {vision.Likelihood(safe_search.racy).name}")
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, duration, selected_index, progress=gr.Progress()):
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
- # Map duration selection to actual seconds
506
- duration_mapping = {
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
- # Update workflow
521
- prompt = build_lora_prompt(subject, selected_index)
522
- workflow_handler.update_prompt(prompt)
523
- workflow_handler.update_input_image(image_url)
524
- await workflow_handler.update_lora(lora_config)
525
- workflow_handler.update_length(video_duration)
526
- workflow_handler.update_output_name(generation_id)
527
-
528
- # Get final workflow
529
- workflow = workflow_handler.get_workflow()
530
-
531
- # Store generation data in Supabase
532
- generation_data = {
533
- "generation_id": generation_id,
534
- "user_id": "anonymous",
535
- "status": "queued",
536
- "progress": 0,
537
- "worker_id": None,
538
- "created_at": datetime.datetime.utcnow().isoformat(),
539
- "message": {
540
- "generationId": generation_id,
541
- "workflow": {
542
- "prompt": workflow
543
- }
544
- },
545
- "metadata": {
546
- "prompt": {
547
- "original": subject,
548
- "enhanced": subject
549
- },
550
- "lora": {
551
- "id": selected_index,
552
- "strength": 1.0,
553
- "name": lora_config["title"]
554
- },
555
- "workflow": "img2vid",
556
- "dimensions": None,
557
- "input_image_url": image_url,
558
- "video_length": {"duration": video_duration},
559
- },
560
- "error": None,
561
- "output_url": None,
562
- "batch_id": None,
563
- "platform": "huggingface"
564
- }
565
-
566
- # Remove await - the execute() method returns the response directly
567
- response = supabase.table('generations').insert(generation_data).execute()
568
- print(f"Stored generation data with ID: {generation_id}")
569
-
570
- # Return generation ID for tracking
571
- return generation_id
 
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, duration, selected_index, progress=gr.Progress(track_tqdm=True)):
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, duration, selected_index)
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: "🔒 Discord members only";
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 April Fool's theme
822
- gr.Markdown("# Remade AI - April Fool's Edition: Wan 2.1 I2V Effects")
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
- # Optionally, update the Discord banner for an April Fool's twist
834
- discord_banner = gr.HTML(
835
  """
836
  <div class="discord-banner">
837
- <h3>✨ Unlock Premium April Fool's Pranks! ✨</h3>
838
- <p>Join our Discord community for exclusive prank effects, surprise features, and more playful fun!</p>
839
- <a href="https://remade.ai/join-discord?utm_source=Huggingface&utm_medium=Social&utm_campaign=hugginface_space&utm_content=april_fools" target="_blank">Join Discord Now</a>
 
 
 
 
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/prank callout
860
  gr.HTML(
861
  """
862
  <div class="discord-feature">
863
- <span class="discord-feature-title">✨ Discord Members:</span> Get access to even more mischievous effects beyond these samples!
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. Our pranksters are hard at work!
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">⚡ Discord Members:</span> Get faster (and prankier) generation speeds!
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, duration, selected_index],
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("Join our Discord to unlock audio generation features!"),
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)