Munaf1987 commited on
Commit
81cccef
·
verified ·
1 Parent(s): b2bf6ea

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +542 -284
app.py CHANGED
@@ -65,132 +65,99 @@ class ProfessionalCartoonFilmGenerator:
65
 
66
  @spaces.GPU
67
  def load_models(self):
68
- """Load state-of-the-art models for professional quality"""
69
- if self.models_loaded:
70
- return
71
-
72
- print("🚀 Loading professional-grade models...")
73
-
74
  try:
75
- # 1. Try FLUX pipeline first (if user has authentication)
76
- print("🎨 Loading FLUX pipeline...")
 
 
 
 
 
 
 
 
 
 
77
  try:
 
 
 
78
  self.flux_pipe = FluxPipeline.from_pretrained(
79
  "black-forest-labs/FLUX.1-dev",
80
- torch_dtype=torch.bfloat16,
81
- variant="fp16",
82
- use_safetensors=True
83
- ).to(self.device)
84
- print("✅ FLUX pipeline loaded successfully!")
85
- self.using_flux = True
86
- except Exception as flux_error:
87
- if "401" in str(flux_error) or "authentication" in str(flux_error).lower():
88
- print("🔐 FLUX authentication failed - model requires Hugging Face token")
89
- print("💡 To use FLUX, you need to:")
90
- print(" 1. Get a Hugging Face token from https://huggingface.co/settings/tokens")
91
- print(" 2. Accept the FLUX model license at https://huggingface.co/black-forest-labs/FLUX.1-dev")
92
- print(" 3. Set your token: huggingface-cli login")
93
- print("🔄 Falling back to Stable Diffusion...")
94
- self.using_flux = False
95
- else:
96
- print(f"❌ FLUX loading failed: {flux_error}")
97
- self.using_flux = False
98
- except Exception as e:
99
- print(f"❌ FLUX pipeline failed: {e}")
100
- self.using_flux = False
101
-
102
- # Load cartoon/anime LoRA for character generation (only if FLUX is available)
103
- if self.using_flux:
104
- print("🎭 Loading cartoon LoRA models...")
105
- try:
106
- # Load multiple LoRA models for different purposes
107
- self.cartoon_lora = hf_hub_download(
108
- "prithivMLmods/Canopus-LoRA-Flux-Anime",
109
- "Canopus-LoRA-Flux-Anime.safetensors"
110
- )
111
- self.character_lora = hf_hub_download(
112
- "enhanceaiteam/Anime-Flux",
113
- "anime-flux.safetensors"
114
- )
115
- self.sketch_lora = hf_hub_download(
116
- "Shakker-Labs/FLUX.1-dev-LoRA-Children-Simple-Sketch",
117
- "FLUX-dev-lora-children-simple-sketch.safetensors"
118
  )
119
- print("✅ LoRA models loaded successfully")
120
- except Exception as e:
121
- print(f"⚠️ Some LoRA models failed to load: {e}")
122
-
123
- # Enable memory optimizations for FLUX
124
- if self.flux_pipe:
125
- self.flux_pipe.enable_vae_slicing()
126
- self.flux_pipe.enable_vae_tiling()
127
 
128
- # Enable flash attention if available
129
- if FLASH_ATTN_AVAILABLE:
130
- try:
131
- self.flux_pipe.enable_xformers_memory_efficient_attention()
132
- print("✅ Flash attention enabled for better performance")
133
- except Exception as e:
134
- print(f"⚠️ Flash attention failed: {e}")
135
- else:
136
- print("ℹ️ Using standard attention (flash attention not available)")
137
-
138
- # Load Stable Diffusion fallback if FLUX is not available
139
- if not self.using_flux:
140
- try:
141
- from diffusers import StableDiffusionPipeline
 
 
 
142
  print("🔄 Loading Stable Diffusion fallback model...")
 
143
 
144
- # Try a more accessible model first
145
- try:
146
- self.flux_pipe = StableDiffusionPipeline.from_pretrained(
147
- "CompVis/stable-diffusion-v1-4",
148
- torch_dtype=torch.float16,
149
- use_safetensors=True,
150
- safety_checker=None,
151
- requires_safety_checker=False
152
- ).to(self.device)
153
- print("✅ Loaded Stable Diffusion v1.4")
154
- except Exception as sd_error:
155
- print(f"⚠️ SD v1.4 failed: {sd_error}")
156
- # Try the original model
157
- self.flux_pipe = StableDiffusionPipeline.from_pretrained(
158
- "runwayml/stable-diffusion-v1-5",
159
- torch_dtype=torch.float16,
160
- use_safetensors=True,
161
- safety_checker=None,
162
- requires_safety_checker=False
163
- ).to(self.device)
164
- print("✅ Loaded Stable Diffusion v1.5")
165
 
166
- # Enable memory optimizations
167
- self.flux_pipe.enable_vae_slicing()
168
- if hasattr(self.flux_pipe, 'enable_vae_tiling'):
169
- self.flux_pipe.enable_vae_tiling()
170
 
 
 
 
 
171
  print("✅ Stable Diffusion fallback loaded successfully")
172
-
173
- except Exception as e2:
174
- print(f"❌ Stable Diffusion fallback also failed: {e2}")
175
- self.flux_pipe = None
176
-
177
- try:
178
- # 2. Advanced script generation model
179
  print("📝 Loading script enhancement model...")
180
- self.script_enhancer = pipeline(
181
- "text-generation",
182
- model="microsoft/DialoGPT-large",
183
- torch_dtype=torch.float16 if self.device == "cuda" else torch.float32,
184
- device=0 if self.device == "cuda" else -1
185
  )
 
 
 
 
 
 
 
 
 
186
  print("✅ Script enhancer loaded")
187
 
 
 
 
 
 
 
 
 
 
 
188
  except Exception as e:
189
- print(f"❌ Script enhancer failed: {e}")
190
- self.script_enhancer = None
191
-
192
- self.models_loaded = True
193
- print("🎬 All professional models loaded!")
194
 
195
  def clear_gpu_memory(self):
196
  """Clear GPU memory between operations"""
@@ -482,203 +449,216 @@ class ProfessionalCartoonFilmGenerator:
482
 
483
  @spaces.GPU
