gnosticdev commited on
Commit
dc5a3c6
verified
1 Parent(s): e1b8370

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +103 -75
app.py CHANGED
@@ -1,92 +1,104 @@
1
- import os
2
  import math
3
  import tempfile
4
  import logging
5
  from PIL import Image
6
- from pydub import AudioSegment
 
7
  from moviepy.editor import (
8
  VideoFileClip, AudioFileClip, ImageClip,
9
  concatenate_videoclips, CompositeVideoClip, CompositeAudioClip
10
  )
11
  import edge_tts
12
  import gradio as gr
13
- import asyncio
 
 
 
14
 
15
- # PATCH PARA PILLOW 10+
16
- Image.ANTIALIAS = Image.Resampling.LANCZOS
17
 
18
- # CONFIGURACI脫N
19
  INTRO_VIDEO = "introvideo.mp4"
20
  OUTRO_VIDEO = "outrovideo.mp4"
21
  MUSIC_BG = "musicafondo.mp3"
22
  FX_SOUND = "fxsound.mp3"
23
  WATERMARK = "watermark.png"
24
 
25
- # Validar archivos
26
  for file in [INTRO_VIDEO, OUTRO_VIDEO, MUSIC_BG, FX_SOUND, WATERMARK]:
27
  if not os.path.exists(file):
28
- raise FileNotFoundError(f"Falta: {file}")
29
-
30
- async def procesar_audio(texto, voz, duracion_total):
31
- try:
32
- communicate = edge_tts.Communicate(texto, voz)
33
- with tempfile.NamedTemporaryFile(delete=False, suffix=".mp3") as tmp_tts:
34
- await communicate.save(tmp_tts.name)
35
- tts_path = tmp_tts.name
36
-
37
- # Verificar que el archivo TTS es v谩lido
38
- if not os.path.exists(tts_path) or os.path.getsize(tts_path) == 0:
39
- raise ValueError("El archivo TTS generado est谩 vac铆o o corrupto.")
40
-
41
- tts_audio = AudioSegment.from_mp3(tts_path)
42
-
43
- # Preparar m煤sica de fondo en loop
44
- bg_music = AudioSegment.from_mp3(MUSIC_BG) - 10 # 10% volumen
45
- repeticiones = math.ceil(duracion_total * 1000 / len(bg_music))
46
- bg_music_loop = bg_music * repeticiones
47
- bg_music_final = bg_music_loop[:duracion_total*1000].fade_out(3000)
48
-
49
- # Combinar TTS (despu茅s de la intro) con m煤sica
50
- intro_duration = VideoFileClip(INTRO_VIDEO).duration * 1000 # Duraci贸n en ms
51
- audio_final = bg_music_final.overlay(tts_audio, position=intro_duration)
52
-
53
- # Guardar como WAV primero (para evitar problemas de formato)
54
- with tempfile.NamedTemporaryFile(delete=False, suffix=".wav") as tmp_wav:
55
- audio_final.export(tmp_wav.name, format="wav")
56
-
57
- # Convertir a MP3 (si es necesario)
58
- with tempfile.NamedTemporaryFile(delete=False, suffix=".mp3") as tmp_mp3:
59
- AudioSegment.from_wav(tmp_wav.name).export(tmp_mp3.name, format="mp3")
60
- return tmp_mp3.name
61
- except Exception as e:
62
- logging.error(f"Error procesando audio: {e}")
63
- raise
64
 
65
  def cortar_video(video_path, metodo="inteligente", duracion=10):
66
  try:
 
67
  video = VideoFileClip(video_path)
68
  if metodo == "manual":
69
- return [
70
- video.subclip(i*duracion, (i+1)*duracion)
71
- for i in range(math.ceil(video.duration/duracion))
72
- ]
73
 
74
- # Simulaci贸n de cortes autom谩ticos
75
  clips = []
76
  ultimo_corte = 0
77
  for i in range(1, math.ceil(video.duration)):
78
  if i % 5 == 0: # Simulaci贸n de pausas
79
  clips.append(video.subclip(ultimo_corte, i))
80
  ultimo_corte = i
81
- return clips if clips else [video]
 
82
  except Exception as e:
83
- logging.error(f"Error cortando video: {e}")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
84
  raise
85
 
86
  def agregar_transiciones(clips):
87
  try:
 
88
  fx_audio = AudioFileClip(FX_SOUND).set_duration(2.5)
