Expertnocode commited on
Commit
96bb40d
·
verified ·
1 Parent(s): 147a0c7

Upload app.py with huggingface_hub

Browse files
Files changed (1) hide show
  1. app.py +161 -58
app.py CHANGED
@@ -73,7 +73,7 @@ def extract_video_path(result):
73
  return result
74
 
75
  def create_slideshow_video(images, duration_per_image=3.0, fps=30):
76
- """Crée une vidéo diaporama à partir de plusieurs images"""
77
  import cv2
78
  import numpy as np
79
 
@@ -88,16 +88,47 @@ def create_slideshow_video(images, duration_per_image=3.0, fps=30):
88
  out = cv2.VideoWriter(output_path, fourcc, fps, (width, height))
89
 
90
  frames_per_image = int(duration_per_image * fps)
 
91
 
92
- for img_path in images:
93
  img = cv2.imread(img_path)
94
  if img is not None:
95
- # Redimensionner l'image si nécessaire
96
  img = cv2.resize(img, (width, height))
97
 
98
- # Écrire les frames pour cette image
99
- for _ in range(frames_per_image):
100
- out.write(img)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
101
 
102
  out.release()
103
  return output_path
@@ -124,6 +155,103 @@ def create_static_background_video(image_path, duration=10.0, fps=30):
124
  out.release()
125
  return output_path
126
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
127
  def generate_background_video(images, prompt="Smooth camera movement through the venue"):
128
  """
129
  Génère une vidéo de fond à partir des images uploadées
@@ -178,6 +306,7 @@ def generate_background_video(images, prompt="Smooth camera movement through the
178
  def generate_talking_avatar(avatar_image, audio_file, model_choice="LatentSync"):
179
  """
180
  Génère une vidéo d'avatar parlant à partir d'une image et d'un audio
 
181
  """
182
  if not avatar_image:
183
  return None, "Veuillez uploader une image d'avatar"
@@ -185,67 +314,41 @@ def generate_talking_avatar(avatar_image, audio_file, model_choice="LatentSync")
185
  if not audio_file:
186
  return None, "Veuillez fournir un fichier audio"
187
 
188
- # Essayer différents espaces et API endpoints jusqu'à ce qu'un fonctionne
189
- spaces_to_try = AVATAR_SPACES.get(model_choice, [])
190
- errors_log = []
191
- api_tests = get_avatar_api_tests(avatar_image, audio_file)
 
 
 
 
192
 
 
 
 
 
193
  for space_id in spaces_to_try:
194
- print(f"[DEBUG] Connexion à {space_id}...")
195
  try:
196
- # Créer le client avec token si disponible
197
  if HF_TOKEN:
198
  client = Client(space_id, hf_token=HF_TOKEN)
199
  else:
200
  client = Client(space_id)
 
 
 
 
 
 
 
201
  except Exception as e:
202
- error_info = f"{space_id}: Échec de connexion - {str(e)[:100]}"
203
- errors_log.append(error_info)
204
- print(f"[DEBUG] ❌ {error_info}")
205
- continue
206
-
207
- for api_name, test_func in api_tests:
208
- try:
209
- # Essayer avec cette configuration d'API
210
- print(f"[DEBUG] Test {space_id} avec config={api_name}")
211
- result = test_func(client)
212
-
213
- # Extraire le chemin vidéo depuis le résultat
214
- video_path = None
215
- if isinstance(result, tuple):
216
- video_path = result[0]
217
- elif isinstance(result, dict):
218
- video_path = result.get('video') or result.get('path') or result.get('value')
219
- elif isinstance(result, str):
220
- video_path = result
221
- else:
222
- video_path = result
223
-
224
- if video_path:
225
- print(f"[DEBUG] ✅ Succès avec {space_id}, config={api_name}")
226
- return video_path, f"✅ Avatar généré ! (Space: {space_id}, Config: {api_name or 'default'})"
227
-
228
- except Exception as e:
229
- error_msg = str(e)
230
- error_info = f"{space_id} (config={api_name}): {error_msg[:200]}"
231
- errors_log.append(error_info)
232
- print(f"[DEBUG] ❌ {error_info}")
233
-
234
- # Si c'est une erreur de "too many arguments" ou "api_name", essayer le suivant
235
- error_keywords = ["too many", "api_name", "could not find", "connection", "not iterable", "bool", "argument"]
236
- if any(x in error_msg.lower() for x in error_keywords):
237
- continue
238
- elif "runtime_error" in error_msg.lower() or "invalid state" in error_msg.lower():
239
- # Ce space est en erreur, passer au suivant
240
- print(f"[DEBUG] Space {space_id} est en RUNTIME_ERROR, passage au suivant")
241
- break
242
- else:
243
- # Si c'est une autre erreur sérieuse, essayer quand même les autres configs
244
- continue
245
 
246
- # Si on arrive ici, aucun espace n'a fonctionné
247
- error_summary = "\n".join(errors_log[-3:]) # Montrer les 3 dernières erreurs
248
- return None, f"❌ Aucun service {model_choice} n'est disponible actuellement.\n\nErreurs récentes:\n{error_summary}"
 
 
 
249
 
250
  def compose_videos(background_video, avatar_video, position="bottom-right", scale=0.3):
251
  """
 