484
  def generate_professional_character_images(self, characters: List[Dict]) -> Dict[str, str]:
485
- """Generate high-quality character images using FLUX + LoRA"""
486
- self.load_models()
487
  character_images = {}
488
 
489
- if not self.flux_pipe:
 
 
 
 
 
 
 
 
 
 
 
 
 
 
490
  print("❌ No image generation pipeline available")
491
  return character_images
 
 
492
 
493
  for character in characters:
 
 
 
494
  try:
495
- print(f"🎭 Generating professional character: {character['name']}")
496
-
497
- # Load appropriate LoRA based on character type (only for FLUX)
498
- if hasattr(self.flux_pipe, 'load_lora_weights') and "anime" in character.get("animation_style", "").lower():
499
- if hasattr(self, 'cartoon_lora'):
500
- try:
501
- self.flux_pipe.load_lora_weights(self.cartoon_lora)
502
- except Exception as e:
503
- print(f"⚠️ LoRA loading failed: {e}")
504
 
505
- # Professional character prompt (optimized for CLIP token limit)
506
- character_desc = character['description'][:100] # Limit description length
507
- animation_style = character.get('animation_style', 'high-quality character design')[:50]
508
-
509
- prompt = f"anime style, professional cartoon character, {character_desc}, character sheet, clean background, 2D animation, Disney quality, detailed, {animation_style}"
510
-
511
- # Use the optimization function to ensure CLIP compatibility
512
- prompt = self.optimize_prompt_for_clip(prompt)
513
 
514
- negative_prompt = """
515
- realistic, 3D render, dark, scary, inappropriate, low quality, blurry,
516
- inconsistent, amateur, simple, crude, manga, sketch
517
- """
518
 
519
- # Handle different pipeline types with CLIP token error handling
520
- try:
521
- if hasattr(self.flux_pipe, 'max_sequence_length'):
522
- # FLUX pipeline
523
- image = self.flux_pipe(
524
- prompt=prompt,
525
- negative_prompt=negative_prompt,
526
- num_inference_steps=25, # High quality steps
527
- guidance_scale=3.5,
528
- height=1024, # High resolution
529
- width=1024,
530
- max_sequence_length=256
531
- ).images[0]
532
- else:
533
- # Stable Diffusion pipeline
534
- image = self.flux_pipe(
535
- prompt=prompt,
536
- negative_prompt=negative_prompt,
537
- num_inference_steps=25, # High quality steps
538
- guidance_scale=7.5,
539
- height=1024, # High resolution
540
- width=1024
541
- ).images[0]
542
- except Exception as e:
543
- if "CLIP" in str(e) and "token" in str(e).lower():
544
- print(f"⚠️ CLIP token error detected, using simplified prompt...")
545
- # Fallback to very simple prompt
546
- simple_prompt = f"anime character, {character['name']}, clean background"
547
- simple_prompt = self.optimize_prompt_for_clip(simple_prompt, max_tokens=30)
548
-
549
- if hasattr(self.flux_pipe, 'max_sequence_length'):
550
- image = self.flux_pipe(
551
- prompt=simple_prompt,
552
- negative_prompt="low quality, blurry",
553
- num_inference_steps=20,
554
- guidance_scale=3.0,
555
- height=1024,
556
- width=1024,
557
- max_sequence_length=128
558
- ).images[0]
559
- else:
560
- image = self.flux_pipe(
561
- prompt=simple_prompt,
562
- negative_prompt="low quality, blurry",
563
- num_inference_steps=20,
564
- guidance_scale=7.0,
565
- height=1024,
566
- width=1024
567
- ).images[0]
568
- else:
569
- raise e
570
 
 
571
  char_path = f"{self.output_dir}/char_{character['name'].replace(' ', '_')}.png"
572
  image.save(char_path)
573
- character_images[character['name']] = char_path
574
-
575
- # Create download URL for character
576
- download_info = self.create_download_url(char_path, f"character_{character['name']}")
577
- print(f"✅ Generated high-quality character: {character['name']}")
578
- print(download_info)
579
 
580
- self.clear_gpu_memory()
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
581
 
582
  except Exception as e:
583
- print(f"❌ Error generating character {character['name']}: {e}")
 
 
 
 
 
 
 
 
 
584
 
585
  return character_images
586
 
587
  @spaces.GPU
588
  def generate_cinematic_backgrounds(self, scenes: List[Dict], color_palette: str) -> Dict[int, str]:
589
- """Generate cinematic background images for each scene"""
590
- self.load_models()
591
  background_images = {}
592
 
593
- if not self.flux_pipe:
 
 
 
 
 
 
 
 
 
 
 
 
 
 
594
  print("❌ No image generation pipeline available")
595
  return background_images
 
 
596
 
597
  for scene in scenes:
 
 
 
598
  try:
599
- print(f"🏞️ Creating cinematic background for scene {scene['scene_number']}")
 
 
 
 
600
 
601
- # Professional background prompt (optimized for CLIP token limit)
602
- background_desc = scene['background'][:80] # Limit background description
603
- mood = scene['mood'][:30]
604
- shot_type = scene.get('shot_type', 'medium shot')[:20]
605
- animation_notes = scene.get('animation_notes', 'professional background art')[:40]
606
 
607
- prompt = f"Professional cartoon background, {background_desc}, {mood} atmosphere, {color_palette} colors, {shot_type}, no characters, detailed environment, Disney quality, {animation_notes}"
608
-
609
- # Use the optimization function to ensure CLIP compatibility
610
- prompt = self.optimize_prompt_for_clip(prompt)
 
611
 
612
- negative_prompt = """
613
- characters, people, animals, realistic, dark, scary, low quality,
614
- blurry, simple, amateur, 3D render
615
- """
616
 