89
- transicion = ImageClip(WATERMARK).set_duration(2.5).resize(height=clips[0].h).set_position(("center", 0.1))
 
 
 
90
 
91
  clips_con_fx = []
92
  for i, clip in enumerate(clips):
@@ -94,19 +106,19 @@ def agregar_transiciones(clips):
94
  clip_watermarked = CompositeVideoClip([clip, transicion])
95
  clips_con_fx.append(clip_watermarked)
96
 
97
- # Agregar transici贸n entre clips
98
- if i < len(clips)-1:
99
  clips_con_fx.append(
100
  CompositeVideoClip([transicion.set_position("center")])
101
  .set_audio(fx_audio)
102
  )
 
103
  return concatenate_videoclips(clips_con_fx)
104
  except Exception as e:
105
- logging.error(f"Error en transiciones: {e}")
106
  raise
107
 
108
  async def procesar_video(
109
- video_input,
110
  texto_tts,
111
  voz_seleccionada,
112
  metodo_corte,
@@ -114,31 +126,36 @@ async def procesar_video(
114
  ):
115
  temp_files = []
116
  try:
 
117
  # Procesar video principal
118
  clips = cortar_video(video_input, metodo_corte, duracion_corte)
119
  video_editado = agregar_transiciones(clips)
120
 
121
- # Agregar intro/outro
122
  intro = VideoFileClip(INTRO_VIDEO)
123
  outro = VideoFileClip(OUTRO_VIDEO)
124
- video_final = concatenate_videoclips([intro, video_editado, outro])
125
 
126
- # Calcular duraci贸n total para la m煤sica
127
- duracion_total = video_final.duration
 
 
 
 
 
128
 
129
- # Generar audio (m煤sica en loop + TTS despu茅s de intro)
130
- audio_mix_path = await procesar_audio(texto_tts, voz_seleccionada, duracion_total)
131
 
132
- # Combinar video y audio
133
- video_final = video_final.set_audio(AudioFileClip(audio_mix_path))
134
 
135
- # Renderizar
136
- with tempfile.NamedTemporaryFile(delete=False, suffix=".mp4") as tmp_out:
137
- video_final.write_videofile(tmp_out.name, codec="libx264", fps=24)
138
- temp_files.append(tmp_out.name)
139
- return tmp_out.name
140
  except Exception as e:
141
- logging.error(f"Error general: {e}")
142
  raise
143
  finally:
144
  # Eliminar archivos temporales
@@ -146,18 +163,19 @@ async def procesar_video(
146
  try:
147
  if os.path.exists(file):
148
  os.remove(file)
 
149
  except Exception as e:
150
- logging.warning(f"Error eliminando {file}: {e}")
151
 
152
  # Interfaz Gradio
153
  with gr.Blocks() as demo:
154
  gr.Markdown("# Video Editor IA")
155
 
156
  with gr.Tab("Principal"):
157
- video_input = gr.Video(label="Subir video") # Solo un video
158
  texto_tts = gr.Textbox(label="Texto para TTS", lines=3)
159
  voz_seleccionada = gr.Dropdown(
160
- label="Voz",
161
  choices=["es-ES-AlvaroNeural", "es-MX-BeatrizNeural"]
162
  )
163
  procesar_btn = gr.Button("Generar Video")
@@ -166,16 +184,26 @@ with gr.Blocks() as demo:
166
  with gr.Tab("Ajustes"):
167
  metodo_corte = gr.Radio(
168
  ["inteligente", "manual"],
169
- label="M茅todo de corte",
170
  value="inteligente"
171
  )
172
- duracion_corte = gr.Slider(1, 60, 10, label="Duraci贸n por corte (solo manual)")
 
 
 
173
 
174
  procesar_btn.click(
175
  procesar_video,
176
- inputs=[video_input, texto_tts, voz_seleccionada, metodo_corte, duracion_corte],
 
 
 
 
 
 
177
  outputs=video_output
178
  )
179
 
180
  if __name__ == "__main__":
 
181
  demo.queue().launch()
 
 
1
  import math
2
  import tempfile
3
  import logging
4
  from PIL import Image
5
+ import os
6
+ import asyncio
7
  from moviepy.editor import (
8
  VideoFileClip, AudioFileClip, ImageClip,
9
  concatenate_videoclips, CompositeVideoClip, CompositeAudioClip
10
  )
11
  import edge_tts
12
  import gradio as gr
13
+ from pydub import AudioSegment
14
+
15
+ # PATCH PARA PILLOW 10+ (cr铆tico)
16
+ Image.ANTIALIAS = Image.Resampling.LANCZOS # Parche antes de importar MoviePy
17
 
18
+ # Configuraci贸n de Logs
19
+ logging.basicConfig(level=logging.INFO, format="%(asctime)s - %(levelname)s - %(message)s")
20
 
21
+ # CONSTANTES DE ARCHIVOS (sin cambios en los nombres)
22
  INTRO_VIDEO = "introvideo.mp4"
23
  OUTRO_VIDEO = "outrovideo.mp4"
24
  MUSIC_BG = "musicafondo.mp3"
25
  FX_SOUND = "fxsound.mp3"
26
  WATERMARK = "watermark.png"
27
 
28
+ # Validar existencia de archivos obligatorios
29
  for file in [INTRO_VIDEO, OUTRO_VIDEO, MUSIC_BG, FX_SOUND, WATERMARK]:
30
  if not os.path.exists(file):
31
+ logging.error(f"Falta archivo necesario: {file}")
32
+ raise FileNotFoundError(f"Falta archivo necesario: {file}")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
33
 
34
  def cortar_video(video_path, metodo="inteligente", duracion=10):
35
  try:
36
+ logging.info("Iniciando corte de video...")
37
  video = VideoFileClip(video_path)
38
  if metodo == "manual":
39
+ clips = [video.subclip(i * duracion, (i + 1) * duracion)
40
+ for i in range(math.ceil(video.duration / duracion))]
41
+ logging.info(f"Video cortado en {len(clips)} clips manuales.")
42
+ return clips
43
 
44
+ # Simulaci贸n b谩sica de cortes autom谩ticos (puedes mejorar esto con VAD)
45
  clips = []
46
  ultimo_corte = 0
47
  for i in range(1, math.ceil(video.duration)):
48
  if i % 5 == 0: # Simulaci贸n de pausas
49
  clips.append(video.subclip(ultimo_corte, i))
50
  ultimo_corte = i
51
+ logging.info(f"Video cortado en {len(clips)} clips autom谩ticos.")
52
+ return clips
53
  except Exception as e:
54
+ logging.error(f"Error al cortar video: {e}")
55
+ raise
56
+
57
+ async def procesar_audio(texto, voz, duracion_total, duracion_intro, duracion_video_editado):
58
+ try:
59
+ logging.info("Generando TTS y mezclando audio...")
60
+ communicate = edge_tts.Communicate(texto, voz)
61
+ with tempfile.NamedTemporaryFile(delete=False, suffix=".mp3") as tmp:
62
+ await communicate.save(tmp.name) # Usar await en lugar de asyncio.run()
63
+ tts_audio = AudioFileClip(tmp.name)
64
+
65
+ # Duraci贸n natural del TTS
66
+ tts_duration = tts_audio.duration
67
+
68
+ # Ajustar TTS para que dure solo lo que dura el video editado
69
+ if tts_duration > duracion_video_editado:
70
+ tts_audio = tts_audio.subclip(0, duracion_video_editado)
71
+
72
+ # Mezclar con m煤sica de fondo
73
+ bg_music = AudioSegment.from_mp3(MUSIC_BG)
74
+ if len(bg_music) < duracion_total * 1000:
75
+ bg_music = bg_music * math.ceil(duracion_total * 1000 / len(bg_music))
76
+ bg_music = bg_music[:duracion_total * 1000].fade_out(3000)
77
+
78
+ with tempfile.NamedTemporaryFile(delete=False, suffix=".mp3") as tmp:
79
+ bg_music.export(tmp.name, format="mp3")
80
+ bg_audio = AudioFileClip(tmp.name).volumex(0.10)
81
+
82
+ # Combinar audios: bg_audio en toda la duraci贸n, tts_audio despu茅s del intro
83
+ audio_final = CompositeAudioClip([
84
+ bg_audio,
85
+ tts_audio.volumex(0.9).set_start(duracion_intro)
86
+ ])
87
+
88
+ logging.info("Audio procesado correctamente.")
89
+ return audio_final
90
+ except Exception as e:
91
+ logging.error(f"Error al procesar audio: {e}")
92
  raise
93
 
94
  def agregar_transiciones(clips):
95
  try:
96
+ logging.info("Agregando transiciones...")
97
  fx_audio = AudioFileClip(FX_SOUND).set_duration(2.5)
98
+ transicion = ImageClip(WATERMARK).set_duration(2.5)
99
+
100
+ # Redimensionar la transici贸n
101
+ transicion = transicion.resize(height=clips[0].h).set_position(("center", 0.1))
102
 
103
  clips_con_fx = []
104
  for i, clip in enumerate(clips):
 
106
  clip_watermarked = CompositeVideoClip([clip, transicion])
107
  clips_con_fx.append(clip_watermarked)
108
 
109
+ if i < len(clips) - 1:
 
110
  clips_con_fx.append(
111
  CompositeVideoClip([transicion.set_position("center")])
112
  .set_audio(fx_audio)
113
  )
114
+ logging.info("Transiciones agregadas correctamente.")
115
  return concatenate_videoclips(clips_con_fx)
116
  except Exception as e:
117
+ logging.error(f"Error al agregar transiciones: {e}")
118
  raise
119
 
120
  async def procesar_video(
121
+ video_input,
122
  texto_tts,
123
  voz_seleccionada,
124
  metodo_corte,
 
126
  ):
127
  temp_files = []
128
  try:
129
+ logging.info("Iniciando procesamiento de video...")
130
  # Procesar video principal
131
  clips = cortar_video(video_input, metodo_corte, duracion_corte)
132
  video_editado = agregar_transiciones(clips)
133
 
134
+ # Cargar intro y outro
135
  intro = VideoFileClip(INTRO_VIDEO)
136
  outro = VideoFileClip(OUTRO_VIDEO)
 
137
 
138
+ # Calcular duraciones
139
+ duracion_intro = intro.duration
140
+ duracion_video_editado = video_editado.duration
141
+ duracion_total = duracion_intro + duracion_video_editado + outro.duration
142
+
143
+ # Concatenar intro, video editado y outro
144
+ video_final = concatenate_videoclips([intro, video_editado, outro])
145
 
146
+ # Procesar audio
147
+ audio_final = await procesar_audio(texto_tts, voz_seleccionada, duracion_total, duracion_intro, duracion_video_editado)
148
 
149
+ # Combinar y renderizar
150
+ video_final = video_final.set_audio(audio_final)
151
 
152
+ with tempfile.NamedTemporaryFile(delete=False, suffix=".mp4") as tmp:
153
+ video_final.write_videofile(tmp.name, codec="libx264", fps=24)
154
+ logging.info("Video procesado y guardado temporalmente.")
155
+ temp_files.append(tmp.name)
156
+ return tmp.name
157
  except Exception as e:
158
+ logging.error(f"Error durante el procesamiento: {e}")
159
  raise
160
  finally:
161
  # Eliminar archivos temporales
 
163
  try:
164
  if os.path.exists(file):
165
  os.remove(file)
166
+ logging.info(f"Archivo temporal eliminado: {file}")
167
  except Exception as e:
168
+ logging.warning(f"No se pudo eliminar el archivo temporal {file}: {e}")
169
 
170
  # Interfaz Gradio
171
  with gr.Blocks() as demo:
172
  gr.Markdown("# Video Editor IA")
173
 
174
  with gr.Tab("Principal"):
175
+ video_input = gr.Video(label="Subir video")
176
  texto_tts = gr.Textbox(label="Texto para TTS", lines=3)
177
  voz_seleccionada = gr.Dropdown(
178
+ label="Seleccionar voz",
179
  choices=["es-ES-AlvaroNeural", "es-MX-BeatrizNeural"]
180
  )
181
  procesar_btn = gr.Button("Generar Video")
 
184
  with gr.Tab("Ajustes"):
185
  metodo_corte = gr.Radio(
186
  ["inteligente", "manual"],
187
+ label="M茅todo de cortes",
188
  value="inteligente"
189
  )
190
+ duracion_corte = gr.Slider(
191
+ 1, 60, 10,
192
+ label="Segundos por corte (solo manual)"
193
+ )
194
 
195
  procesar_btn.click(
196
  procesar_video,
197
+ inputs=[
198
+ video_input,
199
+ texto_tts,
200
+ voz_seleccionada,
201
+ metodo_corte,
202
+ duracion_corte
203
+ ],
204
  outputs=video_output
205
  )
206
 
207
  if __name__ == "__main__":
208
+ logging.info("Iniciando aplicaci贸n Gradio...")
209
  demo.queue().launch()