73
  return result
74
 
75
  def create_slideshow_video(images, duration_per_image=3.0, fps=30):
76
+ """Crée une vidéo diaporama animée à partir de plusieurs images"""
77
  import cv2
78
  import numpy as np
79
 
 
88
  out = cv2.VideoWriter(output_path, fourcc, fps, (width, height))
89
 
90
  frames_per_image = int(duration_per_image * fps)
91
+ transition_frames = 30 # 1 seconde de transition
92
 
93
+ for i, img_path in enumerate(images):
94
  img = cv2.imread(img_path)
95
  if img is not None:
 
96
  img = cv2.resize(img, (width, height))
97
 
98
+ # Animation de zoom/pan Ken Burns effect
99
+ for frame in range(frames_per_image):
100
+ # Effet de zoom progressif
101
+ progress = frame / frames_per_image
102
+ zoom = 1.0 + 0.1 * progress # Zoom de 0% à 10%
103
+
104
+ # Calculer nouvelles dimensions
105
+ new_width = int(width * zoom)
106
+ new_height = int(height * zoom)
107
+
108
+ # Redimensionner
109
+ zoomed = cv2.resize(img, (new_width, new_height))
110
+
111
+ # Centrer et cropper
112
+ x_offset = (new_width - width) // 2
113
+ y_offset = (new_height - height) // 2
114
+
115
+ if x_offset >= 0 and y_offset >= 0:
116
+ cropped = zoomed[y_offset:y_offset+height, x_offset:x_offset+width]
117
+ else:
118
+ cropped = img
119
+
120
+ out.write(cropped)
121
+
122
+ # Transition fade vers l'image suivante (si pas dernière image)
123
+ if i < len(images) - 1:
124
+ next_img = cv2.imread(images[i + 1])
125
+ if next_img is not None:
126
+ next_img = cv2.resize(next_img, (width, height))
127
+
128
+ for t in range(transition_frames):
129
+ alpha = t / transition_frames
130
+ blended = cv2.addWeighted(img, 1 - alpha, next_img, alpha, 0)
131
+ out.write(blended)
132
 
133
  out.release()
134
  return output_path
 
155
  out.release()
156
  return output_path
157
 