617
- # Handle different pipeline types for backgrounds with CLIP token error handling
618
- try:
619
- if hasattr(self.flux_pipe, 'max_sequence_length'):
620
- # FLUX pipeline
621
- image = self.flux_pipe(
622
- prompt=prompt,
623
- negative_prompt=negative_prompt,
624
- num_inference_steps=20,
625
- guidance_scale=3.0,
626
- height=768, # 4:3 aspect ratio for traditional animation
627
- width=1024,
628
- max_sequence_length=256
629
- ).images[0]
630
- else:
631
- # Stable Diffusion pipeline
632
- image = self.flux_pipe(
633
- prompt=prompt,
634
- negative_prompt=negative_prompt,
635
- num_inference_steps=20,
636
- guidance_scale=7.0,
637
- height=768, # 4:3 aspect ratio for traditional animation
638
- width=1024
639
- ).images[0]
640
- except Exception as e:
641
- if "CLIP" in str(e) and "token" in str(e).lower():
642
- print(f"⚠️ CLIP token error detected for background, using simplified prompt...")
643
- # Fallback to very simple prompt
644
- simple_prompt = f"cartoon background, {scene['background'][:40]}, clean"
645
- simple_prompt = self.optimize_prompt_for_clip(simple_prompt, max_tokens=25)
646
-
647
- if hasattr(self.flux_pipe, 'max_sequence_length'):
648
- image = self.flux_pipe(
649
- prompt=simple_prompt,
650
- negative_prompt="characters, low quality",
651
- num_inference_steps=15,
652
- guidance_scale=3.0,
653
- height=768,
654
- width=1024,
655
- max_sequence_length=128
656
- ).images[0]
657
- else:
658
- image = self.flux_pipe(
659
- prompt=simple_prompt,
660
- negative_prompt="characters, low quality",
661
- num_inference_steps=15,
662
- guidance_scale=7.0,
663
- height=768,
664
- width=1024
665
- ).images[0]
666
- else:
667
- raise e
668
 
669
- bg_path = f"{self.output_dir}/bg_scene_{scene['scene_number']}.png"
 
670
  image.save(bg_path)
671
- background_images[scene['scene_number']] = bg_path
672
-
673
- # Create download URL for background
674
- download_info = self.create_download_url(bg_path, f"background_scene_{scene['scene_number']}")
675
- print(f"✅ Created cinematic background for scene {scene['scene_number']}")
676
- print(download_info)
677
 
678
- self.clear_gpu_memory()
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
679
 
680
  except Exception as e:
681
  print(f"❌ Error generating background for scene {scene['scene_number']}: {e}")
 
 
 
 
 
 
 
 
 
682
 
683
  return background_images
684
 
@@ -687,6 +667,13 @@ class ProfessionalCartoonFilmGenerator:
687
  try:
688
  print("🎬 Setting up Open-Sora 2.0 for video generation...")
689
 
 
 
 
 
 
 
 
690
  # Check if we're already in the right directory
691
  current_dir = os.getcwd()
692
  opensora_dir = os.path.join(current_dir, "Open-Sora")
@@ -694,33 +681,97 @@ class ProfessionalCartoonFilmGenerator:
694
  # Clone Open-Sora repository if it doesn't exist
695
  if not os.path.exists(opensora_dir):
696
  print("📥 Cloning Open-Sora repository...")
697
- subprocess.run([
698
- "git", "clone", "https://github.com/hpcaitech/Open-Sora.git"
699
- ], check=True, capture_output=True)
 
 
 
 
 
 
 
 
700
 
701
  # Check if the repository was cloned successfully
702
  if not os.path.exists(opensora_dir):
703
  print("❌ Failed to clone Open-Sora repository")
704
  return False
705
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
706
  # Check if model weights exist
707
  ckpts_dir = os.path.join(opensora_dir, "ckpts")
708
  if not os.path.exists(ckpts_dir):
709
  print("📥 Downloading Open-Sora 2.0 model...")
710
  try:
711
- subprocess.run([
 
712
  "huggingface-cli", "download", "hpcai-tech/Open-Sora-v2",
713
  "--local-dir", ckpts_dir
714
- ], check=True, capture_output=True)
715
- except Exception as e:
716
- print(f"❌ Model download failed: {e}")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
717
  return False
 
 
 
718
 
719
  print("✅ Open-Sora setup completed")
720
  return True
721
 
722
  except Exception as e:
723
  print(f"❌ Open-Sora setup failed: {e}")
 
 
724
  return False
725
 
726
  @spaces.GPU
@@ -746,17 +797,23 @@ class ProfessionalCartoonFilmGenerator:
746
  if video_path:
747
  print(f"✅ Open-Sora video generated for scene {scene_num}")
748
  else:
749
- print(f"❌ Open-Sora failed for scene {scene_num}, trying fallback...")
750
- video_path = self._create_professional_static_video(scene, background_images)
 
 
 
751
 
752
  # If professional video fails, try simple video
753
  if not video_path:
754
- print(f"🔄 Professional video failed, trying simple video for scene {scene_num}...")
755
  video_path = self._create_simple_static_video(scene, background_images)
756
  else:
757
- print(f"🎬 Using static video fallback for scene {scene_num}...")
758
- # Fallback to enhanced static video
759
- video_path = self._create_professional_static_video(scene, background_images)
 
 
 
760
 
761
  if video_path and os.path.exists(video_path):
762
  scene_videos.append(video_path)
@@ -804,6 +861,7 @@ class ProfessionalCartoonFilmGenerator:
804
 
805
  # Use the optimization function to ensure CLIP compatibility
806
  prompt = self.optimize_prompt_for_clip(prompt)
 
807
 
808
  video_path = f"{self.output_dir}/video_scene_{scene['scene_number']}.mp4"
809
 
@@ -815,6 +873,18 @@ class ProfessionalCartoonFilmGenerator:
815
  print("❌ Open-Sora directory not found")
816
  return None
817
 
 
 
 
 
 
 
 
 
 
 
 
 
818
  # Run Open-Sora inference
819
  cmd = [
820
  "torchrun", "--nproc_per_node", "1", "--standalone",
@@ -827,7 +897,14 @@ class ProfessionalCartoonFilmGenerator:
827
  "--motion-score", "6" # High motion for dynamic scenes
828
  ]
829
 
830
- result = subprocess.run(cmd, capture_output=True, text=True, cwd=opensora_dir)
 
 
 
 
 
 
 
831
 
832
  if result.returncode == 0:
833
  # Find generated video file
@@ -835,12 +912,22 @@ class ProfessionalCartoonFilmGenerator:
835
  if file.endswith('.mp4') and 'scene' not in file:
836
  src_path = os.path.join(self.output_dir, file)
837
  os.rename(src_path, video_path)
 
838
  return video_path
 
 
 
 
 
 
839
 
 
 
840
  return None
841
-
842
  except Exception as e:
843
  print(f"❌ Open-Sora generation failed: {e}")
 
 
844
  return None
845
 
846
  def _create_professional_static_video(self, scene: Dict, background_images: Dict) -> str:
@@ -1225,6 +1312,177 @@ class ProfessionalCartoonFilmGenerator:
1225
  }
1226
  return None, error_info, f"❌ Generation failed: {str(e)}", [], []
1227
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1228
  # Initialize professional generator
1229
  generator = ProfessionalCartoonFilmGenerator()
1230
 
 
65
 
66
  @spaces.GPU
67
  def load_models(self):
68
+ """Load all required AI models for professional generation"""
 
 
 
 
 
69
  try:
70
+ print("🚀 Loading professional-grade models...")
71
+
72
+ # Clear GPU memory first
73
+ self.clear_gpu_memory()
74
+
75
+ # Detect device and set appropriate dtype
76
+ self.device = "cuda" if torch.cuda.is_available() else "cpu"
77
+ self.dtype = torch.float16 if self.device == "cuda" else torch.float32
78
+
79
+ print(f"🎮 Using device: {self.device} with dtype: {self.dtype}")
80
+
81
+ # Try to load FLUX first
82
  try:
83
+ print("🎨 Loading FLUX pipeline...")
84
+ from diffusers import FluxPipeline
85
+
86
  self.flux_pipe = FluxPipeline.from_pretrained(
87
  "black-forest-labs/FLUX.1-dev",
88
+ torch_dtype=self.dtype,
89
+ device_map="auto" if self.device == "cuda" else None
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
90
  )
 
 
 
 
 
 
 
 
91
 
92
+ if self.device == "cuda":
93
+ self.flux_pipe = self.flux_pipe.to("cuda")
94
+
95
+ print("✅ FLUX pipeline loaded successfully")
96
+ self.flux_available = True
97
+
98
+ except Exception as e:
99
+ print("🔐 FLUX authentication failed - model requires Hugging Face token")
100
+ print("💡 To use FLUX, you need to:")
101
+ print(" 1. Get a Hugging Face token from https://huggingface.co/settings/tokens")
102
+ print(" 2. Accept the FLUX model license at https://huggingface.co/black-forest-labs/FLUX.1-dev")
103
+ print(" 3. Set your token: huggingface-cli login")
104
+ print("🔄 Falling back to Stable Diffusion...")
105
+ self.flux_available = False
106
+
107
+ # Load Stable Diffusion fallback
108
+ if not self.flux_available:
109
  print("🔄 Loading Stable Diffusion fallback model...")
110
+ from diffusers import StableDiffusionPipeline, DDIMScheduler
111
 
112
+ self.sd_pipe = StableDiffusionPipeline.from_pretrained(
113
+ "CompVis/stable-diffusion-v1-4",
114
+ torch_dtype=self.dtype,
115
+ safety_checker=None,
116
+ requires_safety_checker=False
117
+ )
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
118
 
119
+ # Configure scheduler for better quality
120
+ self.sd_pipe.scheduler = DDIMScheduler.from_config(self.sd_pipe.scheduler.config)
 
 
121
 
122
+ if self.device == "cuda":
123
+ self.sd_pipe = self.sd_pipe.to("cuda")
124
+
125
+ print("✅ Loaded Stable Diffusion v1.4")
126
  print("✅ Stable Diffusion fallback loaded successfully")
127
+
128
+ # Load script enhancement model with correct device
 
 
 
 
 
129
  print("📝 Loading script enhancement model...")
130
+ self.script_model = AutoModelForCausalLM.from_pretrained(
131
+ "microsoft/DialoGPT-medium",
132
+ torch_dtype=self.dtype,
133
+ device_map="auto" if self.device == "cuda" else None
 
134
  )
135
+ self.script_tokenizer = AutoTokenizer.from_pretrained("microsoft/DialoGPT-medium")
136
+
137
+ if self.script_tokenizer.pad_token is None:
138
+ self.script_tokenizer.pad_token = self.script_tokenizer.eos_token
139
+
140
+ if self.device == "cuda":
141
+ self.script_model = self.script_model.to("cuda")
142
+
143
+ print(f"Device set to use {self.device}")
144
  print("✅ Script enhancer loaded")
145
 
146
+ # Set model states
147
+ if self.device == "cuda":
148
+ if self.flux_available:
149
+ self.flux_pipe.enable_model_cpu_offload()
150
+ else:
151
+ self.sd_pipe.enable_model_cpu_offload()
152
+
153
+ print("🎬 All professional models loaded!")
154
+ return True
155
+
156
  except Exception as e:
157
+ print(f"❌ Model loading failed: {e}")
158
+ import traceback
159
+ traceback.print_exc()
160
+ return False
 
161
 
162
  def clear_gpu_memory(self):
163
  """Clear GPU memory between operations"""
 
449
 
450
  @spaces.GPU
451
  def generate_professional_character_images(self, characters: List[Dict]) -> Dict[str, str]:
452
+ """Generate professional character images with consistency"""
 
453
  character_images = {}
454
 
455
+ print(f"🎭 Generating {len(characters)} professional character designs...")
456
+
457
+ # Check if we have any image generation pipeline available
458
+ if not hasattr(self, 'flux_available'):
459
+ print("❌ No image generation models loaded")
460
+ return character_images
461
+
462
+ pipeline = None
463
+ if self.flux_available and hasattr(self, 'flux_pipe'):
464
+ pipeline = self.flux_pipe
465
+ model_name = "FLUX"
466
+ elif hasattr(self, 'sd_pipe'):
467
+ pipeline = self.sd_pipe
468
+ model_name = "Stable Diffusion"
469
+ else:
470
  print("❌ No image generation pipeline available")
471
  return character_images
472
+
473
+ print(f"🎨 Using {model_name} for character generation")
474
 
475
  for character in characters:
476
+ character_name = character['name']
477
+ print(f"\n🎨 Generating character: {character_name}")
478
+
479
  try:
480
+ # Build comprehensive character prompt
481
+ base_prompt = f"Professional cartoon character design, {character['name']}, {character['description']}"
 
 
 
 
 
 
 