158
+ def get_audio_duration(audio_path):
159
+ """Obtient la durée d'un fichier audio en secondes"""
160
+ try:
161
+ import librosa
162
+ duration = librosa.get_duration(path=audio_path)
163
+ return duration
164
+ except:
165
+ # Fallback sans librosa
166
+ try:
167
+ import wave
168
+ with wave.open(audio_path, 'r') as f:
169
+ frames = f.getnframes()
170
+ rate = f.getframerate()
171
+ duration = frames / float(rate)
172
+ return duration
173
+ except:
174
+ # Fallback par défaut
175
+ return 10.0
176
+
177
+ def create_simple_talking_avatar(avatar_image, audio_file):
178
+ """Crée un avatar parlant simple avec légères animations"""
179
+ import cv2
180
+ import numpy as np
181
+
182
+ # Obtenir la durée de l'audio
183
+ duration = get_audio_duration(audio_file)
184
+ fps = 30
185
+ total_frames = int(duration * fps)
186
+
187
+ output_path = tempfile.mktemp(suffix='.mp4')
188
+
189
+ # Lire l'image d'avatar
190
+ img = cv2.imread(avatar_image)
191
+ height, width = img.shape[:2]
192
+
193
+ # Créer le writer vidéo
194
+ fourcc = cv2.VideoWriter_fourcc(*'mp4v')
195
+ out = cv2.VideoWriter(output_path, fourcc, fps, (width, height))
196
+
197
+ for frame_num in range(total_frames):
198
+ # Créer une légère oscillation pour simuler la parole
199
+ scale_factor = 1.0 + 0.02 * np.sin(frame_num * 0.3) # Oscillation douce
200
+
201
+ # Redimensionner légèrement l'image
202
+ new_width = int(width * scale_factor)
203
+ new_height = int(height * scale_factor)
204
+
205
+ if new_width > 0 and new_height > 0:
206
+ resized = cv2.resize(img, (new_width, new_height))
207
+
208
+ # Centrer l'image redimensionnée
209
+ if new_width > width or new_height > height:
210
+ # Crop si plus grand
211
+ x_offset = (new_width - width) // 2
212
+ y_offset = (new_height - height) // 2
213
+ frame = resized[y_offset:y_offset+height, x_offset:x_offset+width]
214
+ else:
215
+ # Pad si plus petit
216
+ frame = np.zeros((height, width, 3), dtype=np.uint8)
217
+ x_offset = (width - new_width) // 2
218
+ y_offset = (height - new_height) // 2
219
+ frame[y_offset:y_offset+new_height, x_offset:x_offset+new_width] = resized
220
+ else:
221
+ frame = img
222
+
223
+ out.write(frame)
224
+
225
+ out.release()
226
+
227
+ # Ajouter l'audio à la vidéo
228
+ return add_audio_to_video(output_path, audio_file)
229
+
230
+ def create_static_avatar_with_audio(avatar_image, audio_file):
231
+ """Crée un avatar statique avec audio"""
232
+ duration = get_audio_duration(audio_file)
233
+ video_path = create_static_background_video(avatar_image, duration)
234
+ return add_audio_to_video(video_path, audio_file)
235
+
236
+ def add_audio_to_video(video_path, audio_path):
237
+ """Ajoute l'audio à une vidéo (nécessite ffmpeg)"""
238
+ try:
239
+ import subprocess
240
+ output_path = tempfile.mktemp(suffix='.mp4')
241
+
242
+ cmd = [
243
+ 'ffmpeg', '-i', video_path, '-i', audio_path,
244
+ '-c:v', 'copy', '-c:a', 'aac', '-strict', 'experimental',
245
+ '-y', output_path
246
+ ]
247
+
248
+ subprocess.run(cmd, check=True, capture_output=True)
249
+ return output_path
250
+ except:
251
+ # Si ffmpeg n'est pas disponible, retourner juste la vidéo
252
+ print("[DEBUG] ffmpeg non disponible, vidéo sans audio")
253
+ return video_path
254
+
255
  def generate_background_video(images, prompt="Smooth camera movement through the venue"):
256
  """
257
  Génère une vidéo de fond à partir des images uploadées
 
306
  def generate_talking_avatar(avatar_image, audio_file, model_choice="LatentSync"):
307
  """
308
  Génère une vidéo d'avatar parlant à partir d'une image et d'un audio
309
+ Fallback vers une solution locale simple
310
  """
311
  if not avatar_image:
312
  return None, "Veuillez uploader une image d'avatar"
 
314
  if not audio_file:
315
  return None, "Veuillez fournir un fichier audio"
316
 
317
+ # 1. Solution locale : avatar qui "bouge" légèrement pendant l'audio
318
+ try:
319
+ print("[DEBUG] Génération d'avatar local...")
320
+ video_path = create_simple_talking_avatar(avatar_image, audio_file)
321
+ if video_path:
322
+ return video_path, "✅ Avatar parlant créé localement (solution de fallback)"
323
+ except Exception as e:
324
+ print(f"[DEBUG] Erreur avatar local: {e}")
325
 
326
+ # 2. Test rapide des APIs externes (heritage code)
327
+ spaces_to_try = AVATAR_SPACES.get(model_choice, [])[:1] # Test juste le premier
328
+ errors_log = ["Services externes indisponibles - utilisation du fallback local"]
329
+
330
  for space_id in spaces_to_try:
 
331
  try:
 
332
  if HF_TOKEN:
333
  client = Client(space_id, hf_token=HF_TOKEN)
334
  else:
335
  client = Client(space_id)
336
+
337
+ api_tests = get_avatar_api_tests(avatar_image, audio_file)
338
+ api_name, test_func = api_tests[0]
339
+ result = test_func(client)
340
+ video_path = extract_video_path(result)
341
+ if video_path:
342
+ return video_path, f"✅ Avatar généré ! (Space: {space_id})"
343
  except Exception as e:
344
+ errors_log.append(f"{space_id}: {str(e)[:50]}...")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
345
 
346
+ # 3. Fallback final : avatar statique avec audio
347
+ try:
348
+ video_path = create_static_avatar_with_audio(avatar_image, audio_file)
349
+ return video_path, "✅ Avatar statique avec audio créé (fallback final)"
350
+ except Exception as e:
351
+ return None, f"❌ Erreur lors de la création de l'avatar: {str(e)}"
352
 
353
  def compose_videos(background_video, avatar_video, position="bottom-right", scale=0.3):
354
  """