482
 
483
+ # Add style and quality modifiers
484
+ if self.flux_available:
485
+ # FLUX-specific prompt
486
+ prompt = f"{base_prompt}, Disney-Pixar animation style, highly detailed character sheet, clean white background, 2D animation model sheet, expressive face, vibrant colors, professional character design, perfect for animation"
487
+ else:
488
+ # Stable Diffusion prompt
489
+ prompt = f"{base_prompt}, anime style, cartoon character, clean background, high quality, detailed, 2D animation style, character sheet"
 
490
 
491
+ # Optimize prompt for CLIP
492
+ prompt = self.optimize_prompt_for_clip(prompt, max_tokens=75)
493
+ print(f"📝 Character prompt: {prompt}")
 
494
 
495
+ # Generate with appropriate settings
496
+ if self.flux_available:
497
+ # FLUX generation settings
498
+ image = pipeline(
499
+ prompt=prompt,
500
+ width=1024,
501
+ height=1024,
502
+ num_inference_steps=25,
503
+ guidance_scale=7.5,
504
+ generator=torch.Generator(device=self.device).manual_seed(42)
505
+ ).images[0]
506
+ else:
507
+ # Stable Diffusion generation settings
508
+ image = pipeline(
509
+ prompt=prompt,
510
+ width=512,
511
+ height=512,
512
+ num_inference_steps=30,
513
+ guidance_scale=7.5,
514
+ generator=torch.Generator(device=self.device).manual_seed(42)
515
+ ).images[0]
516
+ # Upscale for SD
517
+ image = image.resize((1024, 1024), Image.Resampling.LANCZOS)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
518
 
519
+ # Save character image
520
  char_path = f"{self.output_dir}/char_{character['name'].replace(' ', '_')}.png"
521
  image.save(char_path)
 
 
 
 
 
 
522
 
523
+ # Verify file was created
524
+ if os.path.exists(char_path):
525
+ file_size = os.path.getsize(char_path)
526
+ character_images[character_name] = char_path
527
+
528
+ # Create download URL
529
+ download_info = self.create_download_url(char_path, f"character_{character['name']}")
530
+ print(f"📥 Generated character_{character['name']}: char_{character['name'].replace(' ', '_')}.png")
531
+ print(f" 📊 File size: {file_size / (1024*1024):.1f} MB")
532
+ print(f" 📁 Internal path: {char_path}")
533
+ print(download_info)
534
+
535
+ # Clear GPU memory after each generation
536
+ if self.device == "cuda":
537
+ torch.cuda.empty_cache()
538
+ gc.collect()
539
+ else:
540
+ print(f"❌ Failed to save character image: {char_path}")
541
 
542
  except Exception as e:
543
+ print(f"❌ Error generating character {character_name}: {e}")
544
+ import traceback
545
+ traceback.print_exc()
546
+ # Continue with next character
547
+ continue
548
+
549
+ print(f"\n📊 Character generation summary:")
550
+ print(f" - Characters requested: {len(characters)}")
551
+ print(f" - Characters generated: {len(character_images)}")
552
+ print(f" - Success rate: {len(character_images)/len(characters)*100:.1f}%")
553
 
554
  return character_images
555
 
556
  @spaces.GPU
557
  def generate_cinematic_backgrounds(self, scenes: List[Dict], color_palette: str) -> Dict[int, str]:
558
+ """Generate professional cinematic backgrounds for each scene"""
 
559
  background_images = {}
560
 
561
+ print(f"🎞️ Generating {len(scenes)} cinematic backgrounds...")
562
+
563
+ # Check if we have any image generation pipeline available
564
+ if not hasattr(self, 'flux_available'):
565
+ print("❌ No image generation models loaded")
566
+ return background_images
567
+
568
+ pipeline = None
569
+ if self.flux_available and hasattr(self, 'flux_pipe'):
570
+ pipeline = self.flux_pipe
571
+ model_name = "FLUX"
572
+ elif hasattr(self, 'sd_pipe'):
573
+ pipeline = self.sd_pipe
574
+ model_name = "Stable Diffusion"
575
+ else:
576
  print("❌ No image generation pipeline available")
577
  return background_images
578
+
579
+ print(f"🎨 Using {model_name} for background generation")
580
 
581
  for scene in scenes:
582
+ scene_num = scene['scene_number']
583
+ print(f"\n🌄 Generating background for scene {scene_num}")
584
+
585
  try:
586
+ # Build cinematic background prompt
587
+ background_desc = scene['background']
588
+ mood = scene.get('mood', 'neutral')
589
+ shot_type = scene.get('shot_type', 'medium shot')
590
+ lighting = scene.get('lighting', 'natural lighting')
591
 
592
+ base_prompt = f"Cinematic background scene, {background_desc}, {mood} atmosphere, {lighting}"
 
 
 
 
593
 
594
+ # Add style and quality modifiers
595
+ if self.flux_available:
596
+ prompt = f"{base_prompt}, Disney-Pixar animation style, detailed landscape, professional background art, vibrant colors, high quality, cinematic composition, no characters"
597
+ else:
598
+ prompt = f"{base_prompt}, anime style background, detailed landscape, high quality, cinematic, {color_palette} color palette, no people"
599
 
600
+ # Optimize for CLIP
601
+ prompt = self.optimize_prompt_for_clip(prompt, max_tokens=75)
602
+ print(f"📝 Background prompt: {prompt}")
 
603
 
604
+ # Generate with appropriate settings
605
+ if self.flux_available:
606
+ # FLUX generation settings
607
+ image = pipeline(
608
+ prompt=prompt,
609
+ width=1024,
610
+ height=768, # 4:3 aspect ratio for video
611
+ num_inference_steps=25,
612
+ guidance_scale=7.5,
613
+ generator=torch.Generator(device=self.device).manual_seed(scene_num * 10)
614
+ ).images[0]
615
+ else:
616
+ # Stable Diffusion generation settings
617
+ image = pipeline(
618
+ prompt=prompt,
619
+ width=512,
620
+ height=384, # 4:3 aspect ratio
621
+ num_inference_steps=30,
622
+ guidance_scale=7.5,
623
+ generator=torch.Generator(device=self.device).manual_seed(scene_num * 10)
624
+ ).images[0]
625
+ # Upscale for SD
626
+ image = image.resize((1024, 768), Image.Resampling.LANCZOS)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
627
 
628
+ # Save background image
629
+ bg_path = f"{self.output_dir}/bg_scene_{scene_num}.png"
630
  image.save(bg_path)
 
 
 
 
 
 
631
 
632
+ # Verify file was created
633
+ if os.path.exists(bg_path):
634
+ file_size = os.path.getsize(bg_path)
635
+ background_images[scene_num] = bg_path
636
+
637
+ # Create download URL
638
+ download_info = self.create_download_url(bg_path, f"background_scene_{scene_num}")
639
+ print(f"📥 Generated background_scene_{scene_num}: bg_scene_{scene_num}.png")
640
+ print(f" 📊 File size: {file_size / (1024*1024):.1f} MB")
641
+ print(f" 📁 Internal path: {bg_path}")
642
+ print(download_info)
643
+
644
+ # Clear GPU memory after each generation
645
+ if self.device == "cuda":
646
+ torch.cuda.empty_cache()
647
+ gc.collect()
648
+ else:
649
+ print(f"❌ Failed to save background image: {bg_path}")
650
 
651
  except Exception as e:
652
  print(f"❌ Error generating background for scene {scene['scene_number']}: {e}")
653
+ import traceback
654
+ traceback.print_exc()
655
+ # Continue with next scene
656
+ continue
657
+
658
+ print(f"\n📊 Background generation summary:")
659
+ print(f" - Scenes requested: {len(scenes)}")
660
+ print(f" - Backgrounds generated: {len(background_images)}")
661
+ print(f" - Success rate: {len(background_images)/len(scenes)*100:.1f}%")
662
 
663
  return background_images
664
 
 
667
  try:
668
  print("🎬 Setting up Open-Sora 2.0 for video generation...")
669
 
670
+ # Check available GPU memory
671
+ if torch.cuda.is_available():
672
+ gpu_memory = torch.cuda.get_device_properties(0).total_memory / (1024**3)
673
+ print(f"🎮 Available GPU memory: {gpu_memory:.1f} GB")
674
+ if gpu_memory < 16:
675
+ print("⚠️ Warning: Open-Sora requires 16GB+ GPU memory for stable operation")
676
+
677
  # Check if we're already in the right directory
678
  current_dir = os.getcwd()
679
  opensora_dir = os.path.join(current_dir, "Open-Sora")
 
681
  # Clone Open-Sora repository if it doesn't exist
682
  if not os.path.exists(opensora_dir):
683
  print("📥 Cloning Open-Sora repository...")
684
+ try:
685
+ result = subprocess.run([
686
+ "git", "clone", "https://github.com/hpcaitech/Open-Sora.git"
687
+ ], check=True, capture_output=True, text=True, timeout=120)
688
+ print("✅ Repository cloned successfully")
689
+ except subprocess.TimeoutExpired:
690
+ print("❌ Repository cloning timed out")
691
+ return False
692
+ except subprocess.CalledProcessError as e:
693
+ print(f"❌ Repository cloning failed: {e.stderr}")
694
+ return False
695
 
696
  # Check if the repository was cloned successfully
697
  if not os.path.exists(opensora_dir):
698
  print("❌ Failed to clone Open-Sora repository")
699
  return False
700
 
701
+ # Check for required scripts
702
+ script_path = os.path.join(opensora_dir, "scripts/diffusion/inference.py")
703
+ config_path = os.path.join(opensora_dir, "configs/diffusion/inference/t2i2v_256px.py")
704
+
705
+ print(f"📁 Checking for script: {script_path}")
706
+ print(f"📁 Checking for config: {config_path}")
707
+
708
+ if not os.path.exists(script_path):
709
+ print(f"❌ Required script not found: {script_path}")
710
+ # List available files for debugging
711
+ scripts_dir = os.path.join(opensora_dir, "scripts")
712
+ if os.path.exists(scripts_dir):
713
+ print(f"📁 Available in scripts/: {os.listdir(scripts_dir)}")
714
+ return False
715
+
716
+ if not os.path.exists(config_path):
717
+ print(f"❌ Required config not found: {config_path}")
718
+ # List available configs for debugging
719
+ configs_dir = os.path.join(opensora_dir, "configs")
720
+ if os.path.exists(configs_dir):
721
+ print(f"📁 Available in configs/: {os.listdir(configs_dir)}")
722
+ return False
723
+
724
  # Check if model weights exist
725
  ckpts_dir = os.path.join(opensora_dir, "ckpts")
726
  if not os.path.exists(ckpts_dir):
727
  print("📥 Downloading Open-Sora 2.0 model...")
728
  try:
729
+ # Use smaller timeout and check if huggingface-cli is available
730
+ result = subprocess.run([
731
  "huggingface-cli", "download", "hpcai-tech/Open-Sora-v2",
732
  "--local-dir", ckpts_dir
733
+ ], check=True, capture_output=True, text=True, timeout=300)
734
+ print("✅ Model downloaded successfully")
735
+ except subprocess.TimeoutExpired:
736
+ print("❌ Model download timed out (5 minutes)")
737
+ return False
738
+ except subprocess.CalledProcessError as e:
739
+ print(f"❌ Model download failed: {e.stderr}")
740
+ return False
741
+ except FileNotFoundError:
742
+ print("❌ huggingface-cli not found - cannot download model")
743
+ return False
744
+ else:
745
+ print("✅ Model weights already exist")
746
+
747
+ # Check dependencies
748
+ try:
749
+ import torch.distributed
750
+ print("✅ torch.distributed available")
751
+ except ImportError:
752
+ print("❌ torch.distributed not available")
753
+ return False
754
+
755
+ # Test if torchrun is available
756
+ try:
757
+ result = subprocess.run(["torchrun", "--help"],
758
+ capture_output=True, text=True, timeout=10)
759
+ if result.returncode == 0:
760
+ print("✅ torchrun available")
761
+ else:
762
+ print("❌ torchrun not working properly")
763
  return False
764
+ except (subprocess.TimeoutExpired, FileNotFoundError):
765
+ print("❌ torchrun not found")
766
+ return False
767
 
768
  print("✅ Open-Sora setup completed")
769
  return True
770
 
771
  except Exception as e:
772
  print(f"❌ Open-Sora setup failed: {e}")
773
+ import traceback
774
+ traceback.print_exc()
775
  return False
776
 
777
  @spaces.GPU
 
797
  if video_path:
798
  print(f"✅ Open-Sora video generated for scene {scene_num}")
799
  else:
800
+ print(f"❌ Open-Sora failed for scene {scene_num}, trying lightweight animation...")
801
+ video_path = self._create_lightweight_animated_video(scene, character_images, background_images)
802
+ if not video_path:
803
+ print(f"🔄 Lightweight animation failed, trying static video...")
804
+ video_path = self._create_professional_static_video(scene, background_images)
805
 
806
  # If professional video fails, try simple video
807
  if not video_path:
808
+ print(f"🔄 All methods failed, trying simple video for scene {scene_num}...")
809
  video_path = self._create_simple_static_video(scene, background_images)
810
  else:
811
+ print(f"🎬 Open-Sora not available, using lightweight animation for scene {scene_num}...")
812
+ # First try lightweight animation, then fallback to static
813
+ video_path = self._create_lightweight_animated_video(scene, character_images, background_images)
814
+ if not video_path:
815
+ print(f"🔄 Lightweight animation failed, using static video fallback...")
816
+ video_path = self._create_professional_static_video(scene, background_images)
817
 
818
  if video_path and os.path.exists(video_path):
819
  scene_videos.append(video_path)
 
861
 
862
  # Use the optimization function to ensure CLIP compatibility
863
  prompt = self.optimize_prompt_for_clip(prompt)
864
+ print(f"🎬 Open-Sora prompt: {prompt}")
865
 
866
  video_path = f"{self.output_dir}/video_scene_{scene['scene_number']}.mp4"
867
 
 
873
  print("❌ Open-Sora directory not found")
874
  return None
875
 
876
+ # Check for required files
877
+ script_path = os.path.join(opensora_dir, "scripts/diffusion/inference.py")
878
+ config_path = os.path.join(opensora_dir, "configs/diffusion/inference/t2i2v_256px.py")
879
+
880
+ if not os.path.exists(script_path):
881
+ print(f"❌ Open-Sora script not found: {script_path}")
882
+ return None
883
+
884
+ if not os.path.exists(config_path):
885
+ print(f"❌ Open-Sora config not found: {config_path}")
886
+ return None
887
+
888
  # Run Open-Sora inference
889
  cmd = [
890
  "torchrun", "--nproc_per_node", "1", "--standalone",
 
897
  "--motion-score", "6" # High motion for dynamic scenes
898
  ]
899
 
900
+ print(f"🎬 Running Open-Sora command: {' '.join(cmd)}")
901
+ result = subprocess.run(cmd, capture_output=True, text=True, cwd=opensora_dir, timeout=300)
902
+
903
+ print(f"🎬 Open-Sora return code: {result.returncode}")
904
+ if result.stdout:
905
+ print(f"🎬 Open-Sora stdout: {result.stdout}")
906
+ if result.stderr:
907
+ print(f"❌ Open-Sora stderr: {result.stderr}")
908
 
909
  if result.returncode == 0:
910
  # Find generated video file
 
912
  if file.endswith('.mp4') and 'scene' not in file:
913
  src_path = os.path.join(self.output_dir, file)
914
  os.rename(src_path, video_path)
915
+ print(f"✅ Open-Sora video generated: {video_path}")
916
  return video_path
917
+
918
+ print("❌ Open-Sora completed but no video file found")
919
+ return None
920
+ else:
921
+ print(f"❌ Open-Sora failed with return code: {result.returncode}")
922
+ return None
923
 
924
+ except subprocess.TimeoutExpired:
925
+ print("❌ Open-Sora generation timed out (5 minutes)")
926
  return None
 
927
  except Exception as e:
928
  print(f"❌ Open-Sora generation failed: {e}")
929
+ import traceback
930
+ traceback.print_exc()
931
  return None
932
 
933
  def _create_professional_static_video(self, scene: Dict, background_images: Dict) -> str:
 
1312
  }
1313
  return None, error_info, f"❌ Generation failed: {str(e)}", [], []
1314
 
1315
+ def _create_lightweight_animated_video(self, scene: Dict, character_images: Dict, background_images: Dict) -> str:
1316
+ """Create lightweight animated video with character/background compositing"""
1317
+ scene_num = scene['scene_number']
1318
+
1319
+ if scene_num not in background_images:
1320
+ print(f"❌ No background image for scene {scene_num}")
1321
+ return None
1322
+
1323
+ video_path = f"{self.output_dir}/video_animated_scene_{scene_num}.mp4"
1324
+
1325
+ try:
1326
+ print(f"🎬 Creating lightweight animated video for scene {scene_num}...")
1327
+
1328
+ # Load background image
1329
+ bg_path = background_images[scene_num]
1330
+ print(f"📁 Loading background from: {bg_path}")
1331
+
1332
+ if not os.path.exists(bg_path):
1333
+ print(f"❌ Background file not found: {bg_path}")
1334
+ return None
1335
+
1336
+ bg_image = Image.open(bg_path).resize((1024, 768))
1337
+ bg_array = np.array(bg_image)
1338
+ bg_array = cv2.cvtColor(bg_array, cv2.COLOR_RGB2BGR)
1339
+
1340
+ # Try to load character images for this scene
1341
+ scene_characters = scene.get('characters_present', [])
1342
+ character_overlays = []
1343
+
1344
+ for char_name in scene_characters:
1345
+ for char_key, char_path in character_images.items():
1346
+ if char_name.lower() in char_key.lower():
1347
+ if os.path.exists(char_path):
1348
+ char_img = Image.open(char_path).convert("RGBA")
1349
+ # Resize character to reasonable size (25% of background)
1350
+ char_w, char_h = char_img.size
1351
+ new_h = int(768 * 0.25) # 25% of background height
1352
+ new_w = int(char_w * (new_h / char_h))
1353
+ char_img = char_img.resize((new_w, new_h))
1354
+ character_overlays.append({
1355
+ 'image': np.array(char_img),
1356
+ 'name': char_name,
1357
+ 'original_pos': (100 + len(character_overlays) * 200, 768 - new_h - 50) # Bottom positioning
1358
+ })
1359
+ print(f"✅ Loaded character: {char_name}")
1360
+ break
1361
+
1362
+ print(f"📐 Background size: {bg_array.shape}")
1363
+ print(f"🎭 Characters loaded: {len(character_overlays)}")
1364
+
1365
+ # Professional video settings
1366
+ fourcc = cv2.VideoWriter_fourcc(*'mp4v')
1367
+ fps = 24 # Cinematic frame rate
1368
+ duration = int(scene.get('duration', 35))
1369
+ total_frames = duration * fps
1370
+
1371
+ print(f"🎬 Video settings: {fps}fps, {duration}s duration, {total_frames} frames")
1372
+
1373
+ out = cv2.VideoWriter(video_path, fourcc, fps, (1024, 768))
1374
+
1375
+ if not out.isOpened():
1376
+ print(f"❌ Failed to open video writer for {video_path}")
1377
+ return None
1378
+
1379
+ # Advanced animation with character movement
1380
+ print(f"🎬 Generating {total_frames} animated frames...")
1381
+
1382
+ for i in range(total_frames):
1383
+ if i % 100 == 0: # Progress update every 100 frames
1384
+ print(f" Frame {i}/{total_frames} ({i/total_frames*100:.1f}%)")
1385
+
1386
+ frame = bg_array.copy()
1387
+ progress = i / total_frames
1388
+
1389
+ # Apply cinematic background effects
1390
+ frame = self._apply_cinematic_effects(frame, scene, progress)
1391
+
1392
+ # Animate characters if available
1393
+ for j, char_data in enumerate(character_overlays):
1394
+ char_img = char_data['image']
1395
+ char_name = char_data['name']
1396
+ base_x, base_y = char_data['original_pos']
1397
+
1398
+ # Different animation patterns based on scene mood
1399
+ mood = scene.get('mood', 'heartwarming')
1400
+
1401
+ if mood == 'exciting':
1402
+ # Bouncing animation
1403
+ offset_y = int(np.sin(progress * 8 * np.pi + j * np.pi/2) * 20)
1404
+ offset_x = int(np.sin(progress * 4 * np.pi + j * np.pi/3) * 15)
1405
+ elif mood == 'peaceful':
1406
+ # Gentle swaying
1407
+ offset_y = int(np.sin(progress * 2 * np.pi + j * np.pi/2) * 8)
1408
+ offset_x = int(np.sin(progress * 1.5 * np.pi + j * np.pi/3) * 12)
1409
+ elif mood == 'mysterious':
1410
+ # Subtle floating
1411
+ offset_y = int(np.sin(progress * 3 * np.pi + j * np.pi/2) * 15)
1412
+ offset_x = int(np.cos(progress * 2 * np.pi + j * np.pi/4) * 10)
1413
+ else:
1414
+ # Default: slight breathing animation
1415
+ scale_factor = 1.0 + np.sin(progress * 4 * np.pi + j * np.pi/2) * 0.02
1416
+ offset_y = int(np.sin(progress * 3 * np.pi + j * np.pi/2) * 5)
1417
+ offset_x = 0
1418
+
1419
+ # Calculate final position
1420
+ final_x = base_x + offset_x
1421
+ final_y = base_y + offset_y
1422
+
1423
+ # Overlay character on frame
1424
+ if char_img.shape[2] == 4: # Has alpha channel
1425
+ frame = self._overlay_character(frame, char_img, final_x, final_y)
1426
+ else:
1427
+ # Simple overlay without alpha
1428
+ char_rgb = cv2.cvtColor(char_img[:,:,:3], cv2.COLOR_RGB2BGR)
1429
+ h, w = char_rgb.shape[:2]
1430
+ if (final_y >= 0 and final_y + h < 768 and
1431
+ final_x >= 0 and final_x + w < 1024):
1432
+ frame[final_y:final_y+h, final_x:final_x+w] = char_rgb
1433
+
1434
+ out.write(frame)
1435
+
1436
+ print(f"✅ All {total_frames} animated frames generated")
1437
+
1438
+ out.release()
1439
+
1440
+ if os.path.exists(video_path):
1441
+ file_size = os.path.getsize(video_path)
1442
+ print(f"✅ Lightweight animated video created: {video_path} ({file_size / (1024*1024):.1f} MB)")
1443
+ return video_path
1444
+ else:
1445
+ print(f"❌ Video file not created: {video_path}")
1446
+ return None
1447
+
1448
+ except Exception as e:
1449
+ print(f"❌ Lightweight animated video creation failed for scene {scene_num}: {e}")
1450
+ import traceback
1451
+ traceback.print_exc()
1452
+ return None
1453
+
1454
+ def _overlay_character(self, background, character_rgba, x, y):
1455
+ """Overlay character with alpha transparency on background"""
1456
+ try:
1457
+ char_h, char_w = character_rgba.shape[:2]
1458
+ bg_h, bg_w = background.shape[:2]
1459
+
1460
+ # Ensure the character fits within background bounds
1461
+ if x < 0 or y < 0 or x + char_w > bg_w or y + char_h > bg_h:
1462
+ return background
1463
+
1464
+ # Extract RGB and alpha channels
1465
+ char_rgb = character_rgba[:, :, :3]
1466
+ char_alpha = character_rgba[:, :, 3] / 255.0
1467
+
1468
+ # Convert character to BGR for OpenCV
1469
+ char_bgr = cv2.cvtColor(char_rgb, cv2.COLOR_RGB2BGR)
1470
+
1471
+ # Get the region of interest from background
1472
+ roi = background[y:y+char_h, x:x+char_w]
1473
+
1474
+ # Blend character with background using alpha
1475
+ for c in range(3):
1476
+ roi[:, :, c] = (char_alpha * char_bgr[:, :, c] +
1477
+ (1 - char_alpha) * roi[:, :, c])
1478
+
1479
+ background[y:y+char_h, x:x+char_w] = roi
1480
+ return background
1481
+
1482
+ except Exception as e:
1483
+ print(f"⚠️ Character overlay failed: {e}")
1484
+ return background
1485
+
1486
  # Initialize professional generator
1487
  generator = ProfessionalCartoonFilmGenerator()
1488